| /* |
| * 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.nifi.nar; |
| |
| import org.apache.nifi.bundle.Bundle; |
| import org.apache.nifi.bundle.BundleCoordinate; |
| import org.apache.nifi.bundle.BundleDetails; |
| import org.apache.nifi.util.FileUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| |
| /** |
| * A singleton class used to initialize the extension and framework classloaders. |
| */ |
| public final class NarClassLoaders { |
| |
| public static final String FRAMEWORK_NAR_ID = "minifi-framework-nar"; |
| |
| private static volatile NarClassLoaders ncl; |
| private volatile InitContext initContext; |
| private static final Logger logger = LoggerFactory.getLogger(NarClassLoaders.class); |
| |
| private final static class InitContext { |
| |
| private final File frameworkWorkingDir; |
| private final File extensionWorkingDir; |
| private final Bundle frameworkBundle; |
| private final Map<String, Bundle> bundles; |
| |
| private InitContext( |
| final File frameworkDir, |
| final File extensionDir, |
| final Bundle frameworkBundle, |
| final Map<String, Bundle> bundles) { |
| this.frameworkWorkingDir = frameworkDir; |
| this.extensionWorkingDir = extensionDir; |
| this.frameworkBundle = frameworkBundle; |
| this.bundles = bundles; |
| } |
| } |
| |
| private NarClassLoaders() { |
| } |
| |
| /** |
| * @return The singleton instance of the NarClassLoaders |
| */ |
| public static NarClassLoaders getInstance() { |
| NarClassLoaders result = ncl; |
| if (result == null) { |
| synchronized (NarClassLoaders.class) { |
| result = ncl; |
| if (result == null) { |
| ncl = result = new NarClassLoaders(); |
| } |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Initializes and loads the NarClassLoaders. This method must be called |
| * before the rest of the methods to access the classloaders are called and |
| * it can be safely called any number of times provided the same framework |
| * and extension working dirs are used. |
| * |
| * @param frameworkWorkingDir where to find framework artifacts |
| * @param extensionsWorkingDir where to find extension artifacts |
| * @throws java.io.IOException if any issue occurs while exploding nar working directories. |
| * @throws java.lang.ClassNotFoundException if unable to load class definition |
| * @throws IllegalStateException already initialized with a given pair of |
| * directories cannot reinitialize or use a different pair of directories. |
| */ |
| public void init(final File frameworkWorkingDir, final File extensionsWorkingDir) throws IOException, ClassNotFoundException { |
| if (frameworkWorkingDir == null || extensionsWorkingDir == null) { |
| throw new NullPointerException("cannot have empty arguments"); |
| } |
| InitContext ic = initContext; |
| if (ic == null) { |
| synchronized (this) { |
| ic = initContext; |
| if (ic == null) { |
| initContext = ic = load(frameworkWorkingDir, extensionsWorkingDir); |
| } |
| } |
| } |
| boolean matching = initContext.extensionWorkingDir.equals(extensionsWorkingDir) |
| && initContext.frameworkWorkingDir.equals(frameworkWorkingDir); |
| if (!matching) { |
| throw new IllegalStateException("Cannot reinitialize and extension/framework directories cannot change"); |
| } |
| } |
| |
| /** |
| * Should be called at most once. |
| */ |
| private InitContext load(final File frameworkWorkingDir, final File extensionsWorkingDir) throws IOException, ClassNotFoundException { |
| // get the system classloader |
| final ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); |
| |
| // get the current context class loader |
| ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader(); |
| |
| // find all nar files and create class loaders for them. |
| final Map<String, Bundle> narDirectoryBundleLookup = new LinkedHashMap<>(); |
| final Map<String, ClassLoader> narCoordinateClassLoaderLookup = new HashMap<>(); |
| final Map<String, Set<BundleCoordinate>> narIdBundleLookup = new HashMap<>(); |
| |
| // make sure the nar directory is there and accessible |
| FileUtils.ensureDirectoryExistAndCanReadAndWrite(frameworkWorkingDir); |
| FileUtils.ensureDirectoryExistAndCanReadAndWrite(extensionsWorkingDir); |
| |
| final List<File> narWorkingDirContents = new ArrayList<>(); |
| final File[] frameworkWorkingDirContents = frameworkWorkingDir.listFiles(); |
| if (frameworkWorkingDirContents != null) { |
| narWorkingDirContents.addAll(Arrays.asList(frameworkWorkingDirContents)); |
| } |
| final File[] extensionsWorkingDirContents = extensionsWorkingDir.listFiles(); |
| if (extensionsWorkingDirContents != null) { |
| narWorkingDirContents.addAll(Arrays.asList(extensionsWorkingDirContents)); |
| } |
| |
| if (!narWorkingDirContents.isEmpty()) { |
| final List<BundleDetails> narDetails = new ArrayList<>(); |
| final Map<String,String> narCoordinatesToWorkingDir = new HashMap<>(); |
| |
| // load the nar details which includes and nar dependencies |
| for (final File unpackedNar : narWorkingDirContents) { |
| BundleDetails narDetail = null; |
| try { |
| narDetail = getNarDetails(unpackedNar); |
| } catch (IllegalStateException e) { |
| logger.warn("Unable to load NAR {} due to {}, skipping...", |
| new Object[] {unpackedNar.getAbsolutePath(), e.getMessage()}); |
| } |
| |
| // prevent the application from starting when there are two NARs with same group, id, and version |
| final String narCoordinate = narDetail.getCoordinate().getCoordinate(); |
| if (narCoordinatesToWorkingDir.containsKey(narCoordinate)) { |
| final String existingNarWorkingDir = narCoordinatesToWorkingDir.get(narCoordinate); |
| throw new IllegalStateException("Unable to load NAR with coordinates " + narCoordinate |
| + " and working directory " + narDetail.getWorkingDirectory() |
| + " because another NAR with the same coordinates already exists at " + existingNarWorkingDir); |
| } |
| |
| narDetails.add(narDetail); |
| narCoordinatesToWorkingDir.put(narCoordinate, narDetail.getWorkingDirectory().getCanonicalPath()); |
| } |
| |
| int narCount; |
| do { |
| // record the number of nars to be loaded |
| narCount = narDetails.size(); |
| |
| // attempt to create each nar class loader |
| for (final Iterator<BundleDetails> narDetailsIter = narDetails.iterator(); narDetailsIter.hasNext();) { |
| final BundleDetails narDetail = narDetailsIter.next(); |
| final BundleCoordinate narDependencyCoordinate = narDetail.getDependencyCoordinate(); |
| |
| // see if this class loader is eligible for loading |
| ClassLoader narClassLoader = null; |
| if (narDependencyCoordinate == null) { |
| narClassLoader = createNarClassLoader(narDetail.getWorkingDirectory(), currentContextClassLoader); |
| } else { |
| final String dependencyCoordinateStr = narDependencyCoordinate.getCoordinate(); |
| |
| // if the declared dependency has already been loaded |
| if (narCoordinateClassLoaderLookup.containsKey(dependencyCoordinateStr)) { |
| final ClassLoader narDependencyClassLoader = narCoordinateClassLoaderLookup.get(dependencyCoordinateStr); |
| narClassLoader = createNarClassLoader(narDetail.getWorkingDirectory(), narDependencyClassLoader); |
| } else { |
| // get all bundles that match the declared dependency id |
| final Set<BundleCoordinate> coordinates = narIdBundleLookup.get(narDependencyCoordinate.getId()); |
| |
| // ensure there are known bundles that match the declared dependency id |
| if (coordinates != null && !coordinates.contains(narDependencyCoordinate)) { |
| // ensure the declared dependency only has one possible bundle |
| if (coordinates.size() == 1) { |
| // get the bundle with the matching id |
| final BundleCoordinate coordinate = coordinates.stream().findFirst().get(); |
| |
| // if that bundle is loaded, use it |
| if (narCoordinateClassLoaderLookup.containsKey(coordinate.getCoordinate())) { |
| logger.warn(String.format("While loading '%s' unable to locate exact NAR dependency '%s'. Only found one possible match '%s'. Continuing...", |
| narDetail.getCoordinate().getCoordinate(), dependencyCoordinateStr, coordinate.getCoordinate())); |
| |
| final ClassLoader narDependencyClassLoader = narCoordinateClassLoaderLookup.get(coordinate.getCoordinate()); |
| narClassLoader = createNarClassLoader(narDetail.getWorkingDirectory(), narDependencyClassLoader); |
| } |
| } |
| } |
| } |
| } |
| |
| // if we were able to create the nar class loader, store it and remove the details |
| final ClassLoader bundleClassLoader = narClassLoader; |
| if (bundleClassLoader != null) { |
| narDirectoryBundleLookup.put(narDetail.getWorkingDirectory().getCanonicalPath(), new Bundle(narDetail, bundleClassLoader)); |
| narCoordinateClassLoaderLookup.put(narDetail.getCoordinate().getCoordinate(), narClassLoader); |
| narDetailsIter.remove(); |
| } |
| } |
| |
| // attempt to load more if some were successfully loaded this iteration |
| } while (narCount != narDetails.size()); |
| |
| // see if any nars couldn't be loaded |
| for (final BundleDetails narDetail : narDetails) { |
| logger.warn(String.format("Unable to resolve required dependency '%s'. Skipping NAR '%s'", |
| narDetail.getDependencyCoordinate().getId(), narDetail.getWorkingDirectory().getAbsolutePath())); |
| } |
| } |
| |
| // find the framework bundle, NarUnpacker already checked that there was a framework NAR and that there was only one |
| final Bundle frameworkBundle = narDirectoryBundleLookup.values().stream() |
| .filter(b -> b.getBundleDetails().getCoordinate().getId().equals(FRAMEWORK_NAR_ID)) |
| .findFirst().orElse(null); |
| |
| return new InitContext(frameworkWorkingDir, extensionsWorkingDir, frameworkBundle, new LinkedHashMap<>(narDirectoryBundleLookup)); |
| } |
| |
| /** |
| * Creates a new NarClassLoader. The parentClassLoader may be null. |
| * |
| * @param narDirectory root directory of nar |
| * @param parentClassLoader parent classloader of nar |
| * @return the nar classloader |
| * @throws IOException ioe |
| * @throws ClassNotFoundException cfne |
| */ |
| private static ClassLoader createNarClassLoader(final File narDirectory, final ClassLoader parentClassLoader) throws IOException, ClassNotFoundException { |
| logger.debug("Loading NAR file: " + narDirectory.getAbsolutePath()); |
| final ClassLoader narClassLoader = new NarClassLoader(narDirectory, parentClassLoader); |
| logger.info("Loaded NAR file: " + narDirectory.getAbsolutePath() + " as class loader " + narClassLoader); |
| return narClassLoader; |
| } |
| |
| /** |
| * Loads the details for the specified NAR. The details will be extracted |
| * from the manifest file. |
| * |
| * @param narDirectory the nar directory |
| * @return details about the NAR |
| * @throws IOException ioe |
| */ |
| private static BundleDetails getNarDetails(final File narDirectory) throws IOException { |
| return NarBundleUtil.fromNarDirectory(narDirectory); |
| } |
| |
| /** |
| * @return the framework class Bundle |
| * |
| * @throws IllegalStateException if the frame Bundle has not been loaded |
| */ |
| public Bundle getFrameworkBundle() { |
| if (initContext == null) { |
| throw new IllegalStateException("Framework bundle has not been loaded."); |
| } |
| |
| return initContext.frameworkBundle; |
| } |
| |
| /** |
| * @param extensionWorkingDirectory the directory |
| * @return the bundle for the specified working directory. Returns |
| * null when no bundle exists for the specified working directory |
| * @throws IllegalStateException if the bundles have not been loaded |
| */ |
| public Bundle getBundle(final File extensionWorkingDirectory) { |
| if (initContext == null) { |
| throw new IllegalStateException("Extensions class loaders have not been loaded."); |
| } |
| |
| try { |
| return initContext.bundles.get(extensionWorkingDirectory.getCanonicalPath()); |
| } catch (final IOException ioe) { |
| if(logger.isDebugEnabled()){ |
| logger.debug("Unable to get extension classloader for working directory '{}'", extensionWorkingDirectory); |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * @return the extensions that have been loaded |
| * @throws IllegalStateException if the extensions have not been loaded |
| */ |
| public Set<Bundle> getBundles() { |
| if (initContext == null) { |
| throw new IllegalStateException("Bundles have not been loaded."); |
| } |
| |
| return new LinkedHashSet<>(initContext.bundles.values()); |
| } |
| |
| } |