diff --git a/extension/persistence/eclipselink/build.gradle.kts b/extension/persistence/eclipselink/build.gradle.kts index 0c167f7ad2..96675b33be 100644 --- a/extension/persistence/eclipselink/build.gradle.kts +++ b/extension/persistence/eclipselink/build.gradle.kts @@ -59,12 +59,3 @@ dependencies { testImplementation(libs.mockito.core) testRuntimeOnly("org.junit.platform:junit-platform-launcher") } - -tasks.register("archiveConf") { - archiveFileName = "conf.jar" - destinationDirectory = layout.buildDirectory.dir("conf") - - from("src/main/resources/META-INF/") { include("persistence.xml") } -} - -tasks.named("test") { dependsOn("archiveConf") } diff --git a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java index 7106952144..7950075c94 100644 --- a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java +++ b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreSessionImpl.java @@ -18,9 +18,6 @@ */ package org.apache.polaris.extension.persistence.impl.eclipselink; -import static org.eclipse.persistence.config.PersistenceUnitProperties.ECLIPSELINK_PERSISTENCE_XML; -import static org.eclipse.persistence.config.PersistenceUnitProperties.JDBC_URL; - import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicates; import jakarta.annotation.Nonnull; @@ -29,14 +26,9 @@ import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityTransaction; import jakarta.persistence.OptimisticLockException; -import jakarta.persistence.Persistence; import jakarta.persistence.PersistenceException; -import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.HashMap; +import java.io.UncheckedIOException; import java.util.List; import java.util.Locale; import java.util.Map; @@ -45,13 +37,6 @@ import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.context.RealmContext; import org.apache.polaris.core.entity.PolarisBaseEntity; @@ -78,10 +63,6 @@ import org.apache.polaris.jpa.models.ModelPrincipalSecrets; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; /** * EclipseLink implementation of a Polaris metadata store supporting persisting and retrieving all @@ -141,56 +122,18 @@ private EntityManagerFactory createEntityManagerFactory( @Nullable String confFile, @Nullable String persistenceUnitName) { String realm = realmContext.getRealmIdentifier(); - EntityManagerFactory factory = realmFactories.getOrDefault(realm, null); - if (factory != null) { - return factory; - } - - ClassLoader prevClassLoader = Thread.currentThread().getContextClassLoader(); - try { - persistenceUnitName = persistenceUnitName == null ? "polaris" : persistenceUnitName; - confFile = confFile == null ? "META-INF/persistence.xml" : confFile; - - // Currently eclipseLink can only support configuration as a resource inside a jar. To support - // external configuration, persistence.xml needs be placed inside a jar and here is to add the - // jar to the classpath. - // Supported configuration file: META-INF/persistence.xml, /tmp/conf.jar!/persistence.xml - int splitPosition = confFile.indexOf("!/"); - if (splitPosition != -1) { - String jarPrefixPath = confFile.substring(0, splitPosition); - confFile = confFile.substring(splitPosition + 2); - URL prefixUrl = this.getClass().getClassLoader().getResource(jarPrefixPath); - if (prefixUrl == null) { - prefixUrl = new File(jarPrefixPath).toURI().toURL(); - } - - LOGGER.debug( - "Creating a new ClassLoader with the jar {} in classpath to load the config file", - prefixUrl); - - URLClassLoader currentClassLoader = - new URLClassLoader(new URL[] {prefixUrl}, this.getClass().getClassLoader()); - - LOGGER.debug("Update ClassLoader in current thread temporarily"); - Thread.currentThread().setContextClassLoader(currentClassLoader); - } - - Map properties = loadProperties(confFile, persistenceUnitName); - // Replace database name in JDBC URL with realm - if (properties.containsKey(JDBC_URL)) { - properties.put(JDBC_URL, properties.get(JDBC_URL).replace("{realm}", realm)); - } - properties.put(ECLIPSELINK_PERSISTENCE_XML, confFile); - - factory = Persistence.createEntityManagerFactory(persistenceUnitName, properties); - realmFactories.putIfAbsent(realm, factory); - - return factory; - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - Thread.currentThread().setContextClassLoader(prevClassLoader); - } + return realmFactories.computeIfAbsent( + realm, + key -> { + try { + PolarisEclipseLinkPersistenceUnit persistenceUnit = + PolarisEclipseLinkPersistenceUnit.locatePersistenceUnit( + confFile, persistenceUnitName); + return persistenceUnit.createEntityManagerFactory(realmContext); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); } @VisibleForTesting @@ -198,42 +141,6 @@ static void clearEntityManagerFactories() { realmFactories.clear(); } - /** Load the persistence unit properties from a given configuration file */ - private Map loadProperties( - @Nonnull String confFile, @Nonnull String persistenceUnitName) throws IOException { - try { - InputStream input = - Thread.currentThread().getContextClassLoader().getResourceAsStream(confFile); - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document doc = builder.parse(input); - XPath xPath = XPathFactory.newInstance().newXPath(); - String expression = - "/persistence/persistence-unit[@name='" + persistenceUnitName + "']/properties/property"; - NodeList nodeList = - (NodeList) xPath.compile(expression).evaluate(doc, XPathConstants.NODESET); - Map properties = new HashMap<>(); - for (int i = 0; i < nodeList.getLength(); i++) { - NamedNodeMap nodeMap = nodeList.item(i).getAttributes(); - properties.put( - nodeMap.getNamedItem("name").getNodeValue(), - nodeMap.getNamedItem("value").getNodeValue()); - } - - return properties; - } catch (XPathExpressionException - | ParserConfigurationException - | SAXException - | IOException e) { - String str = - String.format( - "Cannot find or parse the configuration file %s for persistence-unit %s", - confFile, persistenceUnitName); - LOGGER.error(str, e); - throw new IOException(str); - } - } - /** {@inheritDoc} */ @Override public T runInTransaction( diff --git a/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkPersistenceUnit.java b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkPersistenceUnit.java new file mode 100644 index 0000000000..7ea8749fd4 --- /dev/null +++ b/extension/persistence/eclipselink/src/main/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkPersistenceUnit.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.extension.persistence.impl.eclipselink; + +import static org.eclipse.persistence.config.PersistenceUnitProperties.ECLIPSELINK_PERSISTENCE_XML; +import static org.eclipse.persistence.config.PersistenceUnitProperties.JDBC_URL; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Persistence; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.extension.persistence.impl.eclipselink.PolarisEclipseLinkPersistenceUnit.ClasspathResourcePolarisEclipseLinkPersistenceUnit; +import org.apache.polaris.extension.persistence.impl.eclipselink.PolarisEclipseLinkPersistenceUnit.FileSystemPolarisEclipseLinkPersistenceUnit; +import org.apache.polaris.extension.persistence.impl.eclipselink.PolarisEclipseLinkPersistenceUnit.JarFilePolarisEclipseLinkPersistenceUnit; +import org.w3c.dom.Document; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +sealed interface PolarisEclipseLinkPersistenceUnit + permits ClasspathResourcePolarisEclipseLinkPersistenceUnit, + FileSystemPolarisEclipseLinkPersistenceUnit, + JarFilePolarisEclipseLinkPersistenceUnit { + + EntityManagerFactory createEntityManagerFactory(RealmContext realmContext) throws IOException; + + record ClasspathResourcePolarisEclipseLinkPersistenceUnit( + URL resource, String resourceName, String persistenceUnitName) + implements PolarisEclipseLinkPersistenceUnit { + + @Override + public EntityManagerFactory createEntityManagerFactory(RealmContext realmContext) + throws IOException { + Map properties = loadProperties(resource, persistenceUnitName, realmContext); + properties.put(ECLIPSELINK_PERSISTENCE_XML, resourceName); + return Persistence.createEntityManagerFactory(persistenceUnitName, properties); + } + } + + record FileSystemPolarisEclipseLinkPersistenceUnit(Path path, String persistenceUnitName) + implements PolarisEclipseLinkPersistenceUnit { + + @Override + public EntityManagerFactory createEntityManagerFactory(RealmContext realmContext) + throws IOException { + Map properties = + loadProperties(path.toUri().toURL(), persistenceUnitName, realmContext); + Path archiveDirectory = path.getParent(); + String descriptorPath = archiveDirectory.getParent().relativize(path).toString(); + properties.put(ECLIPSELINK_PERSISTENCE_XML, descriptorPath); + ClassLoader prevClassLoader = Thread.currentThread().getContextClassLoader(); + try (URLClassLoader currentClassLoader = + new URLClassLoader( + new URL[] {archiveDirectory.getParent().toUri().toURL()}, + this.getClass().getClassLoader())) { + Thread.currentThread().setContextClassLoader(currentClassLoader); + return Persistence.createEntityManagerFactory(persistenceUnitName, properties); + } finally { + Thread.currentThread().setContextClassLoader(prevClassLoader); + } + } + } + + record JarFilePolarisEclipseLinkPersistenceUnit( + URL confUrl, URL jarUrl, String descriptorPath, String persistenceUnitName) + implements PolarisEclipseLinkPersistenceUnit { + + @Override + public EntityManagerFactory createEntityManagerFactory(RealmContext realmContext) + throws IOException { + Map properties = loadProperties(confUrl, persistenceUnitName, realmContext); + properties.put(ECLIPSELINK_PERSISTENCE_XML, descriptorPath); + ClassLoader prevClassLoader = Thread.currentThread().getContextClassLoader(); + try (URLClassLoader currentClassLoader = + new URLClassLoader(new URL[] {jarUrl}, this.getClass().getClassLoader())) { + Thread.currentThread().setContextClassLoader(currentClassLoader); + return Persistence.createEntityManagerFactory(persistenceUnitName, properties); + } finally { + Thread.currentThread().setContextClassLoader(prevClassLoader); + } + } + } + + static PolarisEclipseLinkPersistenceUnit locatePersistenceUnit( + String confFile, String persistenceUnitName) throws IOException { + if (persistenceUnitName == null) { + persistenceUnitName = "polaris"; + } + if (confFile == null) { + confFile = "META-INF/persistence.xml"; + } + // Try an embedded config file first + int splitPosition = confFile.indexOf("!/"); + if (splitPosition != -1) { + String jarPrefix = confFile.substring(0, splitPosition); + String descriptorPath = confFile.substring(splitPosition + 2); + URL jarUrl = classpathResource(jarPrefix); + if (jarUrl != null) { + // The JAR is in the classpath + URL confUrl = URI.create("jar:" + jarUrl + "!/" + descriptorPath).toURL(); + return new ClasspathResourcePolarisEclipseLinkPersistenceUnit( + confUrl, confFile, persistenceUnitName); + } else { + // The JAR is in the filesystem + jarUrl = fileSystemPath(jarPrefix).toUri().toURL(); + URL confUrl = URI.create("jar:" + jarUrl + "!/" + descriptorPath).toURL(); + return new JarFilePolarisEclipseLinkPersistenceUnit( + confUrl, jarUrl, descriptorPath, persistenceUnitName); + } + } + // Try a classpath resource next + URL resource = classpathResource(confFile); + if (resource != null) { + return new ClasspathResourcePolarisEclipseLinkPersistenceUnit( + resource, confFile, persistenceUnitName); + } + // Try a filesystem path last + try { + return new FileSystemPolarisEclipseLinkPersistenceUnit( + fileSystemPath(confFile), persistenceUnitName); + } catch (Exception e) { + throw new IllegalStateException("Cannot find classpath resource or file: " + confFile, e); + } + } + + private static Path fileSystemPath(String pathStr) { + Path path = Paths.get(pathStr); + if (!Files.exists(path) || !Files.isRegularFile(path)) { + throw new IllegalStateException("Not a regular file: " + pathStr); + } + return path.normalize().toAbsolutePath(); + } + + @Nullable + private static URL classpathResource(String resourceName) throws IOException { + Enumeration resources = + Thread.currentThread().getContextClassLoader().getResources(resourceName); + if (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + if (resources.hasMoreElements()) { + throw new IllegalStateException( + "Multiple resources found in classpath for " + resourceName); + } + return resource; + } + return null; + } + + /** Load the persistence unit properties from a given configuration file */ + private static Map loadProperties( + @Nonnull URL confFile, + @Nonnull String persistenceUnitName, + @Nonnull RealmContext realmContext) + throws IOException { + try (InputStream input = confFile.openStream()) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document doc = builder.parse(input); + XPath xPath = XPathFactory.newInstance().newXPath(); + String expression = + "/persistence/persistence-unit[@name='" + persistenceUnitName + "']/properties/property"; + NodeList nodeList = + (NodeList) xPath.compile(expression).evaluate(doc, XPathConstants.NODESET); + Map properties = new HashMap<>(); + for (int i = 0; i < nodeList.getLength(); i++) { + NamedNodeMap nodeMap = nodeList.item(i).getAttributes(); + properties.put( + nodeMap.getNamedItem("name").getNodeValue(), + nodeMap.getNamedItem("value").getNodeValue()); + } + // Replace database name in JDBC URL with realm + if (properties.containsKey(JDBC_URL)) { + properties.put( + JDBC_URL, + properties.get(JDBC_URL).replace("{realm}", realmContext.getRealmIdentifier())); + } + return properties; + } catch (XPathExpressionException + | ParserConfigurationException + | SAXException + | IOException e) { + String str = + String.format( + "Cannot find or parse the configuration file %s for persistence-unit %s", + confFile, persistenceUnitName); + throw new IOException(str, e); + } + } +} diff --git a/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java b/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java index bbee4e3788..9fbf216702 100644 --- a/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java +++ b/extension/persistence/eclipselink/src/test/java/org/apache/polaris/extension/persistence/impl/eclipselink/PolarisEclipseLinkMetaStoreManagerTest.java @@ -25,7 +25,11 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.ZoneId; +import java.util.Objects; import java.util.stream.Stream; import org.apache.polaris.core.PolarisCallContext; import org.apache.polaris.core.PolarisConfigurationStore; @@ -204,11 +208,39 @@ void testRotateLegacyPrincipalSecret() { private static class CreateStoreSessionArgs implements ArgumentsProvider { @Override - public Stream provideArguments(ExtensionContext extensionContext) { + public Stream provideArguments(ExtensionContext extensionContext) + throws URISyntaxException { + Path persistenceXml = + Paths.get( + Objects.requireNonNull(getClass().getResource("/META-INF/persistence.xml")).toURI()); + Path confJar = + Paths.get( + Objects.requireNonNull( + getClass() + .getResource( + "/org/apache/polaris/extension/persistence/impl/eclipselink/test-conf.jar")) + .toURI()); return Stream.of( + // conf file not provided + Arguments.of(null, true), + // classpath resource Arguments.of("META-INF/persistence.xml", true), - Arguments.of("./build/conf/conf.jar!/persistence.xml", true), - Arguments.of("/dummy_path/conf.jar!/persistence.xml", false)); + Arguments.of("META-INF/dummy.xml", false), + // classpath resource, embedded + Arguments.of( + "org/apache/polaris/extension/persistence/impl/eclipselink/test-conf.jar!/persistence.xml", + true), + Arguments.of( + "org/apache/polaris/extension/persistence/impl/eclipselink/test-conf.jar!/dummy.xml", + false), + Arguments.of("dummy/test-conf.jar!/persistence.xml", false), + // filesystem path + Arguments.of(persistenceXml.toString(), true), + Arguments.of("/dummy_path/conf/persistence.xml", false), + // filesystem path, embedded + Arguments.of(confJar + "!/persistence.xml", true), + Arguments.of(confJar + "!/dummy.xml", false), + Arguments.of("/dummy_path/test-conf.jar!/persistence.xml", false)); } } } diff --git a/extension/persistence/eclipselink/src/test/resources/org/apache/polaris/extension/persistence/impl/eclipselink/test-conf.jar b/extension/persistence/eclipselink/src/test/resources/org/apache/polaris/extension/persistence/impl/eclipselink/test-conf.jar new file mode 100644 index 0000000000..2e42cc1191 Binary files /dev/null and b/extension/persistence/eclipselink/src/test/resources/org/apache/polaris/extension/persistence/impl/eclipselink/test-conf.jar differ