blob: 13108c171eebf2423e23f53a848085b122fb61f3 [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 org.apache.nifi.util.FileUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
/**
*
*/
public final class NarUnpacker {
private static final Logger logger = LoggerFactory.getLogger(NarUnpacker.class);
private static String HASH_FILENAME = "nar-md5sum";
private static final FileFilter NAR_FILTER = new FileFilter() {
@Override
public boolean accept(File pathname) {
final String nameToTest = pathname.getName().toLowerCase();
return nameToTest.endsWith(".nar") && pathname.isFile();
}
};
public static ExtensionMapping unpackNars(final NiFiProperties props) {
final List<Path> narLibraryDirs = props.getNarLibraryDirectories();
final File frameworkWorkingDir = props.getFrameworkWorkingDirectory();
final File extensionsWorkingDir = props.getExtensionsWorkingDirectory();
final File docsWorkingDir = props.getComponentDocumentationWorkingDirectory();
try {
File unpackedFramework = null;
final Set<File> unpackedExtensions = new HashSet<>();
final List<File> narFiles = new ArrayList<>();
// make sure the nar directories are there and accessible
FileUtils.ensureDirectoryExistAndCanReadAndWrite(frameworkWorkingDir);
FileUtils.ensureDirectoryExistAndCanReadAndWrite(extensionsWorkingDir);
FileUtils.ensureDirectoryExistAndCanReadAndWrite(docsWorkingDir);
for (Path narLibraryDir : narLibraryDirs) {
File narDir = narLibraryDir.toFile();
// Test if the source NARs can be read
FileUtils.ensureDirectoryExistAndCanRead(narDir);
File[] dirFiles = narDir.listFiles(NAR_FILTER);
if (dirFiles != null) {
List<File> fileList = Arrays.asList(dirFiles);
narFiles.addAll(fileList);
}
}
if (!narFiles.isEmpty()) {
for (File narFile : narFiles) {
logger.debug("Expanding NAR file: " + narFile.getAbsolutePath());
// get the manifest for this nar
try (final JarFile nar = new JarFile(narFile)) {
final Manifest manifest = nar.getManifest();
// lookup the nar id
final Attributes attributes = manifest.getMainAttributes();
final String narId = attributes.getValue("Nar-Id");
// determine if this is the framework
if (NarClassLoaders.FRAMEWORK_NAR_ID.equals(narId)) {
if (unpackedFramework != null) {
throw new IllegalStateException(
"Multiple framework NARs discovered. Only one framework is permitted.");
}
unpackedFramework = unpackNar(narFile, frameworkWorkingDir);
} else {
unpackedExtensions.add(unpackNar(narFile, extensionsWorkingDir));
}
}
}
// ensure we've found the framework nar
if (unpackedFramework == null) {
throw new IllegalStateException("No framework NAR found.");
} else if (!unpackedFramework.canRead()) {
throw new IllegalStateException("Framework NAR cannot be read.");
}
// Determine if any nars no longer exist and delete their
// working directories. This happens
// if a new version of a nar is dropped into the lib dir.
// ensure no old framework are present
final File[] frameworkWorkingDirContents = frameworkWorkingDir.listFiles();
if (frameworkWorkingDirContents != null) {
for (final File unpackedNar : frameworkWorkingDirContents) {
if (!unpackedFramework.equals(unpackedNar)) {
FileUtils.deleteFile(unpackedNar, true);
}
}
}
// ensure no old extensions are present
final File[] extensionsWorkingDirContents = extensionsWorkingDir.listFiles();
if (extensionsWorkingDirContents != null) {
for (final File unpackedNar : extensionsWorkingDirContents) {
if (!unpackedExtensions.contains(unpackedNar)) {
FileUtils.deleteFile(unpackedNar, true);
}
}
}
}
// attempt to delete any docs files that exist so that any
// components that have been removed
// will no longer have entries in the docs folder
final File[] docsFiles = docsWorkingDir.listFiles();
if (docsFiles != null) {
for (final File file : docsFiles) {
FileUtils.deleteFile(file, true);
}
}
final ExtensionMapping extensionMapping = new ExtensionMapping();
mapExtensions(extensionsWorkingDir, docsWorkingDir, extensionMapping);
return extensionMapping;
} catch (IOException e) {
logger.warn("Unable to load NAR library bundles due to " + e
+ " Will proceed without loading any further Nar bundles");
if (logger.isDebugEnabled()) {
logger.warn("", e);
}
}
return null;
}
private static void mapExtensions(final File workingDirectory, final File docsDirectory,
final ExtensionMapping mapping) throws IOException {
final File[] directoryContents = workingDirectory.listFiles();
if (directoryContents != null) {
for (final File file : directoryContents) {
if (file.isDirectory()) {
mapExtensions(file, docsDirectory, mapping);
} else if (file.getName().toLowerCase().endsWith(".jar")) {
unpackDocumentation(file, docsDirectory, mapping);
}
}
}
}
/**
* Unpacks the specified nar into the specified base working directory.
*
* @param nar
* the nar to unpack
* @param baseWorkingDirectory
* the directory to unpack to
* @return the directory to the unpacked NAR
* @throws IOException
* if unable to explode nar
*/
private static File unpackNar(final File nar, final File baseWorkingDirectory)
throws IOException {
final File narWorkingDirectory = new File(baseWorkingDirectory, nar.getName() + "-unpacked");
// if the working directory doesn't exist, unpack the nar
if (!narWorkingDirectory.exists()) {
unpack(nar, narWorkingDirectory, calculateMd5sum(nar));
} else {
// the working directory does exist. Run MD5 sum against the nar
// file and check if the nar has changed since it was deployed.
final byte[] narMd5 = calculateMd5sum(nar);
final File workingHashFile = new File(narWorkingDirectory, HASH_FILENAME);
if (!workingHashFile.exists()) {
FileUtils.deleteFile(narWorkingDirectory, true);
unpack(nar, narWorkingDirectory, narMd5);
} else {
final byte[] hashFileContents = Files.readAllBytes(workingHashFile.toPath());
if (!Arrays.equals(hashFileContents, narMd5)) {
logger.info("Contents of nar {} have changed. Reloading.",
new Object[] { nar.getAbsolutePath() });
FileUtils.deleteFile(narWorkingDirectory, true);
unpack(nar, narWorkingDirectory, narMd5);
}
}
}
return narWorkingDirectory;
}
/**
* Unpacks the NAR to the specified directory. Creates a checksum file that
* used to determine if future expansion is necessary.
*
* @param workingDirectory
* the root directory to which the NAR should be unpacked.
* @throws IOException
* if the NAR could not be unpacked.
*/
private static void unpack(final File nar, final File workingDirectory, final byte[] hash)
throws IOException {
try (JarFile jarFile = new JarFile(nar)) {
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
String name = jarEntry.getName();
File f = new File(workingDirectory, name);
if (jarEntry.isDirectory()) {
FileUtils.ensureDirectoryExistAndCanAccess(f);
} else {
makeFile(jarFile.getInputStream(jarEntry), f);
}
}
}
final File hashFile = new File(workingDirectory, HASH_FILENAME);
try (final FileOutputStream fos = new FileOutputStream(hashFile)) {
fos.write(hash);
}
}
private static void unpackDocumentation(final File jar, final File docsDirectory,
final ExtensionMapping extensionMapping) throws IOException {
// determine the components that may have documentation
determineDocumentedNiFiComponents(jar, extensionMapping);
// look for all documentation related to each component
try (final JarFile jarFile = new JarFile(jar)) {
for (final String componentName : extensionMapping.getAllExtensionNames()) {
final String entryName = "docs/" + componentName;
// go through each entry in this jar
for (final Enumeration<JarEntry> jarEnumeration = jarFile.entries(); jarEnumeration
.hasMoreElements();) {
final JarEntry jarEntry = jarEnumeration.nextElement();
// if this entry is documentation for this component
if (jarEntry.getName().startsWith(entryName)) {
final String name = StringUtils.substringAfter(jarEntry.getName(), "docs/");
// if this is a directory create it
if (jarEntry.isDirectory()) {
final File componentDocsDirectory = new File(docsDirectory, name);
// ensure the documentation directory can be created
if (!componentDocsDirectory.exists()
&& !componentDocsDirectory.mkdirs()) {
logger.warn("Unable to create docs directory "
+ componentDocsDirectory.getAbsolutePath());
break;
}
} else {
// if this is a file, write to it
final File componentDoc = new File(docsDirectory, name);
makeFile(jarFile.getInputStream(jarEntry), componentDoc);
}
}
}
}
}
}
private static void determineDocumentedNiFiComponents(final File jar,
final ExtensionMapping extensionMapping) throws IOException {
try (final JarFile jarFile = new JarFile(jar)) {
final JarEntry processorEntry = jarFile
.getJarEntry("META-INF/services/org.apache.nifi.processor.Processor");
final JarEntry reportingTaskEntry = jarFile
.getJarEntry("META-INF/services/org.apache.nifi.reporting.ReportingTask");
final JarEntry controllerServiceEntry = jarFile
.getJarEntry("META-INF/services/org.apache.nifi.controller.ControllerService");
extensionMapping.addAllProcessors(determineDocumentedNiFiComponents(jarFile,
processorEntry));
extensionMapping.addAllReportingTasks(determineDocumentedNiFiComponents(jarFile,
reportingTaskEntry));
extensionMapping.addAllControllerServices(determineDocumentedNiFiComponents(jarFile,
controllerServiceEntry));
}
}
private static List<String> determineDocumentedNiFiComponents(final JarFile jarFile,
final JarEntry jarEntry) throws IOException {
final List<String> componentNames = new ArrayList<>();
if (jarEntry == null) {
return componentNames;
}
try (final InputStream entryInputStream = jarFile.getInputStream(jarEntry);
final BufferedReader reader = new BufferedReader(new InputStreamReader(
entryInputStream))) {
String line;
while ((line = reader.readLine()) != null) {
final String trimmedLine = line.trim();
if (!trimmedLine.isEmpty() && !trimmedLine.startsWith("#")) {
final int indexOfPound = trimmedLine.indexOf("#");
final String effectiveLine = (indexOfPound > 0) ? trimmedLine.substring(0,
indexOfPound) : trimmedLine;
componentNames.add(effectiveLine);
}
}
}
return componentNames;
}
/**
* Creates the specified file, whose contents will come from the
* <tt>InputStream</tt>.
*
* @param inputStream
* the contents of the file to create.
* @param file
* the file to create.
* @throws IOException
* if the file could not be created.
*/
private static void makeFile(final InputStream inputStream, final File file) throws IOException {
try (final InputStream in = inputStream;
final FileOutputStream fos = new FileOutputStream(file)) {
byte[] bytes = new byte[65536];
int numRead;
while ((numRead = in.read(bytes)) != -1) {
fos.write(bytes, 0, numRead);
}
}
}
/**
* Calculates an md5 sum of the specified file.
*
* @param file
* to calculate the md5sum of
* @return the md5sum bytes
* @throws IOException
* if cannot read file
*/
private static byte[] calculateMd5sum(final File file) throws IOException {
try (final FileInputStream inputStream = new FileInputStream(file)) {
final MessageDigest md5 = MessageDigest.getInstance("md5");
final byte[] buffer = new byte[1024];
int read = inputStream.read(buffer);
while (read > -1) {
md5.update(buffer, 0, read);
read = inputStream.read(buffer);
}
return md5.digest();
} catch (NoSuchAlgorithmException nsae) {
throw new IllegalArgumentException(nsae);
}
}
private NarUnpacker() {
}
}