/*
* 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.documentation;
import org.apache.nifi.bundle.Bundle;
import org.apache.nifi.bundle.BundleCoordinate;
import org.apache.nifi.components.ConfigurableComponent;
import org.apache.nifi.controller.ControllerService;
import org.apache.nifi.documentation.html.HtmlDocumentationWriter;
import org.apache.nifi.documentation.html.HtmlProcessorDocumentationWriter;
import org.apache.nifi.nar.ExtensionManager;
import org.apache.nifi.nar.ExtensionMapping;
import org.apache.nifi.processor.Processor;
import org.apache.nifi.reporting.InitializationException;
import org.apache.nifi.reporting.ReportingTask;
import org.apache.nifi.util.NiFiProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Set;
/**
* Uses the ExtensionManager to get a list of Processor, ControllerService, and
* Reporting Task classes that were loaded and generate documentation for them.
*
*
*/
public class DocGenerator {
private static final Logger logger = LoggerFactory.getLogger(DocGenerator.class);
/**
* Generates documentation into the work/docs dir specified by
* NiFiProperties.
*
* @param properties to lookup nifi properties
* @param extensionMapping extension mapping
*/
public static void generate(final NiFiProperties properties, final ExtensionMapping extensionMapping) {
final File explodedNiFiDocsDir = properties.getComponentDocumentationWorkingDirectory();
logger.debug("Generating documentation for: " + extensionMapping.size() + " components in: " + explodedNiFiDocsDir);
documentConfigurableComponent(ExtensionManager.getExtensions(Processor.class), explodedNiFiDocsDir);
documentConfigurableComponent(ExtensionManager.getExtensions(ControllerService.class), explodedNiFiDocsDir);
documentConfigurableComponent(ExtensionManager.getExtensions(ReportingTask.class), explodedNiFiDocsDir);
}
/**
* Documents a type of configurable component.
*
* @param extensionClasses types of a configurable component
* @param explodedNiFiDocsDir base directory of component documentation
*/
private static void documentConfigurableComponent(final Set<Class> extensionClasses, final File explodedNiFiDocsDir) {
for (final Class<?> extensionClass : extensionClasses) {
if (ConfigurableComponent.class.isAssignableFrom(extensionClass)) {
final String extensionClassName = extensionClass.getCanonicalName();
final Bundle bundle = ExtensionManager.getBundle(extensionClass.getClassLoader());
if (bundle == null) {
logger.warn("No coordinate found for {}, skipping...", new Object[] {extensionClassName});
continue;
}
final BundleCoordinate coordinate = bundle.getBundleDetails().getCoordinate();
final String path = coordinate.getGroup() + "/" + coordinate.getId() + "/" + coordinate.getVersion() + "/" + extensionClassName;
final File componentDirectory = new File(explodedNiFiDocsDir, path);
componentDirectory.mkdirs();
final Class<? extends ConfigurableComponent> componentClass = extensionClass.asSubclass(ConfigurableComponent.class);
try {
logger.debug("Documenting: " + componentClass);
document(componentDirectory, componentClass, coordinate);
} catch (Exception e) {
logger.warn("Unable to document: " + componentClass, e);
}
}
}
}
/**
* Generates the documentation for a particular configurable component. Will
* check to see if an "additionalDetails.html" file exists and will link
* that from the generated documentation.
*
* @param componentDocsDir the component documentation directory
* @param componentClass the class to document
* @throws InstantiationException ie
* @throws IllegalAccessException iae
* @throws IOException ioe
* @throws InitializationException ie
*/
private static void document(final File componentDocsDir, final Class<? extends ConfigurableComponent> componentClass, final BundleCoordinate bundleCoordinate)
throws InstantiationException, IllegalAccessException, IOException, InitializationException {
// use temp components from ExtensionManager which should always be populated before doc generation
final String classType = componentClass.getCanonicalName();
final ConfigurableComponent component = ExtensionManager.getTempComponent(classType, bundleCoordinate);
final DocumentationWriter writer = getDocumentWriter(componentClass);
final File baseDocumentationFile = new File(componentDocsDir, "index.html");
if (baseDocumentationFile.exists()) {
logger.warn(baseDocumentationFile + " already exists, overwriting!");
}
try (final OutputStream output = new BufferedOutputStream(new FileOutputStream(baseDocumentationFile))) {
writer.write(component, output, hasAdditionalInfo(componentDocsDir));
}
}
/**
* Returns the DocumentationWriter for the type of component. Currently
* Processor, ControllerService, and ReportingTask are supported.
*
* @param componentClass the class that requires a DocumentationWriter
* @return a DocumentationWriter capable of generating documentation for
* that specific type of class
*/
private static DocumentationWriter getDocumentWriter(final Class<? extends ConfigurableComponent> componentClass) {
if (Processor.class.isAssignableFrom(componentClass)) {
return new HtmlProcessorDocumentationWriter();
} else if (ControllerService.class.isAssignableFrom(componentClass)) {
return new HtmlDocumentationWriter();
} else if (ReportingTask.class.isAssignableFrom(componentClass)) {
return new HtmlDocumentationWriter();
}
return null;
}
/**
* Checks to see if a directory to write to has an additionalDetails.html in
* it already.
*
* @param directory to check
* @return true if additionalDetails.html exists, false otherwise.
*/
private static boolean hasAdditionalInfo(File directory) {
return directory.list(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
return name.equalsIgnoreCase(HtmlDocumentationWriter.ADDITIONAL_DETAILS_HTML);
}
}).length > 0;
}
}