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")
+
+      );
+    }
+  }
+}

Reply via email to