From e7bf591f0c1c6e3b5ac10e79d0c9dd6e35b65646 Mon Sep 17 00:00:00 2001 From: Mandy Chessell Date: Thu, 18 Jul 2024 10:50:39 +0100 Subject: [PATCH] Add custom class loader to OCF (#8288) Signed-off-by: Mandy Chessell --- bom/build.gradle | 4 +- .../connectors/ConnectorProviderBase.java | 20 ++- .../IsolatedConnectorClassLoader.java | 75 ++++++++++ .../IsolatedConnectorProviderBase.java | 133 ++++++++++++++++++ .../OMRSRepositoryConnectorProviderBase.java | 1 - 5 files changed, 225 insertions(+), 8 deletions(-) create mode 100644 open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/IsolatedConnectorClassLoader.java create mode 100644 open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/IsolatedConnectorProviderBase.java diff --git a/bom/build.gradle b/bom/build.gradle index 450bedd012f..4ae6f429035 100644 --- a/bom/build.gradle +++ b/bom/build.gradle @@ -24,7 +24,7 @@ ext { antlrVersion = '3.5.3' ST4Version = '4.3.4' avroVersion = '1.11.3' - xtdbVersion = '1.24.3' + xtdbVersion = '1.24.4' clojureVersion = '1.11.1' classgraphVersion = '4.8.172' classmateVersion = '1.5.1' @@ -75,7 +75,7 @@ ext { lang3Version = '3.14.0' logbackVersion = '1.5.6' lettuceVersion = '6.3.2.RELEASE' - // TODO: Version 9 now available + // TODO: Lucene Version 9 now available but changed the naming of Codec files and so does not work with XTDB luceneVersion = '8.11.3' openlineageVersion = '1.16.0' ossVersion = '4.16.0' diff --git a/open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/ConnectorProviderBase.java b/open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/ConnectorProviderBase.java index 7693fc3fa31..9cc53bdc229 100644 --- a/open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/ConnectorProviderBase.java +++ b/open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/ConnectorProviderBase.java @@ -5,8 +5,6 @@ import org.odpi.openmetadata.frameworks.auditlog.AuditLog; import org.odpi.openmetadata.frameworks.auditlog.AuditLoggingComponent; import org.odpi.openmetadata.frameworks.auditlog.ComponentDescription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.odpi.openmetadata.frameworks.connectors.ffdc.ConnectionCheckedException; import org.odpi.openmetadata.frameworks.connectors.ffdc.ConnectorCheckedException; import org.odpi.openmetadata.frameworks.connectors.ffdc.OCFErrorCode; @@ -14,6 +12,8 @@ import org.odpi.openmetadata.frameworks.connectors.properties.ConnectorTypeProperties; import org.odpi.openmetadata.frameworks.connectors.properties.beans.Connection; import org.odpi.openmetadata.frameworks.connectors.properties.beans.ConnectorType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; @@ -22,12 +22,10 @@ /** * ConnectorProviderBase is a base class for a connector provider. It manages all the class loading * for subclass implementations of the connector provider along with the generation of new connector guids. - * * ConnectorProviderBase creates a connector instance with the class name from the private variable called * connectorClassName. This class name is initialized to null. If the getConnector method is called when * the connectorClassName is null, it throws ConnectorCheckedException. * This is its default behaviour. - * * To use the ConnectorProviderBase, create a new class that extends the ConnectorProviderBase class * and in the constructor call super.setConnectorClassName("your connector's class name"); */ @@ -333,7 +331,7 @@ public Connector getConnector(ConnectionProperties connection) throws Connection */ try { - Class connectorClass = Class.forName(connectorClassName); + Class connectorClass = getClassForConnector(); Object potentialConnector = connectorClass.getDeclaredConstructor().newInstance(); connector = (Connector)potentialConnector; @@ -406,6 +404,18 @@ public Connector getConnector(ConnectionProperties connection) throws Connection } + /** + * Use the standard class loader to retrieve the class for the connector. + * + * @return class + * @throws ClassNotFoundException unable to locate a class by that name on the class path + */ + protected Class getClassForConnector() throws ClassNotFoundException + { + return Class.forName(connectorClassName); + } + + /** * Provide a common implementation of hashCode for all OCF Connector Provider objects. The UUID is unique and * is randomly assigned and so its hashCode is as good as anything to describe the hash code of the properties diff --git a/open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/IsolatedConnectorClassLoader.java b/open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/IsolatedConnectorClassLoader.java new file mode 100644 index 00000000000..3a15cab5d01 --- /dev/null +++ b/open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/IsolatedConnectorClassLoader.java @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* Copyright Contributors to the ODPi Egeria project. */ + +package org.odpi.openmetadata.frameworks.connectors; + + +import java.io.*; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.jar.JarFile; + +/** + * IsolatedConnectorClassLoader is used by a connector provider to create a connector instance that uses class + * implementations from its own JAR file rather than any class implementations that may have already been loaded. + */ +public class IsolatedConnectorClassLoader extends URLClassLoader +{ + private final ClassLoader defaultClassLoader; + private final JarFile jarfile; + private final String jarFileName; + private final String jarFileSpec; + + + public IsolatedConnectorClassLoader(String jarFileName) throws IOException + { + this(jarFileName, + Thread.currentThread().getContextClassLoader().getParent(), + Thread.currentThread().getContextClassLoader()); + } + + + /** + * Creates a new class loader of the specified name and using the + * specified parent class loader for delegation. + * + * @param jarFileName name of the jar file to load from + * @param jdkClassLoader the parent class loader for JRE classes + * @param defaultClassLoader the class loader to use if can not find class in JAR. + */ + protected IsolatedConnectorClassLoader(String jarFileName, + ClassLoader jdkClassLoader, + ClassLoader defaultClassLoader) throws IOException + { + super(new URL[]{}, jdkClassLoader); + this.defaultClassLoader = defaultClassLoader; + this. jarFileName = jarFileName; + this.jarfile = new JarFile(jarFileName); + super.addURL(new File(jarFileName).toURI().toURL()); + + this.jarFileSpec = "jar:file:" + jarFileName + "!/"; + super.addURL(new URL(jarFileSpec)); + } + + + /** + * This loads classes from the JDK, then the JAR file, then the default class loader. + * + * @param name The binary name of the class + * + * @return loaded class + * @throws ClassNotFoundException not found on the class path + */ + @Override + public Class loadClass(String name) throws ClassNotFoundException + { + try + { + return super.findClass(name); + } + catch (ClassNotFoundException notFound) + { + return defaultClassLoader.loadClass(name); + } + } +} diff --git a/open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/IsolatedConnectorProviderBase.java b/open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/IsolatedConnectorProviderBase.java new file mode 100644 index 00000000000..930f588de2d --- /dev/null +++ b/open-metadata-implementation/frameworks/open-connector-framework/src/main/java/org/odpi/openmetadata/frameworks/connectors/IsolatedConnectorProviderBase.java @@ -0,0 +1,133 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +/* Copyright Contributors to the ODPi Egeria project. */ + +package org.odpi.openmetadata.frameworks.connectors; + + +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; + +/** + * IsolatedConnectorProviderBase provides extensions to ConnectorProviderBase that uses a custom class loader to + * load the connector class in getConnector. This custom class loader give preference to the classes in the + * same JAR file as the connector provider's implementation over other classes on the class path. + * Note: + * this process assumes that the connector class is not already loaded through a reference in the connector providers + * implementation - ie private static final String connectorClassName should be set to a literal string. + * + */ +public class IsolatedConnectorProviderBase extends ConnectorProviderBase +{ + + /** + * Use a custom class loader to favour classes that are located in the connector implementation's JAR file. + * + * @return class + * @throws ClassNotFoundException unable to locate a class by that name on the class path + */ + protected Class getClassForConnector() throws ClassNotFoundException + { + try + { + IsolatedConnectorClassLoader isolatedConnectorClassLoader = new IsolatedConnectorClassLoader(this.getJARFileURL(this.getClass())); + + // return Class.forName(getConnectorClassName(), true, isolatedConnectorClassLoader); + + return isolatedConnectorClassLoader.loadClass(getConnectorClassName()); + } + catch (IOException error) + { + throw new ClassNotFoundException("Bad provider class name", error); + } + } + + + /** + * Extract the name of the JAR file from the provider's class. + * + * @param providerClass class for this connector provider + * @return JAR file name in URL form + * @throws MalformedURLException should not happen - this means the class was not loaded from a JAR file. + */ + private String getJARFileURL(Class providerClass) throws MalformedURLException + { + URL qualifiedClassURL = providerClass.getResource(providerClass.getSimpleName() + ".class"); + + if (qualifiedClassURL != null) + { + String qualifiedClassName = qualifiedClassURL.toString(); + String noClassQualifiedName = qualifiedClassName.split("!")[0]; + String[] noJarQualifiedNameSplit = noClassQualifiedName.split("jar:file:"); + + String noJarQualifiedName = noJarQualifiedNameSplit[noJarQualifiedNameSplit.length - 1]; + + return noJarQualifiedName; + } + + return null; + } + + + /** + * Return the path name of the Connector Provider's JAR file. + * + * @return file name of this connector provider's jar file + */ + private String getMyJARFilePath() + { + try + { + return byGetProtectionDomain(this.getClass()); + } + catch (Exception error) + { + // Cannot get jar file path using byGetProtectionDomain because the runtime does not permit it + } + + return byGetResource(this.getClass()); + } + + + + /** + * This method + * @param providerClass + * @return + * @throws URISyntaxException + */ + String byGetProtectionDomain(Class providerClass) throws URISyntaxException + { + URL url = providerClass.getProtectionDomain().getCodeSource().getLocation(); + return Paths.get(url.toURI()).toString(); + } + + String byGetResource(Class providerClass) + { + final URL classResource = providerClass.getResource(providerClass.getSimpleName() + ".class"); + if (classResource == null) + { + throw new RuntimeException("class resource is null"); + } + + final String url = classResource.toString(); + if (url.startsWith("jar:file:")) + { + // extract 'file:......jarName.jar' part from the url string + String path = url.replaceAll("^jar:(file:.*[.]jar)!/.*", "$1"); + try + { + return Paths.get(new URI(path)).toString(); + } + catch (Exception error) + { + throw new RuntimeException("Invalid Jar File URL String", error); + } + } + + throw new RuntimeException("Invalid Jar File URL String"); + } +} diff --git a/open-metadata-implementation/repository-services/repository-services-apis/src/main/java/org/odpi/openmetadata/repositoryservices/connectors/stores/metadatacollectionstore/repositoryconnector/OMRSRepositoryConnectorProviderBase.java b/open-metadata-implementation/repository-services/repository-services-apis/src/main/java/org/odpi/openmetadata/repositoryservices/connectors/stores/metadatacollectionstore/repositoryconnector/OMRSRepositoryConnectorProviderBase.java index dff3297fed5..a89ae7089f5 100644 --- a/open-metadata-implementation/repository-services/repository-services-apis/src/main/java/org/odpi/openmetadata/repositoryservices/connectors/stores/metadatacollectionstore/repositoryconnector/OMRSRepositoryConnectorProviderBase.java +++ b/open-metadata-implementation/repository-services/repository-services-apis/src/main/java/org/odpi/openmetadata/repositoryservices/connectors/stores/metadatacollectionstore/repositoryconnector/OMRSRepositoryConnectorProviderBase.java @@ -9,7 +9,6 @@ * The OMRSRepositoryConnectorProviderBase provides a base class for the connector provider supporting OMRS Connectors. * It adds no function but provides a placeholder for additional function if needed for the creation of * any OMRS Repository connectors. - * * It extends ConnectorProviderBase which does the creation of connector instances. The subclasses of * OMRSRepositoryConnectorProviderBase must initialize ConnectorProviderBase with the Java class * name of the OMRS Connector implementation (by calling super.setConnectorClassName(className)).