METRON-1165 Add ability for BundeSystem to add bundles after initialization ( bundle added to lib dir ) (ottobackwards) closes apache/metron#739
Project: http://git-wip-us.apache.org/repos/asf/metron/repo Commit: http://git-wip-us.apache.org/repos/asf/metron/commit/1c63c1eb Tree: http://git-wip-us.apache.org/repos/asf/metron/tree/1c63c1eb Diff: http://git-wip-us.apache.org/repos/asf/metron/diff/1c63c1eb Branch: refs/heads/feature/METRON-1136-extensions-parsers Commit: 1c63c1eb31ac5b2f6e3b6fcd1c8964ce78124697 Parents: 09a62c5 Author: ottobackwards <[email protected]> Authored: Fri Sep 8 22:23:00 2017 -0400 Committer: otto <[email protected]> Committed: Fri Sep 8 22:23:00 2017 -0400 ---------------------------------------------------------------------- .../metron/bundles/BundleClassLoaders.java | 260 ++---------- .../bundles/BundleClassLoadersContext.java | 421 +++++++++++++++++++ .../org/apache/metron/bundles/BundleSystem.java | 49 ++- .../apache/metron/bundles/ExtensionManager.java | 259 +++--------- .../metron/bundles/ExtensionManagerContext.java | 372 ++++++++++++++++ .../org/apache/metron/bundles/AbstractFoo2.java | 27 ++ .../bundles/BundleClassLoadersContextTest.java | 144 +++++++ .../apache/metron/bundles/BundleSystemTest.java | 88 +++- .../bundles/ExtensionManagerContextTest.java | 110 +++++ .../org/apache/metron/parsers/BasicParser.java | 21 + .../metron-parser-foo-bundle-0.4.1.bundle | Bin 0 -> 21983 bytes .../src/test/resources/bundle.properties | 2 +- 12 files changed, 1299 insertions(+), 454 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/metron/blob/1c63c1eb/bundles-lib/src/main/java/org/apache/metron/bundles/BundleClassLoaders.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/BundleClassLoaders.java b/bundles-lib/src/main/java/org/apache/metron/bundles/BundleClassLoaders.java index 946c71a..525caf6 100644 --- a/bundles-lib/src/main/java/org/apache/metron/bundles/BundleClassLoaders.java +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/BundleClassLoaders.java @@ -14,26 +14,19 @@ * 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 java.lang.invoke.MethodHandles; import java.net.URISyntaxException; import java.util.*; -import org.apache.commons.collections.CollectionUtils; 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.BundleSelector; -import org.apache.metron.bundles.util.FileUtils; -import org.apache.metron.bundles.util.BundleUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,33 +36,9 @@ import org.slf4j.LoggerFactory; public final class BundleClassLoaders { private static volatile BundleClassLoaders bundleClassLoaders; - private static volatile InitContext initContext; + private static volatile BundleClassLoadersContext initContext; private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - /** - * Holds the context from {@code BundleClassLoaders} initialization, - * being the coordinate to bundle mapping. - * - * After initialization these are not changed, and as such they - * are immutable. - * - */ - private final static class InitContext { - - private final List<FileObject> extensionDirs; - private final Map<String, Bundle> bundles; - private final BundleProperties properties; - - private InitContext( - final List<FileObject> extensionDirs, - final Map<String, Bundle> bundles, - final BundleProperties properties) { - this.extensionDirs = ImmutableList.copyOf(extensionDirs); - this.bundles = ImmutableMap.copyOf(bundles); - this.properties = properties; - } - } - private BundleClassLoaders() { } @@ -122,199 +91,18 @@ public final class BundleClassLoaders { throw new IllegalStateException("BundleClassloader already exists"); } BundleClassLoaders b = new BundleClassLoaders(); - InitContext ic = b.load(fileSystemManager, extensionsDirs, props); + BundleClassLoadersContext ic = b.load(fileSystemManager, extensionsDirs, props); initContext = ic; bundleClassLoaders = b; } } - private InitContext load(final FileSystemManager fileSystemManager, - final List<FileObject> extensionsDirs, BundleProperties props) + private BundleClassLoadersContext load(final FileSystemManager fileSystemManager, + final List<FileObject> extensionsDirs, BundleProperties properties) throws FileSystemException, ClassNotFoundException, URISyntaxException { - // get the system classloader - final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); - - // find all bundle files and create class loaders for them. - final Map<String, Bundle> directoryBundleLookup = new LinkedHashMap<>(); - final Map<String, ClassLoader> coordinateClassLoaderLookup = new HashMap<>(); - final Map<String, Set<BundleCoordinates>> idBundleLookup = new HashMap<>(); - - for (FileObject extensionsDir : extensionsDirs) { - // make sure the bundle directory is there and accessible - FileUtils.ensureDirectoryExistAndCanRead(extensionsDir); - - final List<FileObject> bundleDirContents = new ArrayList<>(); - FileObject[] dirFiles = extensionsDir.findFiles(new BundleSelector(props.getArchiveExtension())); - if (dirFiles != null) { - List<FileObject> fileList = Arrays.asList(dirFiles); - bundleDirContents.addAll(fileList); - } - - if (!bundleDirContents.isEmpty()) { - final List<BundleDetails> bundleDetails = new ArrayList<>(); - final Map<String, String> bundleCoordinatesToBundleFile = new HashMap<>(); - - // load the bundle details which includes bundle dependencies - for (final FileObject bundleFile : bundleDirContents) { - if(!bundleFile.exists() || !bundleFile.isFile()) { - continue; - } - BundleDetails bundleDetail = null; - try { - bundleDetail = getBundleDetails(bundleFile, props); - } catch (IllegalStateException e) { - logger.warn("Unable to load BUNDLE {} due to {}, skipping...", - new Object[]{bundleFile.getURL(), e.getMessage()}); - } - - // prevent the application from starting when there are two BUNDLEs with same group, id, and version - final String bundleCoordinate = bundleDetail.getCoordinates().getCoordinates(); - if (bundleCoordinatesToBundleFile.containsKey(bundleCoordinate)) { - final String existingBundleWorkingDir = bundleCoordinatesToBundleFile - .get(bundleCoordinate); - throw new IllegalStateException( - "Unable to load BUNDLE with coordinates " + bundleCoordinate - + " and bundle file " + bundleDetail.getBundleFile() - + " because another BUNDLE with the same coordinates already exists at " - + existingBundleWorkingDir); - } - - bundleDetails.add(bundleDetail); - bundleCoordinatesToBundleFile.put(bundleCoordinate, - bundleDetail.getBundleFile().getURL().toURI().toString()); - } - - for (final Iterator<BundleDetails> bundleDetailsIter = bundleDetails.iterator(); - bundleDetailsIter.hasNext(); ) { - final BundleDetails bundleDetail = bundleDetailsIter.next(); - // populate bundle lookup - idBundleLookup.computeIfAbsent(bundleDetail.getCoordinates().getId(), - id -> new HashSet<>()).add(bundleDetail.getCoordinates()); - } - - int bundleCount; - do { - // record the number of bundles to be loaded - bundleCount = bundleDetails.size(); - - // attempt to create each bundle class loader - for (final Iterator<BundleDetails> bundleDetailsIter = bundleDetails.iterator(); - bundleDetailsIter.hasNext(); ) { - final BundleDetails bundleDetail = bundleDetailsIter.next(); - final BundleCoordinates bundleDependencyCoordinate = bundleDetail - .getDependencyCoordinates(); - - // see if this class loader is eligible for loading - ClassLoader potentialBundleClassLoader = null; - if (bundleDependencyCoordinate == null) { - potentialBundleClassLoader = createBundleClassLoader(fileSystemManager, - bundleDetail.getBundleFile(), ClassLoader.getSystemClassLoader()); - } else { - final String dependencyCoordinateStr = bundleDependencyCoordinate - .getCoordinates(); - - // if the declared dependency has already been loaded - if (coordinateClassLoaderLookup.containsKey(dependencyCoordinateStr)) { - final ClassLoader bundleDependencyClassLoader = coordinateClassLoaderLookup - .get(dependencyCoordinateStr); - potentialBundleClassLoader = createBundleClassLoader( - fileSystemManager, bundleDetail.getBundleFile(), - bundleDependencyClassLoader); - } else { - // get all bundles that match the declared dependency id - final Set<BundleCoordinates> coordinates = idBundleLookup - .get(bundleDependencyCoordinate.getId()); - - // ensure there are known bundles that match the declared dependency id - if (coordinates != null && !coordinates - .contains(bundleDependencyCoordinate)) { - // ensure the declared dependency only has one possible bundle - if (coordinates.size() == 1) { - // get the bundle with the matching id - final BundleCoordinates coordinate = coordinates.stream() - .findFirst().get(); - - // if that bundle is loaded, use it - if (coordinateClassLoaderLookup - .containsKey(coordinate.getCoordinates())) { - logger.warn(String.format( - "While loading '%s' unable to locate exact BUNDLE dependency '%s'. Only found one possible match '%s'. Continuing...", - bundleDetail.getCoordinates().getCoordinates(), - dependencyCoordinateStr, - coordinate.getCoordinates())); - - final ClassLoader bundleDependencyClassLoader = coordinateClassLoaderLookup - .get(coordinate.getCoordinates()); - potentialBundleClassLoader = createBundleClassLoader( - fileSystemManager, bundleDetail.getBundleFile(), - bundleDependencyClassLoader); - } - } - } - } - } - - // if we were able to create the bundle class loader, store it and remove the details - final ClassLoader bundleClassLoader = potentialBundleClassLoader; - if (bundleClassLoader != null) { - directoryBundleLookup - .put(bundleDetail.getBundleFile().getURL().toURI().toString(), - new Bundle(bundleDetail, bundleClassLoader)); - coordinateClassLoaderLookup - .put(bundleDetail.getCoordinates().getCoordinates(), - bundleClassLoader); - bundleDetailsIter.remove(); - } - } - - // attempt to load more if some were successfully loaded this iteration - } while (bundleCount != bundleDetails.size()); - - // see if any bundle couldn't be loaded - for (final BundleDetails bundleDetail : bundleDetails) { - logger.warn(String - .format("Unable to resolve required dependency '%s'. Skipping BUNDLE '%s'", - bundleDetail.getDependencyCoordinates().getId(), - bundleDetail.getBundleFile().getURL().toURI().toString())); - } - } - } - return new InitContext(extensionsDirs, new LinkedHashMap<>(directoryBundleLookup), props); - } - - /** - * Creates a new BundleClassLoader. The parentClassLoader may be null. - * - * @param bundleFile the Bundle File - * @param parentClassLoader parent classloader of bundle - * @return the bundle classloader - * @throws FileSystemException ioe - * @throws ClassNotFoundException cfne - */ - private static ClassLoader createBundleClassLoader(final FileSystemManager fileSystemManager, - final FileObject bundleFile, final ClassLoader parentClassLoader) - throws FileSystemException, ClassNotFoundException { - logger.debug("Loading Bundle file: " + bundleFile.getURL()); - final ClassLoader bundleClassLoader = new VFSBundleClassLoader.Builder() - .withFileSystemManager(fileSystemManager) - .withBundleFile(bundleFile) - .withParentClassloader(parentClassLoader).build(); - logger.info( - "Loaded Bundle file: " + bundleFile.getURL() + " as class loader " + bundleClassLoader); - return bundleClassLoader; - } - - /** - * Loads the details for the specified BUNDLE. The details will be extracted from the manifest - * file. - * - * @param bundleFile the bundle file - * @return details about the Bundle - * @throws FileSystemException ioe - */ - private static BundleDetails getBundleDetails(final FileObject bundleFile, BundleProperties props) - throws FileSystemException { - return BundleUtil.fromBundleFile(bundleFile, props); + return new BundleClassLoadersContext.Builder().withFileSystemManager(fileSystemManager) + .withExtensionDirs(extensionsDirs) + .withBundleProperties(properties).build(); } /** @@ -328,7 +116,7 @@ public final class BundleClassLoaders { } try { - return initContext.bundles.get(extensionFile.getURL().toURI().toString()); + return initContext.getBundles().get(extensionFile.getURL().toURI().toString()); } catch (URISyntaxException | FileSystemException e) { if (logger.isDebugEnabled()) { logger.debug("Unable to get extension classloader for bundle '{}'", @@ -346,8 +134,34 @@ public final class BundleClassLoaders { if (initContext == null) { throw new IllegalStateException("Bundles have not been loaded."); } - - return new LinkedHashSet<>(initContext.bundles.values()); + return new LinkedHashSet<>(initContext.getBundles().values()); } + /** + * Add a bundle to the BundleClassLoaders. + * Post initialization with will load a bundle and merge + * it's information into the context. + * + * This method has limited access, only package classes that + * can ensure thread saftey and control should call. + * @param bundleName the file name of the bundle. This is the name not the path, and the file + * should exist in the configured library directories + * @return The {@link Bundle} that is created + * @throws FileSystemException + * @throws URISyntaxException + * @throws ClassNotFoundException + */ + protected Bundle addBundle(String bundleName) + throws FileSystemException, URISyntaxException, ClassNotFoundException { + if (initContext == null) { + throw new IllegalStateException("Bundles have not been loaded."); + } + BundleClassLoadersContext newContext = new BundleClassLoadersContext.Builder() + .withBundleProperties(initContext.getProperties()) + .withExtensionDirs(initContext.getExtensionDirs()) + .withFileSystemManager(initContext.getFileSystemManager()).build(bundleName); + + initContext.merge(newContext); + return initContext.getBundles().values().stream().findFirst().get(); + } } http://git-wip-us.apache.org/repos/asf/metron/blob/1c63c1eb/bundles-lib/src/main/java/org/apache/metron/bundles/BundleClassLoadersContext.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/BundleClassLoadersContext.java b/bundles-lib/src/main/java/org/apache/metron/bundles/BundleClassLoadersContext.java new file mode 100644 index 0000000..ea2c77e --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/BundleClassLoadersContext.java @@ -0,0 +1,421 @@ +/* + * 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.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import java.lang.invoke.MethodHandles; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang.StringUtils; +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.BundleSelector; +import org.apache.metron.bundles.util.BundleUtil; +import org.apache.metron.bundles.util.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Context object for the {@link BundleClassLoaders}. + */ +public class BundleClassLoadersContext { + + private static final Logger logger = LoggerFactory + .getLogger(MethodHandles.lookup().lookupClass()); + + /** + * Builder class for BundleClassLoadersContext + */ + public static class Builder { + + FileSystemManager fileSystemManager; + List<FileObject> extensionsDirs; + FileObject bundleFile; + BundleProperties properties; + + public Builder() { + } + + /** + * Provides a {@link FileSystemManager}. + * @param fileSystemManager + * @return + */ + public Builder withFileSystemManager(FileSystemManager fileSystemManager) { + this.fileSystemManager = fileSystemManager; + return this; + } + + /** + * Provides the extension library directories. + * @param extensionDirs + * @return + */ + public Builder withExtensionDirs(List<FileObject> extensionDirs) { + this.extensionsDirs = extensionDirs; + return this; + } + + /** + * Provides the BundleProperties. + * @param properties + * @return + */ + public Builder withBundleProperties(BundleProperties properties) { + this.properties = properties; + return this; + } + + /** + * Builds a BundleClassLoaderContext. + * When built the context will be loaded from the provided + * library directories, using the {@link FileSystemManager} and {@BundleProperties}. + * + * An IllegalArgumentException will be thrown if any of the FileSystemManager, + * BundleProperties, or Extension Directories are missing or invalid. + * + * @return A loaded BundleClassLoaderContext + * @throws FileSystemException if there is a problem reading the bundles + * @throws ClassNotFoundException if there is a problem creating the classloaders + * @throws URISyntaxException if there is an invalid configuration + */ + public BundleClassLoadersContext build() + throws FileSystemException, ClassNotFoundException, URISyntaxException { + return build(null); + } + + /** + * Builds a BundleClassLoaderContext. + * When built the context will be loaded from the provided + * explicitBundleToLoad, using the {@link FileSystemManager} and {@BundleProperties}. + * + * If the explicteBundleToLoad is null or empty, then the extensionDirs will be used. + * + * This method can be used as a means to build a context for a single bundle. + * + * An IllegalArgumentException will be thrown if any of the FileSystemManager, + * BundleProperties, or Extension Directories are missing or invalid. + * + * @return A loaded BundleClassLoaderContext + * @throws FileSystemException if there is a problem reading the bundles + * @throws ClassNotFoundException if there is a problem creating the classloaders + * @throws URISyntaxException if there is an invalid configuration + */ + public BundleClassLoadersContext build(String explicitBundleToLoad) + throws FileSystemException, ClassNotFoundException, URISyntaxException { + + if(extensionsDirs == null || extensionsDirs.size() == 0) { + throw new IllegalArgumentException("missing extensionDirs"); + } + + if(properties == null) { + throw new IllegalArgumentException("properties are required"); + } + + if (fileSystemManager == null) { + throw new IllegalArgumentException("fileSystemManager is required"); + } + + // get the system classloader + final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); + + // find all bundle files and create class loaders for them. + final Map<String, Bundle> directoryBundleLookup = new LinkedHashMap<>(); + final Map<String, ClassLoader> coordinateClassLoaderLookup = new HashMap<>(); + final Map<String, Set<BundleCoordinates>> idBundleLookup = new HashMap<>(); + boolean foundExplicitLoadBundle = false; + boolean explicitBundleIsNotFile = false; + for (FileObject extensionsDir : extensionsDirs) { + // make sure the bundle directory is there and accessible + FileUtils.ensureDirectoryExistAndCanRead(extensionsDir); + + final List<FileObject> bundleDirContents = new ArrayList<>(); + FileObject[] dirFiles = null; + + // are we loading all bundles into this context or one explicit bundle? + // if it is explicit, we need to flag finding it, since for explict loads + // a bundle that doesn't exist or is not a file is an error + if (explicitBundleToLoad == null) { + dirFiles = extensionsDir + .findFiles(new BundleSelector(properties.getArchiveExtension())); + } else { + FileObject explicitBundleFileObject = extensionsDir.resolveFile(explicitBundleToLoad); + if (explicitBundleFileObject.exists()) { + foundExplicitLoadBundle = true; + if (!explicitBundleFileObject.isFile()) { + explicitBundleIsNotFile = true; + } + dirFiles = new FileObject[]{explicitBundleFileObject}; + } + } + + if (dirFiles != null) { + List<FileObject> fileList = Arrays.asList(dirFiles); + bundleDirContents.addAll(fileList); + } + + if (!bundleDirContents.isEmpty()) { + final List<BundleDetails> bundleDetails = new ArrayList<>(); + final Map<String, String> bundleCoordinatesToBundleFile = new HashMap<>(); + + // load the bundle details which includes bundle dependencies + for (final FileObject bundleFile : bundleDirContents) { + if (!bundleFile.exists() || !bundleFile.isFile()) { + continue; + } + BundleDetails bundleDetail = null; + try { + bundleDetail = getBundleDetails(bundleFile, properties); + } catch (IllegalStateException e) { + logger.warn("Unable to load BUNDLE {} due to {}, skipping...", + new Object[]{bundleFile.getURL(), e.getMessage()}); + } + + // prevent the application from starting when there are two BUNDLEs with same group, id, and version + final String bundleCoordinate = bundleDetail.getCoordinates().getCoordinates(); + if (bundleCoordinatesToBundleFile.containsKey(bundleCoordinate)) { + final String existingBundleWorkingDir = bundleCoordinatesToBundleFile + .get(bundleCoordinate); + throw new IllegalStateException( + "Unable to load BUNDLE with coordinates " + bundleCoordinate + + " and bundle file " + bundleDetail.getBundleFile() + + " because another BUNDLE with the same coordinates already exists at " + + existingBundleWorkingDir); + } + + bundleDetails.add(bundleDetail); + bundleCoordinatesToBundleFile.put(bundleCoordinate, + bundleDetail.getBundleFile().getURL().toURI().toString()); + } + + for (final Iterator<BundleDetails> bundleDetailsIter = bundleDetails.iterator(); + bundleDetailsIter.hasNext(); ) { + final BundleDetails bundleDetail = bundleDetailsIter.next(); + // populate bundle lookup + idBundleLookup.computeIfAbsent(bundleDetail.getCoordinates().getId(), + id -> new HashSet<>()).add(bundleDetail.getCoordinates()); + } + + int bundleCount; + do { + // record the number of bundles to be loaded + bundleCount = bundleDetails.size(); + + // attempt to create each bundle class loader + for (final Iterator<BundleDetails> bundleDetailsIter = bundleDetails.iterator(); + bundleDetailsIter.hasNext(); ) { + final BundleDetails bundleDetail = bundleDetailsIter.next(); + final BundleCoordinates bundleDependencyCoordinate = bundleDetail + .getDependencyCoordinates(); + + // see if this class loader is eligible for loading + ClassLoader potentialBundleClassLoader = null; + if (bundleDependencyCoordinate == null) { + potentialBundleClassLoader = createBundleClassLoader(fileSystemManager, + bundleDetail.getBundleFile(), ClassLoader.getSystemClassLoader()); + } else { + final String dependencyCoordinateStr = bundleDependencyCoordinate + .getCoordinates(); + + // if the declared dependency has already been loaded + if (coordinateClassLoaderLookup.containsKey(dependencyCoordinateStr)) { + final ClassLoader bundleDependencyClassLoader = coordinateClassLoaderLookup + .get(dependencyCoordinateStr); + potentialBundleClassLoader = createBundleClassLoader( + fileSystemManager, bundleDetail.getBundleFile(), + bundleDependencyClassLoader); + } else { + // get all bundles that match the declared dependency id + final Set<BundleCoordinates> coordinates = idBundleLookup + .get(bundleDependencyCoordinate.getId()); + + // ensure there are known bundles that match the declared dependency id + if (coordinates != null && !coordinates + .contains(bundleDependencyCoordinate)) { + // ensure the declared dependency only has one possible bundle + if (coordinates.size() == 1) { + // get the bundle with the matching id + final BundleCoordinates coordinate = coordinates.stream() + .findFirst().get(); + + // if that bundle is loaded, use it + if (coordinateClassLoaderLookup + .containsKey(coordinate.getCoordinates())) { + logger.warn(String.format( + "While loading '%s' unable to locate exact BUNDLE dependency '%s'. Only found one possible match '%s'. Continuing...", + bundleDetail.getCoordinates().getCoordinates(), + dependencyCoordinateStr, + coordinate.getCoordinates())); + + final ClassLoader bundleDependencyClassLoader = coordinateClassLoaderLookup + .get(coordinate.getCoordinates()); + potentialBundleClassLoader = createBundleClassLoader( + fileSystemManager, bundleDetail.getBundleFile(), + bundleDependencyClassLoader); + } + } + } + } + } + + // if we were able to create the bundle class loader, store it and remove the details + final ClassLoader bundleClassLoader = potentialBundleClassLoader; + if (bundleClassLoader != null) { + directoryBundleLookup + .put(bundleDetail.getBundleFile().getURL().toURI().toString(), + new Bundle(bundleDetail, bundleClassLoader)); + coordinateClassLoaderLookup + .put(bundleDetail.getCoordinates().getCoordinates(), + bundleClassLoader); + bundleDetailsIter.remove(); + } + } + + // attempt to load more if some were successfully loaded this iteration + } while (bundleCount != bundleDetails.size()); + + // see if any bundle couldn't be loaded + for (final BundleDetails bundleDetail : bundleDetails) { + logger.warn(String + .format("Unable to resolve required dependency '%s'. Skipping BUNDLE '%s'", + bundleDetail.getDependencyCoordinates().getId(), + bundleDetail.getBundleFile().getURL().toURI().toString())); + } + } + } + // did we find it, and if we did was it a file? + if (StringUtils.isNotEmpty(explicitBundleToLoad)) { + if (!foundExplicitLoadBundle) { + StringBuilder builder = new StringBuilder(); + builder.append(String.format("Bundle File %s does not exist in ", explicitBundleToLoad)); + for (FileObject extDir : extensionsDirs) { + builder.append(extDir.getURL()).append(" "); + } + throw new IllegalArgumentException(builder.toString()); + } else if (explicitBundleIsNotFile) { + throw new IllegalArgumentException( + String.format("%s was found, but is not a file", explicitBundleToLoad)); + } + } + return new BundleClassLoadersContext(fileSystemManager, extensionsDirs, + new LinkedHashMap<>(directoryBundleLookup), properties); + } + + /** + * Loads the details for the specified BUNDLE. The details will be extracted from the manifest + * file. + * + * @param bundleFile the bundle file + * @return details about the Bundle + * @throws FileSystemException ioe + */ + private BundleDetails getBundleDetails(final FileObject bundleFile, BundleProperties props) + throws FileSystemException { + return BundleUtil.fromBundleFile(bundleFile, props); + } + + /** + * Creates a new BundleClassLoader. The parentClassLoader may be null. + * + * @param bundleFile the Bundle File + * @param parentClassLoader parent classloader of bundle + * @return the bundle classloader + * @throws FileSystemException ioe + * @throws ClassNotFoundException cfne + */ + private ClassLoader createBundleClassLoader(final FileSystemManager fileSystemManager, + final FileObject bundleFile, final ClassLoader parentClassLoader) + throws FileSystemException, ClassNotFoundException { + logger.debug("Loading Bundle file: " + bundleFile.getURL()); + final ClassLoader bundleClassLoader = new VFSBundleClassLoader.Builder() + .withFileSystemManager(fileSystemManager) + .withBundleFile(bundleFile) + .withParentClassloader(parentClassLoader).build(); + logger.info( + "Loaded Bundle file: " + bundleFile.getURL() + " as class loader " + bundleClassLoader); + return bundleClassLoader; + } + } + + private List<FileObject> extensionDirs; + private Map<String, Bundle> bundles; + private final BundleProperties properties; + private final FileSystemManager fileSystemManager; + + private BundleClassLoadersContext( + final FileSystemManager fileSystemManager, + final List<FileObject> extensionDirs, + final Map<String, Bundle> bundles, + final BundleProperties properties) { + this.extensionDirs = ImmutableList.copyOf(extensionDirs); + this.bundles = ImmutableMap.copyOf(bundles); + this.properties = properties; + this.fileSystemManager = fileSystemManager; + } + + /** + * Merges another BundleClassLoadersContext into this one, + * creating a union of the two. + * Responsibility for synchronization of access to this context is up to + * the holder of it's reference + * @param other a BundleClassLoadersContext instance to merge into this one + */ + public void merge(BundleClassLoadersContext other) { + + extensionDirs = ImmutableList.copyOf(Stream.concat(extensionDirs.stream(), other.extensionDirs.stream().filter((x)-> !extensionDirs.contains(x))).collect( + Collectors.toList())); + bundles = ImmutableMap.copyOf(Stream.of(bundles,other.bundles).map(Map::entrySet).flatMap( + Collection::stream).collect( + Collectors.toMap(Entry::getKey, Entry::getValue, (s,a) -> s))); + } + + public List<FileObject> getExtensionDirs() { + return extensionDirs; + } + + public Map<String, Bundle> getBundles() { + return bundles; + } + + public BundleProperties getProperties() { + return properties; + } + + public FileSystemManager getFileSystemManager() { + return fileSystemManager; + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/1c63c1eb/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 index 7e93044..71cf42b 100644 --- a/bundles-lib/src/main/java/org/apache/metron/bundles/BundleSystem.java +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/BundleSystem.java @@ -20,12 +20,15 @@ package org.apache.metron.bundles; import com.google.common.annotations.VisibleForTesting; import java.lang.invoke.MethodHandles; import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; +import org.apache.commons.lang.StringUtils; 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.util.BundleProperties; @@ -145,26 +148,26 @@ public class BundleSystem { BundleClassLoaders.init(fileSystemManager, libFileObjects, properties); ExtensionManager .init(extensionClasses, systemBundle, BundleClassLoaders.getInstance().getBundles()); - return new BundleSystem(fileSystemManager, extensionClasses, systemBundle, properties); + return new BundleSystem(fileSystemManager, extensionClasses, libFileObjects, systemBundle, properties); } catch (Exception e) { throw new NotInitializedException(e); } } - - } private final BundleProperties properties; private final FileSystemManager fileSystemManager; private final List<Class> extensionClasses; + private final List<FileObject> extensionDirectories; private final Bundle systemBundle; - private BundleSystem(FileSystemManager fileSystemManager, List<Class> extensionClasses, - Bundle systemBundle, BundleProperties properties) { + private BundleSystem(FileSystemManager fileSystemManager, List<Class> extensionClasses,List<FileObject> + extensionDirectories, Bundle systemBundle, BundleProperties properties) { this.properties = properties; this.fileSystemManager = fileSystemManager; this.extensionClasses = extensionClasses; this.systemBundle = systemBundle; + this.extensionDirectories = extensionDirectories; } /** @@ -180,23 +183,47 @@ public class BundleSystem { public <T> T createInstance(final String specificClassName, final Class<T> clazz) throws ClassNotFoundException, InstantiationException, NotInitializedException, IllegalAccessException { - return BundleThreadContextClassLoader.createInstance(specificClassName, clazz, this.properties); + synchronized (BundleSystem.class) { + 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); - }); + synchronized (BundleSystem.class) { + ExtensionManager.getInstance().getExtensions(extensionType).forEach((x) -> { + set.add((Class<T>) x); + }); + } return set; } + /** + * Loads a Bundle into the system. + * + * @param bundleFileName the name of a Bundle file to load into the system. This file must exist + * in one of the library directories + */ + public void addBundle(String bundleFileName) + throws NotInitializedException, ClassNotFoundException, FileSystemException, URISyntaxException { + if (StringUtils.isEmpty(bundleFileName)) { + throw new IllegalArgumentException("bundleFileName cannot be null or empty"); + } + synchronized (BundleSystem.class) { + Bundle bundle = BundleClassLoaders.getInstance().addBundle(bundleFileName); + ExtensionManager.getInstance().addBundle(bundle); + } + } + @VisibleForTesting() public static void reset() { - BundleClassLoaders.reset(); - ExtensionManager.reset(); + synchronized (BundleSystem.class) { + BundleClassLoaders.reset(); + ExtensionManager.reset(); + } } } http://git-wip-us.apache.org/repos/asf/metron/blob/1c63c1eb/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 index 5eb82c6..c87ae2e 100644 --- a/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionManager.java +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionManager.java @@ -19,13 +19,15 @@ 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.io.IOException; import java.lang.invoke.MethodHandles; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -39,22 +41,10 @@ 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). @@ -66,40 +56,13 @@ import java.util.concurrent.ConcurrentHashMap; public class ExtensionManager { private static volatile ExtensionManager extensionManager; - private static volatile InitContext initContext; + private static volatile ExtensionManagerContext 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(){} /** @@ -143,57 +106,15 @@ public class ExtensionManager { throw new IllegalStateException("ExtensionManager already exists"); } ExtensionManager em = new ExtensionManager(); - InitContext ic = em.discoverExtensions(classes, systemBundle, bundles); + ExtensionManagerContext ic = new ExtensionManagerContext.Builder() + .withClasses(classes) + .withSystemBundle(systemBundle) + .withBundles(bundles).build(); 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. @@ -229,115 +150,6 @@ public class ExtensionManager { } /** - * 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 @@ -369,7 +181,7 @@ public class ExtensionManager { // 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) + if (initContext.getRequiresInstanceClassLoading().contains(classType) && (bundleClassLoader instanceof URLClassLoader)) { final URLClassLoader registeredUrlClassLoader = (URLClassLoader) bundleClassLoader; instanceClassLoader = new InstanceClassLoader(instanceIdentifier, classType, @@ -379,7 +191,7 @@ public class ExtensionManager { bundleClassLoader); } - initContext.instanceClassloaderLookup.put(instanceIdentifier, instanceClassLoader); + initContext.getInstanceClassloaderLookup().put(instanceIdentifier, instanceClassLoader); return instanceClassLoader; } @@ -392,7 +204,7 @@ public class ExtensionManager { public ClassLoader getInstanceClassLoader(final String instanceIdentifier) throws NotInitializedException { checkInitialized(); - return initContext.instanceClassloaderLookup.get(instanceIdentifier); + return initContext.getInstanceClassloaderLookup().get(instanceIdentifier); } /** @@ -402,7 +214,7 @@ public class ExtensionManager { */ public Set<Class> getExtensionClasses() throws NotInitializedException { checkInitialized(); - return ImmutableSet.copyOf(initContext.definitionMap.keySet()); + return ImmutableSet.copyOf(initContext.getDefinitionMap().keySet()); } /** @@ -417,7 +229,7 @@ public class ExtensionManager { return null; } checkInitialized(); - final ClassLoader classLoader = initContext.instanceClassloaderLookup.remove(instanceIdentifier); + final ClassLoader classLoader = initContext.getInstanceClassloaderLookup().remove(instanceIdentifier); if (classLoader != null && (classLoader instanceof URLClassLoader)) { final URLClassLoader urlClassLoader = (URLClassLoader) classLoader; try { @@ -442,7 +254,7 @@ public class ExtensionManager { throw new IllegalArgumentException("Class type cannot be null"); } checkInitialized(); - return initContext.requiresInstanceClassLoading.contains(classType); + return initContext.getRequiresInstanceClassLoading().contains(classType); } /** @@ -456,7 +268,7 @@ public class ExtensionManager { throw new IllegalArgumentException("Class type cannot be null"); } checkInitialized(); - final List<Bundle> bundles = initContext.classNameBundleLookup.get(classType); + final List<Bundle> bundles = initContext.getClassNameBundleLookup().get(classType); return bundles == null ? Collections.emptyList() : new ArrayList<>(bundles); } @@ -471,7 +283,7 @@ public class ExtensionManager { throw new IllegalArgumentException("BundleCoordinates cannot be null"); } checkInitialized(); - return initContext.bundleCoordinateBundleLookup.get(bundleCoordinates); + return initContext.getBundleCoordinateBundleLookup().get(bundleCoordinates); } /** @@ -485,7 +297,7 @@ public class ExtensionManager { throw new IllegalArgumentException("ClassLoader cannot be null"); } checkInitialized(); - return initContext.classLoaderBundleLookup.get(classLoader); + return initContext.getClassLoaderBundleLookup().get(classLoader); } public Set<Class> getExtensions(final Class<?> definition) throws NotInitializedException { @@ -493,7 +305,7 @@ public class ExtensionManager { throw new IllegalArgumentException("Class cannot be null"); } checkInitialized(); - final Set<Class> extensions = initContext.definitionMap.get(definition); + final Set<Class> extensions = initContext.getDefinitionMap().get(definition); return (extensions == null) ? Collections.<Class>emptySet() : extensions; } @@ -502,12 +314,12 @@ public class ExtensionManager { final StringBuilder builder = new StringBuilder(); builder.append("Extension Type Mapping to Bundle:"); - for (final Map.Entry<Class, Set<Class>> entry : initContext.definitionMap.entrySet()) { + for (final Map.Entry<Class, Set<Class>> entry : initContext.getDefinitionMap().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(); + final List<Bundle> bundles = initContext.getClassNameBundleLookup().containsKey(type.getName()) + ? initContext.getClassNameBundleLookup().get(type.getName()) : Collections.emptyList(); builder.append("\n\t").append(type.getName()); @@ -524,8 +336,31 @@ public class ExtensionManager { logger.info(builder.toString()); } + /** + * Add a new {@link Bundle} and it's extensions to the system + * This is an operation that would happen after initialization. + * This method has limited access, only package classes that + * can ensure thread saftey and control should call. + * + * + * @param bundle the {@link Bundle} to load + * @throws NotInitializedException If we are not initialized yet + */ + protected void addBundle(Bundle bundle) throws NotInitializedException { + checkInitialized(); + + Set<Bundle> bundles = new HashSet<>(); + bundles.add(bundle); + ExtensionManagerContext newContext = new ExtensionManagerContext.Builder().withBundles(bundles) + .withClasses(new ArrayList<Class>(initContext.getDefinitionMap().keySet())) + .withSystemBundle(initContext.getSystemBundle()) + .build(); + + initContext.merge(newContext); + } + public void checkInitialized() throws NotInitializedException { - InitContext ic = initContext; + ExtensionManagerContext ic = initContext; if (ic == null) { throw new NotInitializedException(); } http://git-wip-us.apache.org/repos/asf/metron/blob/1c63c1eb/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionManagerContext.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionManagerContext.java b/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionManagerContext.java new file mode 100644 index 0000000..853bdd7 --- /dev/null +++ b/bundles-lib/src/main/java/org/apache/metron/bundles/ExtensionManagerContext.java @@ -0,0 +1,372 @@ +/* + * 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.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.vfs2.FileSystemManager; +import org.apache.metron.bundles.annotation.behavior.RequiresInstanceClassLoading; +import org.apache.metron.bundles.bundle.Bundle; +import org.apache.metron.bundles.bundle.BundleCoordinates; +import org.apache.metron.bundles.util.ImmutableCollectionUtils; +import org.atteo.classindex.ClassIndex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Context object for the {@link ExtensionManager}. + */ +public class ExtensionManagerContext { + + private static final Logger logger = LoggerFactory + .getLogger(MethodHandles.lookup().lookupClass()); + /** + * Builder class for ExtensionManagerContext + */ + public static class Builder { + + List<Class> classes; + Bundle systemBundle; + Set<Bundle> bundles; + + /** + * Provides the {@link Class} definitions that will specify what extensions + * are to be loaded + * @param classes + * @return + */ + public Builder withClasses(List<Class> classes) { + this.classes = classes; + return this; + } + + /** + * Provides the SystemBundle. + * This bundle represents the system or main classloader + * @param systemBundle + * @return + */ + public Builder withSystemBundle(Bundle systemBundle) { + this.systemBundle = systemBundle; + return this; + } + + /** + * Provides the Bundles used to load the extensions + * @param bundles + * @return + */ + public Builder withBundles(Set<Bundle> bundles) { + this.bundles = bundles; + return this; + } + + public Builder() { + } + + /** + * * Builds a BundleClassLoaderContext. + * When built the context will be loaded from the provided + * explicitBundleToLoad, using the {@link FileSystemManager} and {@BundleProperties}. + * + * If the explicteBundleToLoad is null or empty, then the extensionDirs will be used. + * + * This method can be used as a means to build a context for a single bundle. + * + * An IllegalArgumentException will be thrown if any of the SystemBundle, + * Classes, or Bundles parameters are missing or invalid + * @return + */ + public ExtensionManagerContext build() { + if (systemBundle == null) { + throw new IllegalArgumentException("systemBundle must be defined"); + } + if (classes == null || classes.size() == 0) { + throw new IllegalArgumentException("classes must be defined"); + } + if (bundles == null) { + throw new IllegalArgumentException("bundles 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 ExtensionManagerContext(systemBundle, definitionMap, classNameBundleLookup, + bundleCoordinateBundleLookup, + classLoaderBundleLookup, requiresInstanceClassLoading, instanceClassloaderLookup); + } + + /** + * 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 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; + } + } + + // Maps a service definition (interface) to those classes that implement the interface + private Map<Class, Set<Class>> definitionMap; + private Map<String, List<Bundle>> classNameBundleLookup; + private Map<BundleCoordinates, Bundle> bundleCoordinateBundleLookup; + private Map<ClassLoader, Bundle> classLoaderBundleLookup; + private Set<String> requiresInstanceClassLoading; + private Map<String, ClassLoader> instanceClassloaderLookup; + private Bundle systemBundle; + + + private ExtensionManagerContext(Bundle systemBundle, 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.systemBundle = systemBundle; + 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); + } + + /** + * Merges another ExtensionManagerContext into this one, creating a union of the two. + * Responsibility for synchronization of access to this context is up to the holder of it's + * reference + * + * @param other a ExtensionManagerContext instance to merge into this one + */ + public void merge(ExtensionManagerContext other) { + + // merge everything together + // not on key matches, we merge the collection values + this.classNameBundleLookup = ImmutableCollectionUtils.immutableMapOfLists( + Stream.of(this.classNameBundleLookup, other.classNameBundleLookup) + .map(Map::entrySet) + .flatMap(Collection::stream) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (entry1, entry2) -> { + return Stream.concat(((List<Bundle>)entry1).stream(),((List<Bundle>)entry2).stream()).filter((x) ->!((List<Bundle>)entry1).contains(x)) + .collect(Collectors.toList()); + }))); + this.definitionMap = ImmutableCollectionUtils.immutableMapOfSets( + Stream.of(this.definitionMap, other.definitionMap) + .map(Map::entrySet) + .flatMap(Collection::stream) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (entry1, entry2) -> { + return Stream.concat(((Set<Class>)entry1).stream(),((Set<Class>)entry2).stream()).filter((x) -> !((Set<Class>)entry2).contains(x)) + .collect(Collectors.toSet()); + }))); + + this.bundleCoordinateBundleLookup = ImmutableMap.copyOf(Stream.of(bundleCoordinateBundleLookup, other.bundleCoordinateBundleLookup).map(Map::entrySet).flatMap( + Collection::stream).collect( + Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (s, a) -> s))); + + this.classLoaderBundleLookup = ImmutableMap.copyOf(Stream.of(classLoaderBundleLookup, other.classLoaderBundleLookup).map(Map::entrySet).flatMap( + Collection::stream).collect( + Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (s, a) -> s))); + + this.requiresInstanceClassLoading = ImmutableSet.copyOf( + Stream.concat(requiresInstanceClassLoading.stream(), + other.requiresInstanceClassLoading.stream().filter((x) -> !requiresInstanceClassLoading.contains(x))).collect( + Collectors.toSet())); + + this.instanceClassloaderLookup = new ConcurrentHashMap<>(Stream.of(instanceClassloaderLookup, other.instanceClassloaderLookup).map(Map::entrySet).flatMap( + Collection::stream).collect( + Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (s, a) -> s))); + } + + + public Map<Class, Set<Class>> getDefinitionMap() { + return definitionMap; + } + + public Map<String, List<Bundle>> getClassNameBundleLookup() { + return classNameBundleLookup; + } + + public Map<BundleCoordinates, Bundle> getBundleCoordinateBundleLookup() { + return bundleCoordinateBundleLookup; + } + + public Map<ClassLoader, Bundle> getClassLoaderBundleLookup() { + return classLoaderBundleLookup; + } + + public Set<String> getRequiresInstanceClassLoading() { + return requiresInstanceClassLoading; + } + + public Map<String, ClassLoader> getInstanceClassloaderLookup() { + return instanceClassloaderLookup; + } + + public Bundle getSystemBundle() { + return systemBundle; + } + +} http://git-wip-us.apache.org/repos/asf/metron/blob/1c63c1eb/bundles-lib/src/test/java/org/apache/metron/bundles/AbstractFoo2.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/test/java/org/apache/metron/bundles/AbstractFoo2.java b/bundles-lib/src/test/java/org/apache/metron/bundles/AbstractFoo2.java new file mode 100644 index 0000000..35dac42 --- /dev/null +++ b/bundles-lib/src/test/java/org/apache/metron/bundles/AbstractFoo2.java @@ -0,0 +1,27 @@ +/* + * 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.atteo.classindex.IndexSubclasses; + +@IndexSubclasses +public abstract class AbstractFoo2 { + + public void Do(){ + System.out.println("Foo"); + } +} http://git-wip-us.apache.org/repos/asf/metron/blob/1c63c1eb/bundles-lib/src/test/java/org/apache/metron/bundles/BundleClassLoadersContextTest.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/test/java/org/apache/metron/bundles/BundleClassLoadersContextTest.java b/bundles-lib/src/test/java/org/apache/metron/bundles/BundleClassLoadersContextTest.java new file mode 100644 index 0000000..d28e4de --- /dev/null +++ b/bundles-lib/src/test/java/org/apache/metron/bundles/BundleClassLoadersContextTest.java @@ -0,0 +1,144 @@ +/* + * 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 static org.apache.metron.bundles.util.TestUtil.loadSpecifiedProperties; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +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.apache.metron.bundles.util.ResourceCopier; +import org.apache.metron.bundles.util.TestUtil; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +public class BundleClassLoadersContextTest { + static final Map<String, String> EMPTY_MAP = new HashMap<String, String>(); + + @AfterClass + public static void after() { + BundleClassLoaders.reset(); + } + + @BeforeClass + public static void copyResources() throws IOException { + ResourceCopier.copyResources(Paths.get("./src/test/resources"), Paths.get("./target")); + } + + @Test + public void merge() throws Exception { + BundleProperties properties = loadSpecifiedProperties("/BundleMapper/conf/bundle.properties", + EMPTY_MAP); + + assertEquals("./target/BundleMapper/lib/", + properties.getProperty("bundle.library.directory")); + assertEquals("./target/BundleMapper/lib2/", + properties.getProperty("bundle.library.directory.alt")); + + String altLib = properties.getProperty("bundle.library.directory.alt"); + String lib = properties.getProperty("bundle.library.directory"); + properties.unSetProperty("bundle.library.directory.alt"); + + FileSystemManager fileSystemManager = FileSystemManagerFactory + .createFileSystemManager(new String[] {properties.getArchiveExtension()}); + + BundleClassLoadersContext firstContext = new BundleClassLoadersContext.Builder().withFileSystemManager(fileSystemManager) + .withExtensionDirs(TestUtil.getExtensionLibs(fileSystemManager,properties)).withBundleProperties(properties).build(); + + Assert.assertEquals(1, firstContext.getBundles().size()); + for (Bundle thisBundle : firstContext.getBundles().values()) { + Assert.assertEquals("org.apache.metron:metron-parser-bar-bundle:0.4.1", + thisBundle.getBundleDetails().getCoordinates().getCoordinates()); + } + + // set the lib again so the utils will pickup the other directory + properties.setProperty("bundle.library.directory", altLib); + + BundleClassLoadersContext secondContext = new BundleClassLoadersContext.Builder().withFileSystemManager(fileSystemManager) + .withExtensionDirs(TestUtil.getExtensionLibs(fileSystemManager,properties)).withBundleProperties(properties).build(); + + + Assert.assertEquals(1, secondContext.getBundles().size()); + for (Bundle thisBundle : secondContext.getBundles().values()) { + Assert.assertEquals("org.apache.metron:metron-parser-foo-bundle:0.4.1", + thisBundle.getBundleDetails().getCoordinates().getCoordinates()); + } + + // ok merge together + + firstContext.merge(secondContext); + Assert.assertEquals(2, firstContext.getBundles().size()); + for (Bundle thisBundle : firstContext.getBundles().values()) { + Assert.assertTrue( + thisBundle.getBundleDetails().getCoordinates().getCoordinates() + .equals("org.apache.metron:metron-parser-bar-bundle:0.4.1") + || + thisBundle.getBundleDetails().getCoordinates().getCoordinates() + .equals("org.apache.metron:metron-parser-foo-bundle:0.4.1") + + ); + } + + // merge a thirds, with duplicates + // set both dirs + properties.setProperty("bundle.library.directory.alt",lib); + + BundleClassLoadersContext thirdContext = new BundleClassLoadersContext.Builder().withFileSystemManager(fileSystemManager) + .withExtensionDirs(TestUtil.getExtensionLibs(fileSystemManager,properties)).withBundleProperties(properties).build(); + + Assert.assertEquals(2, thirdContext.getBundles().size()); + for (Bundle thisBundle : thirdContext.getBundles().values()) { + Assert.assertTrue( + thisBundle.getBundleDetails().getCoordinates().getCoordinates() + .equals("org.apache.metron:metron-parser-bar-bundle:0.4.1") + || + thisBundle.getBundleDetails().getCoordinates().getCoordinates() + .equals("org.apache.metron:metron-parser-foo-bundle:0.4.1") + + ); + } + + // merge them + firstContext.merge(thirdContext); + Assert.assertEquals(2, firstContext.getBundles().size()); + for (Bundle thisBundle : firstContext.getBundles().values()) { + Assert.assertTrue( + thisBundle.getBundleDetails().getCoordinates().getCoordinates() + .equals("org.apache.metron:metron-parser-bar-bundle:0.4.1") + || + thisBundle.getBundleDetails().getCoordinates().getCoordinates() + .equals("org.apache.metron:metron-parser-foo-bundle:0.4.1") + + ); + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/metron/blob/1c63c1eb/bundles-lib/src/test/java/org/apache/metron/bundles/BundleSystemTest.java ---------------------------------------------------------------------- diff --git a/bundles-lib/src/test/java/org/apache/metron/bundles/BundleSystemTest.java b/bundles-lib/src/test/java/org/apache/metron/bundles/BundleSystemTest.java index ee0fd40..e455c7f 100644 --- a/bundles-lib/src/test/java/org/apache/metron/bundles/BundleSystemTest.java +++ b/bundles-lib/src/test/java/org/apache/metron/bundles/BundleSystemTest.java @@ -19,11 +19,24 @@ package org.apache.metron.bundles; import static org.junit.Assert.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.apache.commons.io.FileUtils; +import org.apache.commons.vfs2.FileSystemManager; import org.apache.metron.bundles.BundleThreadContextClassLoaderTest.WithPropertiesConstructor; +import org.apache.metron.bundles.bundle.Bundle; import org.apache.metron.bundles.util.BundleProperties; +import org.apache.metron.bundles.util.FileSystemManagerFactory; +import org.apache.metron.bundles.util.ResourceCopier; +import org.apache.metron.parsers.interfaces.MessageParser; +import org.junit.After; import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; public class BundleSystemTest { @@ -31,17 +44,33 @@ public class BundleSystemTest { @AfterClass public static void after() { BundleClassLoaders.reset(); - ExtensionManager.reset();; + ExtensionManager.reset(); + File t = new File("target/BundleMapper/lib/metron-parser-foo-bundle-0.4.1.bundle"); + if (t.exists()) { + t.delete(); + } } - + + @After + public void afterTest() { + ExtensionManager.reset(); + BundleClassLoaders.reset(); + } + + @BeforeClass + public static void copyResources() throws IOException { + ResourceCopier.copyResources(Paths.get("./src/test/resources"), Paths.get("./target")); + } + @Test public void createInstance() throws Exception { BundleProperties properties = BundleProperties - .createBasicBundleProperties("src/test/resources/bundle.properties", null); + .createBasicBundleProperties("target/bundle.properties", null); - properties.setProperty(BundleProperties.BUNDLE_LIBRARY_DIRECTORY,"src/test/resources/BundleMapper/lib"); - BundleSystem bundleSystem = new BundleSystem.Builder().withBundleProperties(properties).withExtensionClasses( - Arrays.asList(AbstractFoo.class)).build(); + properties.setProperty(BundleProperties.BUNDLE_LIBRARY_DIRECTORY, "target/BundleMapper/lib"); + BundleSystem bundleSystem = new BundleSystem.Builder().withBundleProperties(properties) + .withExtensionClasses( + Arrays.asList(AbstractFoo.class)).build(); Assert.assertTrue( bundleSystem.createInstance(WithPropertiesConstructor.class.getName(), WithPropertiesConstructor.class) instanceof WithPropertiesConstructor); @@ -53,4 +82,49 @@ public class BundleSystemTest { BundleSystem bundleSystem = new BundleSystem.Builder().build(); } -} \ No newline at end of file + @Test + public void testAddBundle() throws Exception { + BundleProperties properties = BundleProperties + .createBasicBundleProperties("target/bundle.properties", null); + + properties.setProperty(BundleProperties.BUNDLE_LIBRARY_DIRECTORY, + "target/BundleMapper/lib"); + File f = new File("target/BundleMapper/metron-parser-foo-bundle-0.4.1.bundle"); + File t = new File( + "target/BundleMapper/lib/metron-parser-foo-bundle-0.4.1.bundle"); + if (t.exists()) { + t.delete(); + } + FileSystemManager fileSystemManager = FileSystemManagerFactory + .createFileSystemManager(new String[]{properties.getArchiveExtension()}); + BundleSystem bundleSystem = new BundleSystem.Builder() + .withFileSystemManager(fileSystemManager) + .withBundleProperties(properties).withExtensionClasses( + Arrays.asList(AbstractFoo.class, MessageParser.class)).build(); + Assert.assertTrue( + bundleSystem.createInstance(WithPropertiesConstructor.class.getName(), + WithPropertiesConstructor.class) instanceof WithPropertiesConstructor); + // copy the file into bundles library + FileUtils.copyFile(f, t); + Assert.assertEquals(1, BundleClassLoaders.getInstance().getBundles().size()); + for (Bundle thisBundle : BundleClassLoaders.getInstance().getBundles()) { + Assert.assertTrue( + thisBundle.getBundleDetails().getCoordinates().getCoordinates() + .equals("org.apache.metron:metron-parser-bar-bundle:0.4.1") + ); + } + bundleSystem.addBundle("metron-parser-foo-bundle-0.4.1.bundle"); + + Assert.assertEquals(2, BundleClassLoaders.getInstance().getBundles().size()); + for (Bundle thisBundle : BundleClassLoaders.getInstance().getBundles()) { + Assert.assertTrue( + thisBundle.getBundleDetails().getCoordinates().getCoordinates() + .equals("org.apache.metron:metron-parser-bar-bundle:0.4.1") + || + thisBundle.getBundleDetails().getCoordinates().getCoordinates() + .equals("org.apache.metron:metron-parser-foo-bundle:0.4.1") + + ); + } + } +}
