You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@polaris.apache.org by em...@apache.org on 2025/01/06 17:49:08 UTC

(polaris) branch main updated: EclipseLink: allow reading persistence.xml from the filesystem (#613)

This is an automated email from the ASF dual-hosted git repository.

emaynard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 6bda0782 EclipseLink: allow reading persistence.xml from the filesystem (#613)
6bda0782 is described below

commit 6bda07826d4ffec145b2bc362fbdf037f0e4771c
Author: Alexandre Dutra <ad...@users.noreply.github.com>
AuthorDate: Mon Jan 6 18:49:04 2025 +0100

    EclipseLink: allow reading persistence.xml from the filesystem (#613)
---
 extension/persistence/eclipselink/build.gradle.kts |   9 -
 .../PolarisEclipseLinkMetaStoreSessionImpl.java    | 119 ++---------
 .../PolarisEclipseLinkPersistenceUnit.java         | 224 +++++++++++++++++++++
 .../PolarisEclipseLinkMetaStoreManagerTest.java    |  38 +++-
 .../persistence/impl/eclipselink/test-conf.jar     | Bin 0 -> 1434 bytes
 5 files changed, 272 insertions(+), 118 deletions(-)

diff --git a/extension/persistence/eclipselink/build.gradle.kts b/extension/persistence/eclipselink/build.gradle.kts
index 0c167f7a..96675b33 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<Jar>("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 71069521..7950075c 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.EntityManager;
 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.Function;
 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.ModelGrantRecord;
 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 @@ public class PolarisEclipseLinkMetaStoreSessionImpl implements PolarisMetaStoreS
       @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<String, String> 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 @@ public class PolarisEclipseLinkMetaStoreSessionImpl implements PolarisMetaStoreS
     realmFactories.clear();
   }
 
-  /** Load the persistence unit properties from a given configuration file */
-  private Map<String, String> 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<String, String> 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> 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 00000000..7ea8749f
--- /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<String, String> 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<String, String> 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<String, String> 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<URL> 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<String, String> 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<String, String> 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 bbee4e37..9fbf2167 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.assertFalse;
 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 @@ public class PolarisEclipseLinkMetaStoreManagerTest extends BasePolarisMetaStore
 
   private static class CreateStoreSessionArgs implements ArgumentsProvider {
     @Override
-    public Stream<? extends Arguments> provideArguments(ExtensionContext extensionContext) {
+    public Stream<? extends Arguments> 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 00000000..2e42cc11
Binary files /dev/null and b/extension/persistence/eclipselink/src/test/resources/org/apache/polaris/extension/persistence/impl/eclipselink/test-conf.jar differ