http://git-wip-us.apache.org/repos/asf/metron/blob/5f7454e4/bundles-lib/src/main/java/org/apache/metron/bundles/BundleSystem.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/BundleSystem.java b/bundles-lib/src/main/java/org/apache/metron/bundles/BundleSystem.java new file mode 100644 index 0000000..7e93044 --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/BundleSystem.java @@ -0,0 +1,202 @@ +/* + * 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.metron.bundles; + +import com.google.common.annotations.VisibleForTesting; +import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSystemManager; +import org.apache.metron.bundles.bundle.Bundle; +import org.apache.metron.bundles.util.BundleProperties; +import org.apache.metron.bundles.util.FileSystemManagerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * High level interface to the Bundle System. While you may want to use the lower level classes it + * is not required, as BundleSystem provides the base required interface for initializing the system + * and instantiating classes + */ +public class BundleSystem { + + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + /** + * Builder for a BundleSystem. only {@link BundleProperties} are required. Beyond that, the + * BundleProperties, if they are the only parameter must have archive extension and bundle + * extension types properties present. + */ + public static class Builder { + + private BundleProperties properties; + private FileSystemManager fileSystemManager; + private List<Class> extensionClasses = new LinkedList<>(); + private Bundle systemBundle; + + /** + * The BundleProperties to use. Unless other builder parameters override options + * (withExtensionClasses ), they must have archive extension and bundle extensions types + * specified + * + * @param properties The BundleProperties + * @return Builder + */ + public Builder withBundleProperties(BundleProperties properties) { + this.properties = properties; + return this; + } + + /** + * Provide a {@link FileSystemManager} to overide the default + * + * @param fileSystemManager override + * @return Builder + */ + public Builder withFileSystemManager(FileSystemManager fileSystemManager) { + this.fileSystemManager = fileSystemManager; + return this; + } + + /** + * Provide Extension Classes. If not provided with this override then the classes will be + * configured from the BundleProperties. If provided, the properties file will not be used for + * classes. + * + * @param extensionClasses override + * @return Builder + */ + public Builder withExtensionClasses(List<Class> extensionClasses) { + this.extensionClasses.addAll(extensionClasses); + return this; + } + + /** + * Provide a SystemBundle. If not provided with this override then the default SystemBundle + * will be created. + */ + public Builder withSystemBundle(Bundle systemBundle) { + this.systemBundle = systemBundle; + return this; + } + + /** + * Builds a new BundleSystem. + * + * @return BundleSystem + * @throws NotInitializedException if any errors happen during build + */ + public BundleSystem build() throws NotInitializedException { + if (this.properties == null) { + throw new IllegalArgumentException("BundleProperties are required"); + } + try { + if (this.fileSystemManager == null) { + this.fileSystemManager = FileSystemManagerFactory + .createFileSystemManager(new String[]{properties.getArchiveExtension()}); + } + if (this.extensionClasses.isEmpty()) { + properties.getBundleExtensionTypes().forEach((x, y) -> { + try { + this.extensionClasses.add(Class.forName(y)); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + }); + } + if (this.systemBundle == null) { + this.systemBundle = ExtensionManager + .createSystemBundle(this.fileSystemManager, this.properties); + } + List<URI> libDirs = properties.getBundleLibraryDirectories(); + List<FileObject> libFileObjects = new ArrayList<>(); + libDirs.forEach((x) -> { + try { + FileObject fileObject = fileSystemManager.resolveFile(x); + if (fileObject.exists()) { + libFileObjects.add(fileObject); + } + } catch (Exception e) { + throw new IllegalStateException(e); + } + }); + + // initialize the Bundle System + BundleClassLoaders.init(fileSystemManager, libFileObjects, properties); + ExtensionManager + .init(extensionClasses, systemBundle, BundleClassLoaders.getInstance().getBundles()); + return new BundleSystem(fileSystemManager, extensionClasses, systemBundle, properties); + } catch (Exception e) { + throw new NotInitializedException(e); + } + } + + + } + + private final BundleProperties properties; + private final FileSystemManager fileSystemManager; + private final List<Class> extensionClasses; + private final Bundle systemBundle; + + private BundleSystem(FileSystemManager fileSystemManager, List<Class> extensionClasses, + Bundle systemBundle, BundleProperties properties) { + this.properties = properties; + this.fileSystemManager = fileSystemManager; + this.extensionClasses = extensionClasses; + this.systemBundle = systemBundle; + } + + /** + * Constructs an instance of the given type using either default no args constructor or a + * constructor which takes a BundleProperties object. + * + * @param specificClassName the implementation class name + * @param clazz the type (T) to create an instance for + * @return an instance of specificClassName which extends T + * @throws ClassNotFoundException if the class cannot be found + * @throws InstantiationException if the class cannot be instantiated + */ + public <T> T createInstance(final String specificClassName, final Class<T> clazz) + throws ClassNotFoundException, InstantiationException, + NotInitializedException, IllegalAccessException { + return BundleThreadContextClassLoader.createInstance(specificClassName, clazz, this.properties); + } + + @SuppressWarnings("unchecked") + public <T> Set<Class<? extends T>> getExtensionsClassesForExtensionType(final Class<T> extensionType) + throws NotInitializedException { + Set<Class<? extends T>> set = new HashSet<Class<? extends T>>(); + ExtensionManager.getInstance().getExtensions(extensionType).forEach((x) -> { + set.add((Class<T>)x); + }); + return set; + } + + @VisibleForTesting() + public static void reset() { + BundleClassLoaders.reset(); + ExtensionManager.reset(); + } + +}
http://git-wip-us.apache.org/repos/asf/metron/blob/5f7454e4/bundles-lib/src/main/java/org/apache/metron/bundles/BundleThreadContextClassLoader.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/BundleThreadContextClassLoader.java b/bundles-lib/src/main/java/org/apache/metron/bundles/BundleThreadContextClassLoader.java new file mode 100644 index 0000000..6d3d104 --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/BundleThreadContextClassLoader.java @@ -0,0 +1,211 @@ +/* + * 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.metron.bundles; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.metron.bundles.bundle.Bundle; +import org.apache.metron.bundles.util.BundleProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BundleThreadContextClassLoader extends URLClassLoader { + private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + static final ContextSecurityManager contextSecurityManager = new ContextSecurityManager(); + private final ClassLoader forward = ClassLoader.getSystemClassLoader(); + + private BundleThreadContextClassLoader() { + super(new URL[0]); + } + + @Override + public void clearAssertionStatus() { + lookupClassLoader().clearAssertionStatus(); + } + + @Override + public URL getResource(String name) { + return lookupClassLoader().getResource(name); + } + + @Override + public InputStream getResourceAsStream(String name) { + return lookupClassLoader().getResourceAsStream(name); + } + + @Override + public Enumeration<URL> getResources(String name) throws IOException { + return lookupClassLoader().getResources(name); + } + + @Override + public Class<?> loadClass(String name) throws ClassNotFoundException { + return lookupClassLoader().loadClass(name); + } + + @Override + public void setClassAssertionStatus(String className, boolean enabled) { + lookupClassLoader().setClassAssertionStatus(className, enabled); + } + + @Override + public void setDefaultAssertionStatus(boolean enabled) { + lookupClassLoader().setDefaultAssertionStatus(enabled); + } + + @Override + public void setPackageAssertionStatus(String packageName, boolean enabled) { + lookupClassLoader().setPackageAssertionStatus(packageName, enabled); + } + + private ClassLoader lookupClassLoader() { + final Class<?>[] classStack = contextSecurityManager.getExecutionStack(); + + for (Class<?> currentClass : classStack) { + final Class<?> bundleClass = findBundleClass(currentClass); + if (bundleClass != null) { + final ClassLoader desiredClassLoader = bundleClass.getClassLoader(); + + // When new Threads are created, the new Thread inherits the ClassLoaderContext of + // the caller. However, the call stack of that new Thread may not trace back to any app-specific + // code. Therefore, the BundleThreadContextClassLoader will be unable to find the appropriate Bundle + // ClassLoader. As a result, we want to set the ContextClassLoader to the Bundle ClassLoader that + // contains the class or resource that we are looking for. + // This locks the current Thread into the appropriate Bundle ClassLoader Context. The framework will change + // the ContextClassLoader back to the BundleThreadContextClassLoader as appropriate via the + // + // TL;DR + // We need to make sure the classloader for the thread is setup correctly to use the bundle classloader + // before we return the class. + // Just looking the class up is not enough. + // + // {@link FlowEngine.beforeExecute(Thread, Runnable)} and + // {@link FlowEngine.afterExecute(Thread, Runnable)} methods. + if (desiredClassLoader instanceof VFSBundleClassLoader) { + Thread.currentThread().setContextClassLoader(desiredClassLoader); + } + return desiredClassLoader; + } + } + return forward; + } + + private Class<?> findBundleClass(final Class<?> cls) { + try { + for (final Class<?> bundleClass : ExtensionManager.getInstance() + .getExtensionClasses()) { + if (bundleClass.isAssignableFrom(cls)) { + return cls; + } else if (cls.getEnclosingClass() != null) { + return findBundleClass(cls.getEnclosingClass()); + } + } + }catch(NotInitializedException e){ + LOG.error("ExtensionManager not initialized",e); + } + + return null; + } + + private static class SingletonHolder { + + public static final BundleThreadContextClassLoader instance = new BundleThreadContextClassLoader(); + } + + public static BundleThreadContextClassLoader getInstance() { + return SingletonHolder.instance; + } + + static class ContextSecurityManager extends SecurityManager { + + Class<?>[] getExecutionStack() { + return getClassContext(); + } + } + + /** + * Constructs an instance of the given type using either default no args + * constructor or a constructor which takes a BundleProperties object + * (preferred). + * + * @param <T> the type to create an instance for + * @param implementationClassName the implementation class name + * @param typeDefinition the type definition + * @param bundleProperties the BundleProperties instance + * @return constructed instance + * @throws InstantiationException if there is an error instantiating the class + * @throws IllegalAccessException if there is an error accessing the type + * @throws ClassNotFoundException if the class cannot be found + */ + public static <T> T createInstance(final String implementationClassName, final Class<T> typeDefinition, final BundleProperties bundleProperties) + throws InstantiationException, IllegalAccessException, ClassNotFoundException, NotInitializedException { + final ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(BundleThreadContextClassLoader.getInstance()); + try { + final List<Bundle> bundles = ExtensionManager.getInstance().getBundles(implementationClassName); + if (bundles.size() == 0) { + throw new IllegalStateException(String.format("The specified implementation class '%s' is not known.", implementationClassName)); + } + if (bundles.size() > 1) { + throw new IllegalStateException(String.format("More than one bundle was found for the specified implementation class '%s', only one is allowed.", implementationClassName)); + } + + final Bundle bundle = bundles.get(0); + final ClassLoader detectedClassLoaderForType = bundle.getClassLoader(); + final Class<?> rawClass = Class.forName(implementationClassName, true, detectedClassLoaderForType); + + Thread.currentThread().setContextClassLoader(detectedClassLoaderForType); + final Class<?> desiredClass = rawClass.asSubclass(typeDefinition); + if(bundleProperties == null){ + return typeDefinition.cast(desiredClass.newInstance()); + } + Constructor<?> constructor = null; + + try { + constructor = desiredClass.getConstructor(BundleProperties.class); + } catch (NoSuchMethodException nsme) { + try { + constructor = desiredClass.getConstructor(); + } catch (NoSuchMethodException nsme2) { + throw new IllegalStateException("Failed to find constructor which takes BundleProperties as argument as well as the default constructor on " + + desiredClass.getName(), nsme2); + } + } + try { + if (constructor.getParameterTypes().length == 0) { + return typeDefinition.cast(constructor.newInstance()); + } else { + return typeDefinition.cast(constructor.newInstance(bundleProperties)); + } + } catch (InvocationTargetException ite) { + throw new IllegalStateException("Failed to instantiate a component due to (see target exception)", ite); + } + } finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/5f7454e4/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionManager.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionManager.java b/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionManager.java new file mode 100644 index 0000000..5eb82c6 --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionManager.java @@ -0,0 +1,534 @@ +/* + * 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.metron.bundles; + + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSystemException; +import org.apache.commons.vfs2.FileSystemManager; +import org.apache.metron.bundles.bundle.Bundle; +import org.apache.metron.bundles.bundle.BundleCoordinates; +import org.apache.metron.bundles.bundle.BundleDetails; +import org.apache.metron.bundles.util.BundleProperties; +import org.apache.metron.bundles.util.DummyFileObject; +import org.apache.metron.bundles.util.FileUtils; +import org.apache.metron.bundles.util.ImmutableCollectionUtils; +import org.apache.metron.bundles.util.StringUtils; +import org.apache.metron.bundles.annotation.behavior.RequiresInstanceClassLoading; + +import org.atteo.classindex.ClassIndex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A Singleton class for scanning through the classpath to load all extension components using + * the ClassIndex and running through all classloaders (root, BUNDLEs). + * + * + * + */ +@SuppressWarnings("rawtypes") +public class ExtensionManager { + + private static volatile ExtensionManager extensionManager; + private static volatile InitContext initContext; + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public static final BundleCoordinates SYSTEM_BUNDLE_COORDINATE = new BundleCoordinates( + BundleCoordinates.DEFAULT_GROUP, "system", BundleCoordinates.DEFAULT_VERSION); + + private static final class InitContext { + + // Maps a service definition (interface) to those classes that implement the interface + private final Map<Class, Set<Class>> definitionMap; + private final Map<String, List<Bundle>> classNameBundleLookup; + private final Map<BundleCoordinates, Bundle> bundleCoordinateBundleLookup; + private final Map<ClassLoader, Bundle> classLoaderBundleLookup; + private final Set<String> requiresInstanceClassLoading; + private final Map<String, ClassLoader> instanceClassloaderLookup; + + private InitContext(Map<Class, Set<Class>> definitionMap, + Map<String, List<Bundle>> classNameBundleLookup, + Map<BundleCoordinates, Bundle> bundleCoordinateBundleLookup, + Map<ClassLoader, Bundle> classLoaderBundleLookup, + Set<String> requiresInstanceClassLoading, + Map<String, ClassLoader> instanceClassloaderLookup) { + + this.definitionMap = ImmutableCollectionUtils.immutableMapOfSets(definitionMap); + this.classNameBundleLookup = ImmutableCollectionUtils + .immutableMapOfLists(classNameBundleLookup); + this.bundleCoordinateBundleLookup = ImmutableMap.copyOf(bundleCoordinateBundleLookup); + this.classLoaderBundleLookup = ImmutableMap.copyOf(classLoaderBundleLookup); + this.requiresInstanceClassLoading = ImmutableSet.copyOf(requiresInstanceClassLoading); + this.instanceClassloaderLookup = new ConcurrentHashMap<>(instanceClassloaderLookup); + } + } + + private ExtensionManager(){} + + /** + * @return The singleton instance of the ExtensionManager + */ + public static ExtensionManager getInstance() throws NotInitializedException { + ExtensionManager result = extensionManager; + if (result == null) { + throw new NotInitializedException("ExtensionManager not initialized"); + } + return result; + } + + /** + * Uninitializes the ExtensionManager. + * TESTING ONLY + */ + @VisibleForTesting + public static void reset() { + synchronized (ExtensionManager.class) { + initContext = null; + extensionManager = null; + } + } + + /** + * Loads all extension class types that can be found on the bootstrap classloader and by creating + * classloaders for all BUNDLES found within the classpath. + * + * @param bundles the bundles to scan through in search of extensions + */ + public static void init(final List<Class> classes, final Bundle systemBundle, final Set<Bundle> bundles) + throws NotInitializedException { + + if (systemBundle == null) { + throw new IllegalArgumentException("systemBundle is required"); + } + + synchronized (ExtensionManager.class) { + if (extensionManager != null) { + throw new IllegalStateException("ExtensionManager already exists"); + } + ExtensionManager em = new ExtensionManager(); + InitContext ic = em.discoverExtensions(classes, systemBundle, bundles); + initContext = ic; + extensionManager = em; + } + } + + private InitContext discoverExtensions(final List<Class> classes, final Bundle systemBundle, final Set<Bundle> bundles) { + + if (classes == null || classes.size() == 0) { + throw new IllegalArgumentException("classes must be defined"); + } + // get the current context class loader + ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader(); + + final Map<Class, Set<Class>> definitionMap = new HashMap<>(); + final Map<String, List<Bundle>> classNameBundleLookup = new HashMap<>(); + final Map<BundleCoordinates, Bundle> bundleCoordinateBundleLookup = new HashMap<>(); + final Map<ClassLoader, Bundle> classLoaderBundleLookup = new HashMap<>(); + final Set<String> requiresInstanceClassLoading = new HashSet<>(); + final Map<String, ClassLoader> instanceClassloaderLookup = new HashMap<>(); + + for(Class c : classes) { + definitionMap.put(c,new HashSet<>()); + } + // load the system bundle first so that any extensions found in JARs directly in lib will be registered as + // being from the system bundle and not from all the other Bundles + loadExtensions(systemBundle, definitionMap, classNameBundleLookup, requiresInstanceClassLoading); + bundleCoordinateBundleLookup.put(systemBundle.getBundleDetails().getCoordinates(), systemBundle); + classLoaderBundleLookup.put(systemBundle.getClassLoader(),systemBundle); + // consider each bundle class loader + for (final Bundle bundle : bundles) { + // Must set the context class loader to the bundle classloader itself + // so that static initialization techniques that depend on the context class loader will work properly + final ClassLoader bcl = bundle.getClassLoader(); + // store in the lookup + classLoaderBundleLookup.put(bcl,bundle); + + Thread.currentThread().setContextClassLoader(bcl); + loadExtensions(bundle, definitionMap, classNameBundleLookup, requiresInstanceClassLoading); + + // Create a look-up from withCoordinates to bundle + bundleCoordinateBundleLookup.put(bundle.getBundleDetails().getCoordinates(), bundle); + } + + // restore the current context class loader if appropriate + if (currentContextClassLoader != null) { + Thread.currentThread().setContextClassLoader(currentContextClassLoader); + } + return new InitContext(definitionMap, classNameBundleLookup, bundleCoordinateBundleLookup, + classLoaderBundleLookup, requiresInstanceClassLoading, instanceClassloaderLookup); + } + + /** + * Returns a bundle representing the system class loader. + * + * @param bundleProperties a BundleProperties instance which will be used to obtain the default + * Bundle library path, which will become the working directory of the returned bundle + * @return a bundle for the system class loader + */ + public static Bundle createSystemBundle(final FileSystemManager fileSystemManager, + final BundleProperties bundleProperties) throws FileSystemException, URISyntaxException { + final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + + final String bundlesLibraryDirectory = bundleProperties + .getProperty(BundleProperties.BUNDLE_LIBRARY_DIRECTORY); + if (StringUtils.isBlank(bundlesLibraryDirectory)) { + throw new IllegalStateException( + "Unable to create system bundle because " + BundleProperties.BUNDLE_LIBRARY_DIRECTORY + + " was null or empty"); + } + final URI bundlesLibraryDirURI = bundleProperties.getBundleLibraryDirectory(); + FileObject bundleDir = fileSystemManager.resolveFile(bundlesLibraryDirURI); + + // Test if the source Bundles can be read + FileUtils.ensureDirectoryExistAndCanRead(bundleDir); + + // the system bundle file object is never accessed, we use a dummy to stand in + final BundleDetails systemBundleDetails = new BundleDetails.Builder() + .withBundleFile(new DummyFileObject()) + .withCoordinates(SYSTEM_BUNDLE_COORDINATE) + .build(); + + return new Bundle(systemBundleDetails, systemClassLoader); + } + + /** + * Loads extensions from the specified bundle. + * + * @param bundle from which to load extensions + */ + @SuppressWarnings("unchecked") + private static void loadExtensions(final Bundle bundle, + Map<Class, Set<Class>> definitionMap, + Map<String, List<Bundle>> classNameBundleLookup, + Set<String> requiresInstanceClassLoading) { + + for (final Map.Entry<Class, Set<Class>> entry : definitionMap.entrySet()) { + // this is another extention point + // what we care about here is getting the right classes from the classloader for the bundle + // this *could* be as a 'service' itself with different implementations + // The NAR system uses the ServiceLoader, but this chokes on abstract classes, because for some + // reason it feels compelled to instantiate the class, + // which there may be in the system. + // Changed to ClassIndex + Class clazz = entry.getKey(); + ClassLoader cl = bundle.getClassLoader(); + Iterable<Class<?>> it = ClassIndex.getSubclasses(clazz, cl); + for (Class<?> c : it) { + if (cl.equals(c.getClassLoader())) { + // check for abstract + if (!Modifier.isAbstract(c.getModifiers())) { + registerServiceClass(c, classNameBundleLookup, requiresInstanceClassLoading, bundle, + entry.getValue()); + } + } + } + it = ClassIndex.getAnnotated(clazz, cl); + for (Class<?> c : it) { + if (cl.equals(clazz.getClassLoader())) { + // check for abstract + if (!Modifier.isAbstract(c.getModifiers())) { + registerServiceClass(c, classNameBundleLookup, requiresInstanceClassLoading, bundle, + entry.getValue()); + } + } + } + + } + } + + + /** + * Registers extension for the specified type from the specified Bundle. + * + * @param type the extension type + * @param classNameBundleMap mapping of classname to Bundle + * @param bundle the Bundle being mapped to + * @param classes to map to this classloader but which come from its ancestors + */ + private static void registerServiceClass(final Class<?> type, + final Map<String, List<Bundle>> classNameBundleMap, + final Set<String> requiresInstanceClassLoading, + final Bundle bundle, + final Set<Class> classes) { + final String className = type.getName(); + + // get the bundles that have already been registered for the class name + List<Bundle> registeredBundles = classNameBundleMap + .computeIfAbsent(className, (x) -> new ArrayList<>()); + + boolean alreadyRegistered = false; + for (final Bundle registeredBundle : registeredBundles) { + final BundleCoordinates registeredCoordinate = registeredBundle.getBundleDetails() + .getCoordinates(); + + // if the incoming bundle has the same withCoordinates as one of the registered bundles + // then consider it already registered + if (registeredCoordinate.equals(bundle.getBundleDetails().getCoordinates())) { + alreadyRegistered = true; + break; + } + + // if the type wasn't loaded from an ancestor, and the type isn't a parsers, cs, or reporting task, then + // fail registration because we don't support multiple versions of any other types + if (!multipleVersionsAllowed(type)) { + throw new IllegalStateException("Attempt was made to load " + className + " from " + + bundle.getBundleDetails().getCoordinates().getCoordinates() + + " but that class name is already loaded/registered from " + registeredBundle + .getBundleDetails().getCoordinates() + + " and multiple versions are not supported for this type" + ); + } + } + + // if none of the above was true then register the new bundle + if (!alreadyRegistered) { + registeredBundles.add(bundle); + classes.add(type); + + if (type.isAnnotationPresent(RequiresInstanceClassLoading.class)) { + requiresInstanceClassLoading.add(className); + } + } + } + + /** + * @param type a Class that we found from a service loader + * @return true if the given class is a parsers, controller service, or reporting task + */ + private static boolean multipleVersionsAllowed(Class<?> type) { + // we don't really need to support multiple versions at this time + return false; + } + + /** + * Determines the effective ClassLoader for the instance of the given type. + * + * @param classType the type of class to lookup the ClassLoader for + * @param instanceIdentifier the identifier of the specific instance of the classType to look up + * the ClassLoader for + * @param bundle the bundle where the classType exists + * @return the ClassLoader for the given instance of the given type, or null if the type is not a + * detected extension type + */ + public ClassLoader createInstanceClassLoader(final String classType, + final String instanceIdentifier, final Bundle bundle) throws NotInitializedException{ + if (StringUtils.isEmpty(classType)) { + throw new IllegalArgumentException("Class-Type is required"); + } + + if (StringUtils.isEmpty(instanceIdentifier)) { + throw new IllegalArgumentException("Instance Identifier is required"); + } + + if (bundle == null) { + throw new IllegalArgumentException("Bundle is required"); + } + + checkInitialized(); + + final ClassLoader bundleClassLoader = bundle.getClassLoader(); + + // If the class is annotated with @RequiresInstanceClassLoading and the registered ClassLoader is a URLClassLoader + // then make a new InstanceClassLoader that is a full copy of the BUNDLE Class Loader, otherwise create an empty + // InstanceClassLoader that has the Bundle ClassLoader as a parent + ClassLoader instanceClassLoader; + if (initContext.requiresInstanceClassLoading.contains(classType) + && (bundleClassLoader instanceof URLClassLoader)) { + final URLClassLoader registeredUrlClassLoader = (URLClassLoader) bundleClassLoader; + instanceClassLoader = new InstanceClassLoader(instanceIdentifier, classType, + registeredUrlClassLoader.getURLs(), registeredUrlClassLoader.getParent()); + } else { + instanceClassLoader = new InstanceClassLoader(instanceIdentifier, classType, new URL[0], + bundleClassLoader); + } + + initContext.instanceClassloaderLookup.put(instanceIdentifier, instanceClassLoader); + return instanceClassLoader; + } + + /** + * Retrieves the InstanceClassLoader for the component with the given identifier. + * + * @param instanceIdentifier the identifier of a component + * @return the instance class loader for the component + */ + public ClassLoader getInstanceClassLoader(final String instanceIdentifier) + throws NotInitializedException { + checkInitialized(); + return initContext.instanceClassloaderLookup.get(instanceIdentifier); + } + + /** + * Retrieves the Set of Classes registered with the ExtensionManager + * @return Set of Class + * @throws NotInitializedException + */ + public Set<Class> getExtensionClasses() throws NotInitializedException { + checkInitialized(); + return ImmutableSet.copyOf(initContext.definitionMap.keySet()); + } + + /** + * Removes the ClassLoader for the given instance and closes it if necessary. + * + * @param instanceIdentifier the identifier of a component to remove the ClassLoader for + * @return the removed ClassLoader for the given instance, or null if not found + */ + public ClassLoader removeInstanceClassLoaderIfExists(final String instanceIdentifier) + throws NotInitializedException { + if (instanceIdentifier == null) { + return null; + } + checkInitialized(); + final ClassLoader classLoader = initContext.instanceClassloaderLookup.remove(instanceIdentifier); + if (classLoader != null && (classLoader instanceof URLClassLoader)) { + final URLClassLoader urlClassLoader = (URLClassLoader) classLoader; + try { + urlClassLoader.close(); + } catch (IOException e) { + logger.warn("Unable to class URLClassLoader for " + instanceIdentifier); + } + } + return classLoader; + } + + /** + * Checks if the given class type requires per-instance class loading (i.e. contains the + * @RequiresInstanceClassLoading annotation) + * + * @param classType the class to check + * @return true if the class is found in the set of classes requiring instance level class + * loading, false otherwise + */ + public boolean requiresInstanceClassLoading(final String classType) throws NotInitializedException { + if (classType == null) { + throw new IllegalArgumentException("Class type cannot be null"); + } + checkInitialized(); + return initContext.requiresInstanceClassLoading.contains(classType); + } + + /** + * Retrieves the bundles that have a class with the given name. + * + * @param classType the class name of an extension + * @return the list of bundles that contain an extension with the given class name + */ + public List<Bundle> getBundles(final String classType) throws NotInitializedException{ + if (classType == null) { + throw new IllegalArgumentException("Class type cannot be null"); + } + checkInitialized(); + final List<Bundle> bundles = initContext.classNameBundleLookup.get(classType); + return bundles == null ? Collections.emptyList() : new ArrayList<>(bundles); + } + + /** + * Retrieves the bundle with the given withCoordinates. + * + * @param bundleCoordinates a withCoordinates to look up + * @return the bundle with the given withCoordinates, or null if none exists + */ + public Bundle getBundle(final BundleCoordinates bundleCoordinates) throws NotInitializedException { + if (bundleCoordinates == null) { + throw new IllegalArgumentException("BundleCoordinates cannot be null"); + } + checkInitialized(); + return initContext.bundleCoordinateBundleLookup.get(bundleCoordinates); + } + + /** + * Retrieves the bundle for the given class loader. + * + * @param classLoader the class loader to look up the bundle for + * @return the bundle for the given class loader + */ + public Bundle getBundle(final ClassLoader classLoader) throws NotInitializedException { + if (classLoader == null) { + throw new IllegalArgumentException("ClassLoader cannot be null"); + } + checkInitialized(); + return initContext.classLoaderBundleLookup.get(classLoader); + } + + public Set<Class> getExtensions(final Class<?> definition) throws NotInitializedException { + if (definition == null) { + throw new IllegalArgumentException("Class cannot be null"); + } + checkInitialized(); + final Set<Class> extensions = initContext.definitionMap.get(definition); + return (extensions == null) ? Collections.<Class>emptySet() : extensions; + } + + public void logClassLoaderMapping() throws NotInitializedException { + checkInitialized(); + final StringBuilder builder = new StringBuilder(); + + builder.append("Extension Type Mapping to Bundle:"); + for (final Map.Entry<Class, Set<Class>> entry : initContext.definitionMap.entrySet()) { + builder.append("\n\t=== ").append(entry.getKey().getSimpleName()).append(" Type ==="); + + for (final Class type : entry.getValue()) { + final List<Bundle> bundles = initContext.classNameBundleLookup.containsKey(type.getName()) + ? initContext.classNameBundleLookup.get(type.getName()) : Collections.emptyList(); + + builder.append("\n\t").append(type.getName()); + + for (final Bundle bundle : bundles) { + final String coordinate = bundle.getBundleDetails().getCoordinates().getCoordinates(); + final String workingDir = bundle.getBundleDetails().getBundleFile().getName().toString(); + builder.append("\n\t\t").append(coordinate).append(" || ").append(workingDir); + } + } + + builder.append("\n\t=== End ").append(entry.getKey().getSimpleName()).append(" types ==="); + } + + logger.info(builder.toString()); + } + + public void checkInitialized() throws NotInitializedException { + InitContext ic = initContext; + if (ic == null) { + throw new NotInitializedException(); + } + } + +} http://git-wip-us.apache.org/repos/asf/metron/blob/5f7454e4/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionMapping.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionMapping.java b/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionMapping.java new file mode 100644 index 0000000..5737f6c --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionMapping.java @@ -0,0 +1,156 @@ +/* + * 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.metron.bundles; + +import org.apache.metron.bundles.bundle.Bundle; +import org.apache.metron.bundles.bundle.BundleCoordinates; + +import java.util.*; +import java.util.function.BiFunction; + +/** + * The ExtensionMapping represents a mapping of the extensions available to the system. + * It is the product of the BundleMapper. + * + * It is NOT used at runtime for loading extensions, rather it may be used by a system to + * have details about the Extensions that exist in a system + * + * Runtime extension loading happens in the {@link ExtensionManager#init} + * + */ +public class ExtensionMapping { + + /* + The extensionNameMap is a map of the following + Extension Type -> Map of Extension Class Types to a set of BundleCoordinates + + For example : + Parser -> MessageParser -> [ bundles with parsers] + + BundleProperties files define with Property names the type and class types, such as: + + bundle.extension.type.parser=org.apache.metron.parsers.interfaces.MessageParser + + This is done to give a namespace to extensions, while supporting future extension types + and classes. This is different from the inspirational Nar system, which defined an explicit set + of supported classes, and a separate map for each. + + */ + private final Map<String, Map<String, Set<BundleCoordinates>>> extensionNameMap = new HashMap<>(); + + private final BiFunction<Set<BundleCoordinates>, Set<BundleCoordinates>, Set<BundleCoordinates>> merger = (oldValue, newValue) -> { + final Set<BundleCoordinates> merged = new HashSet<>(); + merged.addAll(oldValue); + merged.addAll(newValue); + return merged; + }; + + void addExtension(final String extensionName, final BundleCoordinates coordinate, + final String type) { + if (!extensionNameMap.containsKey(extensionName)) { + Map<String, Set<BundleCoordinates>> bundles = new HashMap<>(); + bundles.put(type, new HashSet<>()); + extensionNameMap.put(extensionName, bundles); + } + extensionNameMap.get(extensionName).computeIfAbsent(type, name -> new HashSet<>()) + .add(coordinate); + } + + void addAllExtensions(final String extensionName, final BundleCoordinates coordinate, + final Collection<String> types) { + if (!extensionNameMap.containsKey(extensionName)) { + Map<String, Set<BundleCoordinates>> bundles = new HashMap<>(); + extensionNameMap.put(extensionName, bundles); + } + types.forEach(name -> { + addExtension(extensionName, coordinate, name); + }); + } + + /** + * Returns a Map of the extension class types to a Set of BundleCoordinates for a given + * extension type. + * @param extensionTypeName the extension type name, such as parser, stellar, indexing + * @return Map of extension class name to a Set of BundleCoordinates + */ + public Map<String, Set<BundleCoordinates>> getExtensionNames(String extensionTypeName) { + if (extensionNameMap.containsKey(extensionTypeName)) { + return Collections.unmodifiableMap(extensionNameMap.get(extensionTypeName)); + } else { + return new HashMap<>(); + } + } + + /** + * Returns all the extensions in the system, mapped by extention type + * @return Map of extension types to a map of extension class to BundleCoordinates + */ + public Map<String, Map<String, Set<BundleCoordinates>>> getAllExtensions() { + return Collections.unmodifiableMap(extensionNameMap); + } + + /** + * Returns a Map of extension class types to a Set of BundleCoordinates for the system. + * This merges all the extension types into one map + * @return Map of extension class name to a Set of BundleCoordinates + */ + public Map<String, Set<BundleCoordinates>> getAllExtensionNames() { + final Map<String, Set<BundleCoordinates>> extensionNames = new HashMap<>(); + for (final Map<String, Set<BundleCoordinates>> bundleSets : extensionNameMap.values()) { + extensionNames.putAll(bundleSets); + } + return extensionNames; + } + + void merge(final ExtensionMapping other) { + other.getAllExtensions().forEach((ex, set) -> { + set.forEach((name, otherCoordinates) -> { + if (!extensionNameMap.containsKey(ex)) { + extensionNameMap.put(ex, new HashMap<>()); + } + extensionNameMap.get(ex).merge(name, otherCoordinates, merger); + }); + }); + } + + + /** + * Returns the number of all the bundles mapped to types. + * Bundles that map multiple types will be counted multiple times, once + * for each extension class exposed. + * + * @return raw count of the number of bundles + */ + public int size() { + int size = 0; + + for (final Map<String, Set<BundleCoordinates>> bundleSets : extensionNameMap.values()) { + for (final Set<BundleCoordinates> coordinates : bundleSets.values()) { + size += coordinates.size(); + } + } + return size; + } + + + /** + * @return true if there are no extension types in the system + */ + public boolean isEmpty() { + return extensionNameMap.isEmpty(); + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/5f7454e4/bundles-lib/src/main/java/org/apache/metron/bundles/InstanceClassLoader.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/InstanceClassLoader.java b/bundles-lib/src/main/java/org/apache/metron/bundles/InstanceClassLoader.java new file mode 100644 index 0000000..cb65605 --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/InstanceClassLoader.java @@ -0,0 +1,161 @@ +/* + * 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.metron.bundles; + +import java.lang.invoke.MethodHandles; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * A ClassLoader created for an instance of a component which lets a client add resources to an intermediary ClassLoader + * that will be checked first when loading/finding classes. + * + * Typically an instance of this ClassLoader will be created by passing in the URLs and parent from a BundleClassLoader in + * order to create a copy of the BundleClassLoader without modifying it. + */ +public class InstanceClassLoader extends URLClassLoader { + + private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + private final String identifier; + private final String instanceType; + private ShimClassLoader shimClassLoader; + + /** + * @param identifier the id of the component this ClassLoader was created for + * @param urls the URLs for the ClassLoader + * @param parent the parent ClassLoader + */ + public InstanceClassLoader(final String identifier, final String type, final URL[] urls, final ClassLoader parent) { + super(urls, parent); + this.identifier = identifier; + this.instanceType = type; + } + + /** + * Initializes a new ShimClassLoader for the provided resources, closing the previous ShimClassLoader if one existed. + * + * @param urls the URLs for the ShimClassLoader + * @throws IOException if the previous ShimClassLoader existed and couldn't be closed + */ + public synchronized void setInstanceResources(final URL[] urls) { + if (shimClassLoader != null) { + try { + shimClassLoader.close(); + } catch (IOException e) { + logger.warn("Unable to close inner URLClassLoader for " + identifier); + } + } + + shimClassLoader = new ShimClassLoader(urls, getParent()); + } + + /** + * @return the URLs for the instance resources that have been set + */ + public synchronized URL[] getInstanceResources() { + if (shimClassLoader != null) { + return shimClassLoader.getURLs(); + } + return new URL[0]; + } + + @Override + public Class<?> loadClass(String name) throws ClassNotFoundException { + return this.loadClass(name, false); + } + + @Override + protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + Class<?> c = null; + // first try the shim + if (shimClassLoader != null) { + try { + c = shimClassLoader.loadClass(name, resolve); + } catch (ClassNotFoundException e) { + c = null; + } + } + // if it wasn't in the shim try our self + if (c == null) { + return super.loadClass(name, resolve); + } else { + return c; + } + } + + @Override + protected Class<?> findClass(String name) throws ClassNotFoundException { + Class<?> c = null; + // first try the shim + if (shimClassLoader != null) { + try { + c = shimClassLoader.findClass(name); + } catch (ClassNotFoundException cnf) { + c = null; + } + } + // if it wasn't in the shim try our self + if (c == null) { + return super.findClass(name); + } else { + return c; + } + } + + @Override + public void close() throws IOException { + if (shimClassLoader != null) { + try { + shimClassLoader.close(); + } catch (IOException e) { + logger.warn("Unable to close inner URLClassLoader for " + identifier); + } + } + super.close(); + } + + /** + * Extend URLClassLoader to increase visibility of protected methods so that InstanceClassLoader can delegate. + */ + private static class ShimClassLoader extends URLClassLoader { + + public ShimClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); + } + + public ShimClassLoader(URL[] urls) { + super(urls); + } + + @Override + public Class<?> findClass(String name) throws ClassNotFoundException { + return super.findClass(name); + } + + @Override + public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { + return super.loadClass(name, resolve); + } + + } + +} http://git-wip-us.apache.org/repos/asf/metron/blob/5f7454e4/bundles-lib/src/main/java/org/apache/metron/bundles/NotInitializedException.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/NotInitializedException.java b/bundles-lib/src/main/java/org/apache/metron/bundles/NotInitializedException.java new file mode 100644 index 0000000..9857a0e --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/NotInitializedException.java @@ -0,0 +1,38 @@ +/* + * 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.metron.bundles; + +public class NotInitializedException extends Exception { + public NotInitializedException() { + } + + public NotInitializedException(String message) { + super(message); + } + + public NotInitializedException(String message, Throwable cause) { + super(message, cause); + } + + public NotInitializedException(Throwable cause) { + super(cause); + } + + public NotInitializedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/5f7454e4/bundles-lib/src/main/java/org/apache/metron/bundles/VFSBundleClassLoader.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/VFSBundleClassLoader.java b/bundles-lib/src/main/java/org/apache/metron/bundles/VFSBundleClassLoader.java new file mode 100644 index 0000000..888e7b3 --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/VFSBundleClassLoader.java @@ -0,0 +1,520 @@ +/* + * 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.metron.bundles; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.net.URL; +import java.security.CodeSource; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.Permissions; +import java.security.SecureClassLoader; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSystemException; +import org.apache.commons.vfs2.FileSystemManager; +import org.apache.commons.vfs2.NameScope; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * <p> + * A <tt>ClassLoader</tt> for loading BUNDLES (plugin archives). BUNDLEs are designed to + * allow isolating bundles of code (comprising one-or-more + * plugin classes and their + * dependencies) from other such bundles; this allows for dependencies and + * processors that require conflicting, incompatible versions of the same + * dependency to run in a single instance of a given process.</p> + * + * <p> + * <tt>BundleClassLoader</tt> follows the delegation model described in + * {@link ClassLoader#findClass(java.lang.String) ClassLoader.findClass(...)}; + * classes are first loaded from the parent <tt>ClassLoader</tt>, and only if + * they cannot be found there does the <tt>BundleClassLoader</tt> provide a + * definition. Specifically, this means that resources are loaded from the application's + * <tt>conf</tt> + * and <tt>lib</tt> directories first, and if they cannot be found there, are + * loaded from the BUNDLE.</p> + * + * <p> + * The packaging of a BUNDLE is such that it is a ZIP file with the following + * directory structure: + * + * <pre> + * +META-INF/ + * +-- bundled-dependencies/ + * +-- <JAR files> + * +-- MANIFEST.MF + * </pre> + * </p> + * + * <p> + * The MANIFEST.MF file contains the same information as a typical JAR file but + * also includes two additional bundle properties: {@code Bundle-Id} and + * {@code Bundle-Dependency-Id}. + * </p> + * + * <p> + * The {@code Bundle-Id} provides a unique identifier for this BUNDLE. + * </p> + * + * <p> + * The {@code Bundle-Dependency-Id} is optional. If provided, it indicates that + * this BUNDLE should inherit all of the dependencies of the BUNDLE with the provided + * ID. Often times, the BUNDLE that is depended upon is referred to as the Parent. + * This is because its ClassLoader will be the parent ClassLoader of the + * dependent BUNDLE. + * </p> + * + * <p> + * If a BUNDLE is built using the Bundles Maven Plugin, the {@code Bundle-Id} property + * will be set to the artifactId of the BUNDLE. The {@code Bundle-Dependency-Id} will + * be set to the artifactId of the BUNDLE that is depended upon. For example, if + * BUNDLE A is defined as such: + * + * <pre> + * ... + * <artifactId>bundle-a</artifactId> + * <packaging>bundle</packaging> + * ... + * <dependencies> + * <dependency> + * <groupId>group</groupId> + * <artifactId>bundle-z</artifactId> + * <b><type>bundle</type></b> + * </dependency> + * </dependencies> + * </pre> + * </p> + * + * + * <p> + * Then the MANIFEST.MF file that is created for Bundle A will have the following + * properties set: + * <ul> + * <li>{@code {Foo}-Id: bundle-a}</li> + * <li>{@code {Foo}-Dependency-Id: bundle-z}</li> + * </ul> + * Where is configurable by BundleProperty META_ID_PREFIX [ default Bundle ] + * </p> + * + * <p> + * Note, above, that the {@code type} of the dependency is set to {@code foo}. + * </p> + * + * <p> + * If the Bundle has more than one dependency of {@code type} {@code Foo}, then the + * Maven Bundle plugin will fail to build the Bundle. + * </p> + * + * This classloader is Metron Bundle aware, in that it understands that the passed in root + * FileObjects may be Bundles, + * This class is adapted from Apache Commons VFS + * VFSClassLoader class v.2.1 + * And the Apache Nifi NarClassLoader v. 1.2 + * + * @see FileSystemManager#createFileSystem + */ +public class VFSBundleClassLoader extends SecureClassLoader { + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + public static class Builder { + + private FileSystemManager fileSystemManager; + private FileObject bundleFile; + private ClassLoader parentClassLoader; + + public VFSBundleClassLoader.Builder withFileSystemManager(FileSystemManager fileSystemManager) { + this.fileSystemManager = fileSystemManager; + return this; + } + + public VFSBundleClassLoader.Builder withBundleFile(FileObject bundleFile) { + this.bundleFile = bundleFile; + return this; + } + + public VFSBundleClassLoader.Builder withParentClassloader(ClassLoader parentClassloader) { + this.parentClassLoader = parentClassloader; + return this; + } + + public VFSBundleClassLoader build() throws FileSystemException { + return new VFSBundleClassLoader(bundleFile, fileSystemManager, parentClassLoader); + } + } + + private final ArrayList<FileObject> resources = new ArrayList<FileObject>(); + private FileObject nativeDir; + public static final String DEPENDENCY_PATH = "META-INF/bundled-dependencies"; + /** + * Constructs a new VFSClassLoader for the given Bundle file. + * + * @param file the file to load the classes and resources from. + * @param manager the FileManager to use when trying create a layered Jar file system. + * @throws FileSystemException if an error occurs. + */ + public VFSBundleClassLoader(final FileObject file, + final FileSystemManager manager) + throws FileSystemException { + this(new FileObject[]{file}, manager, null); + } + + /** + * Constructs a new VFSClassLoader for the given file. + * + * @param file the Bundle FileObject to load the classes and resources from. + * @param manager the FileManager to use when trying create a layered Jar file system. + * @param parent the parent class loader for delegation. + * @throws FileSystemException if an error occurs. + */ + public VFSBundleClassLoader(final FileObject file, + final FileSystemManager manager, + final ClassLoader parent) + throws FileSystemException { + this(new FileObject[]{file}, manager, parent); + } + + /** + * Constructs a new VFSClassLoader for the given files. The files will be searched in the order + * specified. + * + * @param files the Bundle FileObjects to load the classes and resources from. + * @param manager the FileManager to use when trying create a layered Jar file system. + * @throws FileSystemException if an error occurs. + */ + public VFSBundleClassLoader(final FileObject[] files, + final FileSystemManager manager) + throws FileSystemException { + this(files, manager, null); + } + + /** + * Constructs a new VFSClassLoader for the given FileObjects. The FileObjects will be searched + * in the order specified. + * + * @param files the Bundle FileObjects to load the classes and resources from. + * @param manager the FileManager to use when trying create a layered Jar file system. + * @param parent the parent class loader for delegation. + * @throws FileSystemException if an error occurs. + */ + public VFSBundleClassLoader(final FileObject[] files, + final FileSystemManager manager, + final ClassLoader parent) throws FileSystemException { + super(parent); + addFileObjects(manager, files); + } + + /** + * Provide access to the file objects this class loader represents. + * + * @return An array of FileObjects. + * @since 2.0 + */ + public FileObject[] getFileObjects() { + return resources.toArray(new FileObject[resources.size()]); + } + + /** + * Appends the specified FileObjects to the list of FileObjects to search for classes and + * resources. If the FileObjects represent Bundles, then the Bundle dependencies will also + * be added as resources to the classloader. + * + * This is the equivelent of unzipping the Bundle and adding each jar in the dependency directory + * by uri. The ability of VFS to create filesystems from jar files, allows the VFSBundleClassLoader + * to create a composite filesystem out of the Bundle zip. + * + * @param manager The FileSystemManager. + * @param files the FileObjects to append to the search path. + * @throws FileSystemException if an error occurs. + */ + private void addFileObjects(final FileSystemManager manager, + final FileObject[] files) throws FileSystemException { + for (FileObject file : files) { + if (!file.exists()) { + // Does not exist - skip + continue; + } + + if (manager.canCreateFileSystem(file)) { + // create a Jar filesystem from the bundle + FileObject bundleFile = manager.createFileSystem(file); + + // resolve the dependency directory within the bundle + FileObject deps = bundleFile.resolveFile(DEPENDENCY_PATH); + if(deps.exists() && deps.isFolder()) { + nativeDir = deps.resolveFile("native"); + FileObject[] depJars = deps.getChildren(); + for (FileObject jarFileObject : depJars) { + // create a filesystem from each jar and add it as + // a resource + jarFileObject = manager.createFileSystem(jarFileObject); + resources.add(jarFileObject); + } + } + } else { + continue; + } + resources.add(file); + } + } + + @Override + protected String findLibrary(final String libname) { + try { + final FileObject libsoFile = nativeDir.resolveFile( "lib" + libname + ".so"); + final FileObject dllFile = nativeDir.resolveFile(libname + ".dll"); + final FileObject soFile = nativeDir.resolveFile(libname + ".so"); + if (libsoFile.exists()) { + return libsoFile.getURL().toString(); + } else if (dllFile.exists()) { + return dllFile.getURL().toString(); + } else if (soFile.exists()) { + return soFile.getURL().toString(); + } + }catch(FileSystemException fse){ + LOGGER.error("Failed to get dependencies",fse); + } + // not found in the bundle. try system native dir + return null; + } + + + /** + * Finds and loads the class with the specified name from the search path. + * + * @throws ClassNotFoundException if the class is not found. + */ + @Override + protected Class<?> findClass(final String name) throws ClassNotFoundException { + try { + final String path = name.replace('.', '/').concat(".class"); + final VFSBundleClassLoaderResource res = loadResource(path); + if (res == null) { + throw new ClassNotFoundException(name); + } + return defineClass(name, res); + } catch (final IOException ioe) { + throw new ClassNotFoundException(name, ioe); + } + } + + /** + * Loads and verifies the class with name and located with res. + */ + private Class<?> defineClass(final String name, final VFSBundleClassLoaderResource res) + throws IOException { + final URL url = res.getCodeSourceURL(); + final String pkgName = res.getPackageName(); + if (pkgName != null) { + final Package pkg = getPackage(pkgName); + if (pkg != null) { + if (pkg.isSealed()) { + if (!pkg.isSealed(url)) { + throw new FileSystemException("vfs.impl/pkg-sealed-other-url", pkgName); + } + } else { + if (isSealed(res)) { + throw new FileSystemException("vfs.impl/pkg-sealing-unsealed", pkgName); + } + } + } else { + definePackage(pkgName, res); + } + } + + final byte[] bytes = res.getBytes(); + final Certificate[] certs = + res.getFileObject().getContent().getCertificates(); + final CodeSource cs = new CodeSource(url, certs); + return defineClass(name, bytes, 0, bytes.length, cs); + } + + /** + * Returns true if the we should seal the package where res resides. + */ + private boolean isSealed(final VFSBundleClassLoaderResource res) + throws FileSystemException { + final String sealed = res.getPackageAttribute(Attributes.Name.SEALED); + return "true".equalsIgnoreCase(sealed); + } + + /** + * Reads attributes for the package and defines it. + */ + private Package definePackage(final String name, + final VFSBundleClassLoaderResource res) + throws FileSystemException { + // TODO - check for MANIFEST_ATTRIBUTES capability first + final String specTitle = res.getPackageAttribute(Name.SPECIFICATION_TITLE); + final String specVendor = res.getPackageAttribute(Attributes.Name.SPECIFICATION_VENDOR); + final String specVersion = res.getPackageAttribute(Name.SPECIFICATION_VERSION); + final String implTitle = res.getPackageAttribute(Name.IMPLEMENTATION_TITLE); + final String implVendor = res.getPackageAttribute(Name.IMPLEMENTATION_VENDOR); + final String implVersion = res.getPackageAttribute(Name.IMPLEMENTATION_VERSION); + + final URL sealBase; + if (isSealed(res)) { + sealBase = res.getCodeSourceURL(); + } else { + sealBase = null; + } + + return definePackage(name, specTitle, specVersion, specVendor, + implTitle, implVersion, implVendor, sealBase); + } + + /** + * Calls super.getPermissions both for the code source and also adds the permissions granted to + * the parent layers. + * + * @param cs the CodeSource. + * @return The PermissionCollections. + */ + @Override + protected PermissionCollection getPermissions(final CodeSource cs) { + try { + final String url = cs.getLocation().toString(); + final FileObject file = lookupFileObject(url); + if (file == null) { + return super.getPermissions(cs); + } + + final FileObject parentLayer = file.getFileSystem().getParentLayer(); + if (parentLayer == null) { + return super.getPermissions(cs); + } + + final Permissions combi = new Permissions(); + PermissionCollection permCollect = super.getPermissions(cs); + copyPermissions(permCollect, combi); + + for (FileObject parent = parentLayer; + parent != null; + parent = parent.getFileSystem().getParentLayer()) { + final CodeSource parentcs = + new CodeSource(parent.getURL(), + parent.getContent().getCertificates()); + permCollect = super.getPermissions(parentcs); + copyPermissions(permCollect, combi); + } + + return combi; + } catch (final FileSystemException fse) { + throw new SecurityException(fse.getMessage()); + } + } + + /** + * Copies the permissions from src to dest. + * + * @param src The source PermissionCollection. + * @param dest The destination PermissionCollection. + */ + protected void copyPermissions(final PermissionCollection src, + final PermissionCollection dest) { + for (final Enumeration<Permission> elem = src.elements(); elem.hasMoreElements(); ) { + final Permission permission = elem.nextElement(); + dest.add(permission); + } + } + + /** + * Does a reverse lookup to find the FileObject when we only have the URL. + */ + private FileObject lookupFileObject(final String name) { + final Iterator<FileObject> it = resources.iterator(); + while (it.hasNext()) { + final FileObject object = it.next(); + if (name.equals(object.getName().getURI())) { + return object; + } + } + return null; + } + + /** + * Finds the resource with the specified name from the search path. This returns null if the + * resource is not found. + * + * @param name The resource name. + * @return The URL that matches the resource. + */ + @Override + protected URL findResource(final String name) { + try { + final VFSBundleClassLoaderResource res = loadResource(name); + if (res != null) { + return res.getURL(); + } + return null; + } catch (final Exception ignored) { + return null; // TODO: report? + } + } + + /** + * Returns an Enumeration of all the resources in the search path with the specified name. <p> + * Gets called from {@link ClassLoader#getResources(String)} after parent class loader was + * questioned. + * + * @param name The resources to find. + * @return An Enumeration of the resources associated with the name. + * @throws FileSystemException if an error occurs. + */ + @Override + protected Enumeration<URL> findResources(final String name) throws IOException { + final List<URL> result = new ArrayList<URL>(2); + + for (FileObject baseFile : resources) { + final FileObject file = baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF); + if (file.exists()) { + result.add(new VFSBundleClassLoaderResource(name, baseFile, file).getURL()); + } + } + + return Collections.enumeration(result); + } + + /** + * Searches through the search path of for the first class or resource with specified name. + * + * @param name The resource to load. + * @return The Resource. + * @throws FileSystemException if an error occurs. + */ + private VFSBundleClassLoaderResource loadResource(final String name) throws FileSystemException { + for (final FileObject baseFile : resources) { + final FileObject file = baseFile.resolveFile(name, NameScope.DESCENDENT_OR_SELF); + if (file.exists()) { + return new VFSBundleClassLoaderResource(name, baseFile, file); + } + } + return null; + } +} + http://git-wip-us.apache.org/repos/asf/metron/blob/5f7454e4/bundles-lib/src/main/java/org/apache/metron/bundles/VFSBundleClassLoaderResource.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/VFSBundleClassLoaderResource.java b/bundles-lib/src/main/java/org/apache/metron/bundles/VFSBundleClassLoaderResource.java new file mode 100644 index 0000000..934d1fc --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/VFSBundleClassLoaderResource.java @@ -0,0 +1,110 @@ +/* + * 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.metron.bundles; + +import java.io.IOException; +import java.net.URL; +import java.util.jar.Attributes; + +import org.apache.commons.vfs2.FileObject; +import org.apache.commons.vfs2.FileSystemException; +import org.apache.commons.vfs2.FileUtil; + +/** + * Helper class for VFSBundleClassLoader. This represents a resource loaded with the classloader. + * This class is adapted from Apache Commons VFS Resource class v.2.1 + * + * @see VFSBundleClassLoader + */ +class VFSBundleClassLoaderResource { + + private final FileObject root; + private final FileObject resource; + private final FileObject packageFolder; + private final String packageName; + + /** + * Creates a new instance. + * + * @param root The code source FileObject. + * @param resource The resource of the FileObject. + */ + public VFSBundleClassLoaderResource(final String name, + final FileObject root, + final FileObject resource) + throws FileSystemException { + this.root = root; + this.resource = resource; + packageFolder = resource.getParent(); + final int pos = name.lastIndexOf('/'); + if (pos == -1) { + packageName = null; + } else { + packageName = name.substring(0, pos).replace('/', '.'); + } + } + + /** + * Returns the URL of the resource. + */ + public URL getURL() throws FileSystemException { + return resource.getURL(); + } + + /** + * Returns the name of the package containing the resource. + */ + public String getPackageName() { + return packageName; + } + + /** + * Returns an attribute of the package containing the resource. + */ + public String getPackageAttribute(final Attributes.Name attrName) throws FileSystemException { + return (String) packageFolder.getContent().getAttribute(attrName.toString()); + } + + /** + * Returns the folder for the package containing the resource. + */ + public FileObject getPackageFolder() { + return packageFolder; + } + + /** + * Returns the FileObject of the resource. + */ + public FileObject getFileObject() { + return resource; + } + + /** + * Returns the code source as an URL. + */ + public URL getCodeSourceURL() throws FileSystemException { + return root.getURL(); + } + + /** + * Returns the data for this resource as a byte array. + */ + public byte[] getBytes() throws IOException { + return FileUtil.getContent(resource); + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/5f7454e4/bundles-lib/src/main/java/org/apache/metron/bundles/annotation/behavior/RequiresInstanceClassLoading.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/annotation/behavior/RequiresInstanceClassLoading.java b/bundles-lib/src/main/java/org/apache/metron/bundles/annotation/behavior/RequiresInstanceClassLoading.java new file mode 100644 index 0000000..a6f4b15 --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/annotation/behavior/RequiresInstanceClassLoading.java @@ -0,0 +1,37 @@ +/* + * 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.metron.bundles.annotation.behavior; + +import java.lang.annotation.*; + +/** + * Marker annotation a component can use to indicate that the framework should create a new ClassLoader + * for each instance of the component, copying all resources from the component's BundleClassLoader to a + * new ClassLoader which will only be used by a given instance of the component. + * + * This annotation is typically used when a component has one or more PropertyDescriptors which set + * dynamicallyModifiesClasspath(boolean) to true. + * + * When this annotation is used it is important to note that each added instance of the component will increase + * the overall memory footprint more than that of a component without this annotation. + */ +@Documented +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface RequiresInstanceClassLoading { +} http://git-wip-us.apache.org/repos/asf/metron/blob/5f7454e4/bundles-lib/src/main/java/org/apache/metron/bundles/bundle/Bundle.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/bundle/Bundle.java b/bundles-lib/src/main/java/org/apache/metron/bundles/bundle/Bundle.java new file mode 100644 index 0000000..e06fb0b --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/bundle/Bundle.java @@ -0,0 +1,48 @@ +/* + * 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.metron.bundles.bundle; + +/** + * Represents a bundle that contains one or more extensions. + */ +public class Bundle { + + private final BundleDetails bundleDetails; + + private final ClassLoader classLoader; + + public Bundle(final BundleDetails bundleDetails, final ClassLoader classLoader) { + this.bundleDetails = bundleDetails; + this.classLoader = classLoader; + + if (this.bundleDetails == null) { + throw new IllegalStateException("BundleDetails cannot be null"); + } + + if (this.classLoader == null) { + throw new IllegalStateException("ClassLoader cannot be null"); + } + } + + public BundleDetails getBundleDetails() { + return bundleDetails; + } + + public ClassLoader getClassLoader() { + return classLoader; + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/5f7454e4/bundles-lib/src/main/java/org/apache/metron/bundles/bundle/BundleCoordinates.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/bundle/BundleCoordinates.java b/bundles-lib/src/main/java/org/apache/metron/bundles/bundle/BundleCoordinates.java new file mode 100644 index 0000000..c031ea9 --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/bundle/BundleCoordinates.java @@ -0,0 +1,96 @@ +/* + * 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.metron.bundles.bundle; + +/** + * The coordinates of a bundle (group, artifact, version). + */ +public class BundleCoordinates { + + public static final String DEFAULT_GROUP = "default"; + public static final String DEFAULT_VERSION = "unversioned"; + + public static final BundleCoordinates UNKNOWN_COORDINATE = new BundleCoordinates(DEFAULT_GROUP, + "unknown", DEFAULT_VERSION); + + private final String group; + private final String id; + private final String version; + private final String coordinates; + + public BundleCoordinates(final String group, final String id, final String version) { + this.group = isBlank(group) ? DEFAULT_GROUP : group; + this.id = id; + this.version = isBlank(version) ? DEFAULT_VERSION : version; + + if (isBlank(id)) { + throw new IllegalStateException("Id is required for BundleCoordinates"); + } + + if (this.group.contains(":") || this.id.contains(":") || this.version.contains(":")) { + throw new IllegalStateException(String + .format("Invalid coordinates: cannot contain : character group[%s] id[%s] version[%s]", + this.group, this.id, this.version)); + } + this.coordinates = this.group + ":" + this.id + ":" + this.version; + } + + private boolean isBlank(String str) { + return str == null || str.trim().length() == 0; + } + + public String getGroup() { + return group; + } + + public String getId() { + return id; + } + + public String getVersion() { + return version; + } + + public final String getCoordinates() { + return coordinates; + } + + @Override + public String toString() { + return coordinates; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + + if (!(obj instanceof BundleCoordinates)) { + return false; + } + + final BundleCoordinates other = (BundleCoordinates) obj; + return getCoordinates().equals(other.getCoordinates()); + } + + @Override + public int hashCode() { + return 37 * this.coordinates.hashCode(); + } + +}