blob: 9219721078932ab8d2d50c7b21afa58545bfd2dc [file] [log] [blame]
/*
* 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 java.io.File;
import java.io.FileInputStream;
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;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import org.apache.nifi.util.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
*/
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 ClassLoader frameworkClassLoader;
private final Map<String, ClassLoader> extensionClassLoaders;
private InitContext(
final File frameworkDir,
final File extensionDir,
final ClassLoader frameworkClassloader,
final Map<String, ClassLoader> extensionClassLoaders) {
this.frameworkWorkingDir = frameworkDir;
this.extensionWorkingDir = extensionDir;
this.frameworkClassLoader = frameworkClassloader;
this.extensionClassLoaders = extensionClassLoaders;
}
}
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, ClassLoader> extensionDirectoryClassLoaderLookup = new LinkedHashMap<>();
final Map<String, ClassLoader> narIdClassLoaderLookup = new HashMap<>();
// make sure the nar directory is there and accessible
FileUtils.ensureDirectoryExistAndCanAccess(frameworkWorkingDir);
FileUtils.ensureDirectoryExistAndCanAccess(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<NarDetails> narDetails = new ArrayList<>();
// load the nar details which includes and nar dependencies
for (final File unpackedNar : narWorkingDirContents) {
final NarDetails narDetail = getNarDetails(unpackedNar);
// ensure the nar contained an identifier
if (narDetail.getNarId() == null) {
logger.warn("No NAR Id found. Skipping: " + unpackedNar.getAbsolutePath());
continue;
}
// store the nar details
narDetails.add(narDetail);
}
int narCount;
do {
// record the number of nars to be loaded
narCount = narDetails.size();
// attempt to create each nar class loader
for (final Iterator<NarDetails> narDetailsIter = narDetails.iterator(); narDetailsIter.hasNext();) {
final NarDetails narDetail = narDetailsIter.next();
final String narDependencies = narDetail.getNarDependencyId();
// see if this class loader is eligible for loading
ClassLoader narClassLoader = null;
if (narDependencies == null) {
narClassLoader = createNarClassLoader(narDetail.getNarWorkingDirectory(), currentContextClassLoader);
} else if (narIdClassLoaderLookup.containsKey(narDetail.getNarDependencyId())) {
narClassLoader = createNarClassLoader(narDetail.getNarWorkingDirectory(), narIdClassLoaderLookup.get(narDetail.getNarDependencyId()));
}
// if we were able to create the nar class loader, store it and remove the details
if (narClassLoader != null) {
extensionDirectoryClassLoaderLookup.put(narDetail.getNarWorkingDirectory().getCanonicalPath(), narClassLoader);
narIdClassLoaderLookup.put(narDetail.getNarId(), 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 NarDetails narDetail : narDetails) {
logger.warn(String.format("Unable to resolve required dependency '%s'. Skipping NAR %s", narDetail.getNarDependencyId(), narDetail.getNarWorkingDirectory().getAbsolutePath()));
}
}
return new InitContext(frameworkWorkingDir, extensionsWorkingDir, narIdClassLoaderLookup.get(FRAMEWORK_NAR_ID), new LinkedHashMap<>(extensionDirectoryClassLoaderLookup));
}
/**
* 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 NarDetails getNarDetails(final File narDirectory) throws IOException {
final NarDetails narDetails = new NarDetails();
narDetails.setNarWorkingDirectory(narDirectory);
final File manifestFile = new File(narDirectory, "META-INF/MANIFEST.MF");
try (final FileInputStream fis = new FileInputStream(manifestFile)) {
final Manifest manifest = new Manifest(fis);
final Attributes attributes = manifest.getMainAttributes();
// get the nar details
narDetails.setNarId(attributes.getValue("Nar-Id"));
narDetails.setNarDependencyId(attributes.getValue("Nar-Dependency-Id"));
}
return narDetails;
}
/**
* @return the framework class loader
*
* @throws IllegalStateException if the frame class loader has not been
* loaded
*/
public ClassLoader getFrameworkClassLoader() {
if (initContext == null) {
throw new IllegalStateException("Framework class loader has not been loaded.");
}
return initContext.frameworkClassLoader;
}
/**
* @param extensionWorkingDirectory the directory
* @return the class loader for the specified working directory. Returns
* null when no class loader exists for the specified working directory
* @throws IllegalStateException if the class loaders have not been loaded
*/
public ClassLoader getExtensionClassLoader(final File extensionWorkingDirectory) {
if (initContext == null) {
throw new IllegalStateException("Extensions class loaders have not been loaded.");
}
try {
return initContext.extensionClassLoaders.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 extension class loaders
* @throws IllegalStateException if the class loaders have not been loaded
*/
public Set<ClassLoader> getExtensionClassLoaders() {
if (initContext == null) {
throw new IllegalStateException("Extensions class loaders have not been loaded.");
}
return new LinkedHashSet<>(initContext.extensionClassLoaders.values());
}
private static class NarDetails {
private String narId;
private String narDependencyId;
private File narWorkingDirectory;
public String getNarDependencyId() {
return narDependencyId;
}
public void setNarDependencyId(String narDependencyId) {
this.narDependencyId = narDependencyId;
}
public String getNarId() {
return narId;
}
public void setNarId(String narId) {
this.narId = narId;
}
public File getNarWorkingDirectory() {
return narWorkingDirectory;
}
public void setNarWorkingDirectory(File narWorkingDirectory) {
this.narWorkingDirectory = narWorkingDirectory;
}
}
}