NIFI-12970 Generate documentation for Python Processors

This closes #8579

Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
Mark Bathori 2024-03-28 18:34:11 +01:00 committed by exceptionfactory
parent d3ff1f53c4
commit 26e5f5a565
No known key found for this signature in database
12 changed files with 1152 additions and 572 deletions

View File

@ -26,6 +26,11 @@
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-framework-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-python-framework-api</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-server-api</artifactId>

View File

@ -22,12 +22,14 @@ 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.documentation.html.HtmlPythonProcessorDocumentationWriter;
import org.apache.nifi.flowanalysis.FlowAnalysisRule;
import org.apache.nifi.nar.ExtensionDefinition;
import org.apache.nifi.nar.ExtensionManager;
import org.apache.nifi.nar.ExtensionMapping;
import org.apache.nifi.parameter.ParameterProvider;
import org.apache.nifi.processor.Processor;
import org.apache.nifi.python.PythonProcessorDetails;
import org.apache.nifi.reporting.ReportingTask;
import org.apache.nifi.util.NiFiProperties;
import org.slf4j.Logger;
@ -100,13 +102,26 @@ public class DocGenerator {
logger.debug("Documentation directory created [{}]", componentDirectory);
}
final Class<?> extensionClass = extensionManager.getClass(extensionDefinition);
final Class<? extends ConfigurableComponent> componentClass = extensionClass.asSubclass(ConfigurableComponent.class);
try {
logger.debug("Documentation generation started: Component Class [{}]", componentClass);
document(extensionManager, componentDirectory, componentClass, coordinate);
} catch (Exception e) {
logger.warn("Documentation generation failed: Component Class [{}]", componentClass, e);
switch (extensionDefinition.getRuntime()) {
case PYTHON -> {
final String componentClass = extensionDefinition.getImplementationClassName();
final PythonProcessorDetails processorDetails = extensionManager.getPythonProcessorDetails(componentClass, extensionDefinition.getVersion());
try {
documentPython(componentDirectory, processorDetails);
} catch (Exception e) {
logger.warn("Documentation generation failed: Component Class [{}]", componentClass, e);
}
}
case JAVA -> {
final Class<?> extensionClass = extensionManager.getClass(extensionDefinition);
final Class<? extends ConfigurableComponent> componentClass = extensionClass.asSubclass(ConfigurableComponent.class);
try {
logger.debug("Documentation generation started: Component Class [{}]", componentClass);
document(extensionManager, componentDirectory, componentClass, coordinate);
} catch (Exception e) {
logger.warn("Documentation generation failed: Component Class [{}]", componentClass, e);
}
}
}
}
}
@ -131,7 +146,7 @@ public class DocGenerator {
final String classType = componentClass.getCanonicalName();
final ConfigurableComponent component = extensionManager.getTempComponent(classType, bundleCoordinate);
final DocumentationWriter writer = getDocumentWriter(extensionManager, componentClass);
final DocumentationWriter<ConfigurableComponent> writer = getDocumentWriter(extensionManager, componentClass);
final File baseDocumentationFile = new File(componentDocsDir, "index.html");
if (baseDocumentationFile.exists()) {
@ -143,7 +158,29 @@ public class DocGenerator {
}
}
private static DocumentationWriter getDocumentWriter(
/**
* 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 processorDetails the python processor to document
* @throws IOException ioe
*/
private static void documentPython(final File componentDocsDir, final PythonProcessorDetails processorDetails) throws IOException {
final DocumentationWriter<PythonProcessorDetails> writer = new HtmlPythonProcessorDocumentationWriter();
final File baseDocumentationFile = new File(componentDocsDir, "index.html");
if (baseDocumentationFile.exists()) {
logger.warn("Overwriting Component Documentation [{}]", baseDocumentationFile);
}
try (final OutputStream output = new BufferedOutputStream(Files.newOutputStream(baseDocumentationFile.toPath()))) {
writer.write(processorDetails, output, hasAdditionalInfo(componentDocsDir));
}
}
private static DocumentationWriter<ConfigurableComponent> getDocumentWriter(
final ExtensionManager extensionManager,
final Class<? extends ConfigurableComponent> componentClass
) {

View File

@ -19,15 +19,12 @@ package org.apache.nifi.documentation;
import java.io.IOException;
import java.io.OutputStream;
import org.apache.nifi.components.ConfigurableComponent;
/**
* Generates documentation for an instance of a ConfigurableComponent
*
*
*/
public interface DocumentationWriter {
public interface DocumentationWriter<T> {
void write(ConfigurableComponent configurableComponent, OutputStream streamToWriteTo,
boolean includesAdditionalDocumentation) throws IOException;
void write(T component, OutputStream streamToWriteTo, boolean includesAdditionalDocumentation) throws IOException;
}

View File

@ -0,0 +1,355 @@
/*
* 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.html;
import org.apache.nifi.documentation.DocumentationWriter;
import org.apache.nifi.util.StringUtils;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
abstract class AbstractHtmlDocumentationWriter<T> implements DocumentationWriter<T> {
/**
* The filename where additional user specified information may be stored.
*/
public static final String ADDITIONAL_DETAILS_HTML = "additionalDetails.html";
static final String NO_DESCRIPTION = "No description provided.";
static final String NO_TAGS = "No tags provided.";
static final String NO_PROPERTIES = "This component has no required or optional properties.";
static final String H2 = "h2";
static final String H3 = "h3";
static final String H4 = "h4";
static final String P = "p";
static final String BR = "br";
static final String SPAN = "span";
static final String STRONG = "strong";
static final String TABLE = "table";
static final String TH = "th";
static final String TR = "tr";
static final String TD = "td";
static final String UL = "ul";
static final String LI = "li";
static final String ID = "id";
@Override
public void write(final T component, final OutputStream outputStream, final boolean includesAdditionalDocumentation) throws IOException {
try {
XMLStreamWriter xmlStreamWriter = XMLOutputFactory.newInstance().createXMLStreamWriter(outputStream, "UTF-8");
xmlStreamWriter.writeDTD("<!DOCTYPE html>");
xmlStreamWriter.writeStartElement("html");
xmlStreamWriter.writeAttribute("lang", "en");
writeHead(component, xmlStreamWriter);
writeBody(component, xmlStreamWriter, includesAdditionalDocumentation);
xmlStreamWriter.writeEndElement();
xmlStreamWriter.close();
} catch (XMLStreamException | FactoryConfigurationError e) {
throw new IOException("Unable to create XMLOutputStream", e);
}
}
/**
* Writes the head portion of the HTML documentation.
*
* @param component the component to describe
* @param xmlStreamWriter the stream to write to
* @throws XMLStreamException thrown if there was a problem writing to the stream
*/
protected void writeHead(final T component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException {
xmlStreamWriter.writeStartElement("head");
xmlStreamWriter.writeStartElement("meta");
xmlStreamWriter.writeAttribute("charset", "utf-8");
xmlStreamWriter.writeEndElement();
writeSimpleElement(xmlStreamWriter, "title", getTitle(component));
xmlStreamWriter.writeStartElement("link");
xmlStreamWriter.writeAttribute("rel", "stylesheet");
xmlStreamWriter.writeAttribute("href", "../../../../../css/component-usage.css");
xmlStreamWriter.writeAttribute("type", "text/css");
xmlStreamWriter.writeEndElement();
xmlStreamWriter.writeEndElement();
xmlStreamWriter.writeStartElement("script");
xmlStreamWriter.writeAttribute("type", "text/javascript");
xmlStreamWriter.writeCharacters("window.onload = function(){if(self==top) { " +
"document.getElementById('nameHeader').style.display = \"inherit\"; } }");
xmlStreamWriter.writeEndElement();
}
/**
* Writes the body section of the documentation, this consists of the component description, the tags, and the PropertyDescriptors.
*
* @param component the component to describe
* @param xmlStreamWriter the stream writer
* @param hasAdditionalDetails whether there are additional details present or not
* @throws XMLStreamException thrown if there was a problem writing to the XML stream
*/
void writeBody(final T component, final XMLStreamWriter xmlStreamWriter, final boolean hasAdditionalDetails) throws XMLStreamException {
xmlStreamWriter.writeStartElement("body");
writeHeader(component, xmlStreamWriter);
writeDeprecationWarning(component, xmlStreamWriter);
writeDescription(component, xmlStreamWriter, hasAdditionalDetails);
writeTags(component, xmlStreamWriter);
writeProperties(component, xmlStreamWriter);
writeDynamicProperties(component, xmlStreamWriter);
writeAdditionalBodyInfo(component, xmlStreamWriter);
writeStatefulInfo(component, xmlStreamWriter);
writeRestrictedInfo(component, xmlStreamWriter);
writeInputRequirementInfo(component, xmlStreamWriter);
writeUseCases(component, xmlStreamWriter);
writeMultiComponentUseCases(component, xmlStreamWriter);
writeSystemResourceConsiderationInfo(component, xmlStreamWriter);
writeSeeAlso(component, xmlStreamWriter);
xmlStreamWriter.writeEndElement();
}
/**
* Write the header to be displayed when loaded outside an iframe.
*
* @param component the component to describe
* @param xmlStreamWriter the stream writer to use
* @throws XMLStreamException thrown if there was a problem writing the XML
*/
private void writeHeader(final T component, XMLStreamWriter xmlStreamWriter) throws XMLStreamException {
xmlStreamWriter.writeStartElement("h1");
xmlStreamWriter.writeAttribute(ID, "nameHeader");
// Style will be overwritten on load if needed
xmlStreamWriter.writeAttribute("style", "display: none;");
xmlStreamWriter.writeCharacters(getTitle(component));
xmlStreamWriter.writeEndElement();
}
/**
* Writes a description of the component.
*
* @param component the component to describe
* @param xmlStreamWriter the stream writer
* @param hasAdditionalDetails whether there are additional details available as 'additionalDetails.html'
* @throws XMLStreamException thrown if there was a problem writing to the XML stream
*/
protected void writeDescription(final T component, final XMLStreamWriter xmlStreamWriter, final boolean hasAdditionalDetails) throws XMLStreamException {
writeSimpleElement(xmlStreamWriter, H2, "Description: ");
writeSimpleElement(xmlStreamWriter, P, getDescription(component));
if (hasAdditionalDetails) {
xmlStreamWriter.writeStartElement(P);
writeLink(xmlStreamWriter, "Additional Details...", ADDITIONAL_DETAILS_HTML);
xmlStreamWriter.writeEndElement();
}
}
/**
* This method may be overridden by subclasses to write additional information to the body of the documentation.
*
* @param component the component to describe
* @param xmlStreamWriter the stream writer
* @throws XMLStreamException thrown if there was a problem writing to the XML stream
*/
protected void writeAdditionalBodyInfo(final T component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException {
}
/**
* Writes a begin element, an id attribute(if specified), then text, then end element for element of the users choosing. Example: &lt;p
* id="p-id"&gt;text&lt;/p&gt;
*
* @param writer the stream writer to use
* @param elementName the name of the element
* @param characters the text of the element
* @param id the id of the element. specifying null will cause no element to be written.
* @throws XMLStreamException xse
*/
protected static void writeSimpleElement(final XMLStreamWriter writer, final String elementName, final String characters, String id) throws XMLStreamException {
writer.writeStartElement(elementName);
if (characters != null) {
if (id != null) {
writer.writeAttribute(ID, id);
}
writer.writeCharacters(characters);
}
writer.writeEndElement();
}
/**
* Writes a begin element, then text, then end element for the element of a users choosing. Example: &lt;p&gt;text&lt;/p&gt;
*
* @param writer the stream writer to use
* @param elementName the name of the element
* @param characters the characters to insert into the element
*/
protected static void writeSimpleElement(final XMLStreamWriter writer, final String elementName, final String characters) throws XMLStreamException {
writeSimpleElement(writer, elementName, characters, null);
}
/**
* A helper method to write a link
*
* @param xmlStreamWriter the stream to write to
* @param text the text of the link
* @param location the location of the link
* @throws XMLStreamException thrown if there was a problem writing to the
* stream
*/
protected void writeLink(final XMLStreamWriter xmlStreamWriter, final String text, final String location) throws XMLStreamException {
xmlStreamWriter.writeStartElement("a");
xmlStreamWriter.writeAttribute("href", location);
xmlStreamWriter.writeCharacters(text);
xmlStreamWriter.writeEndElement();
}
void writeUseCaseConfiguration(final String configuration, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException {
if (StringUtils.isEmpty(configuration)) {
return;
}
writeSimpleElement(xmlStreamWriter, H4, "Configuration:");
final String[] splits = configuration.split("\\n");
for (final String split : splits) {
xmlStreamWriter.writeStartElement(P);
final Matcher matcher = Pattern.compile("`(.*?)`").matcher(split);
int startIndex = 0;
while (matcher.find()) {
final int start = matcher.start();
if (start > 0) {
xmlStreamWriter.writeCharacters(split.substring(startIndex, start));
}
writeSimpleElement(xmlStreamWriter, "code", matcher.group(1));
startIndex = matcher.end();
}
if (split.length() > startIndex) {
if (startIndex == 0) {
xmlStreamWriter.writeCharacters(split);
} else {
xmlStreamWriter.writeCharacters(split.substring(startIndex));
}
}
xmlStreamWriter.writeEndElement();
}
}
/**
* Gets the class name of the component.
*
* @param component the component to describe
* @return the class name of the component
*/
abstract String getTitle(final T component);
/**
* Writes a warning about the deprecation of a component.
*
* @param component the component to describe
* @param xmlStreamWriter the stream writer
* @throws XMLStreamException thrown if there was a problem writing to the XML stream
*/
abstract void writeDeprecationWarning(final T component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
/**
* Gets a description of the component using the CapabilityDescription annotation.
*
* @param component the component to describe
* @return a description of the component
*/
abstract String getDescription(final T component);
/**
* Writes the tag list of the component.
*
* @param component the component to describe
* @param xmlStreamWriter the stream writer
* @throws XMLStreamException thrown if there was a problem writing to the XML stream
*/
abstract void writeTags(final T component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
/**
* Writes the PropertyDescriptors out as a table.
*
* @param component the component to describe
* @param xmlStreamWriter the stream writer
* @throws XMLStreamException thrown if there was a problem writing to the XML Stream
*/
abstract void writeProperties(final T component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
abstract void writeDynamicProperties(final T component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
/**
* Write the description of the Stateful annotation if provided in this component.
*
* @param component the component to describe
* @param xmlStreamWriter the stream writer to use
* @throws XMLStreamException thrown if there was a problem writing the XML
*/
abstract void writeStatefulInfo(final T component, XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
/**
* Write the description of the Restricted annotation if provided in this component.
*
* @param component the component to describe
* @param xmlStreamWriter the stream writer to use
* @throws XMLStreamException thrown if there was a problem writing the XML
*/
abstract void writeRestrictedInfo(final T component, XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
/**
* Add in the documentation information regarding the component whether it accepts an incoming relationship or not.
*
* @param component the component to describe
* @param xmlStreamWriter the stream writer to use
* @throws XMLStreamException thrown if there was a problem writing the XML
*/
abstract void writeInputRequirementInfo(final T component, XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
abstract void writeUseCases(final T component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
abstract void writeMultiComponentUseCases(final T component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
/**
* Writes the list of components that may be linked from this component.
*
* @param component the component to describe
* @param xmlStreamWriter the stream writer to use
* @throws XMLStreamException thrown if there was a problem writing the XML
*/
abstract void writeSeeAlso(final T component, XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
/**
* Writes all the system resource considerations for this component
*
* @param component the component to describe
* @param xmlStreamWriter the xml stream writer to use
* @throws XMLStreamException thrown if there was a problem writing the XML
*/
abstract void writeSystemResourceConsiderationInfo(final T component, XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
}

View File

@ -85,20 +85,20 @@ public class HtmlProcessorDocumentationWriter extends HtmlDocumentationWriter {
throws XMLStreamException {
List<ReadsAttribute> attributesRead = getReadsAttributes(processor);
writeSimpleElement(xmlStreamWriter, "h3", "Reads Attributes: ");
if (attributesRead.size() > 0) {
xmlStreamWriter.writeStartElement("table");
xmlStreamWriter.writeAttribute("id", "reads-attributes");
xmlStreamWriter.writeStartElement("tr");
writeSimpleElement(xmlStreamWriter, "th", "Name");
writeSimpleElement(xmlStreamWriter, "th", "Description");
writeSimpleElement(xmlStreamWriter, H3, "Reads Attributes: ");
if (!attributesRead.isEmpty()) {
xmlStreamWriter.writeStartElement(TABLE);
xmlStreamWriter.writeAttribute(ID, "reads-attributes");
xmlStreamWriter.writeStartElement(TR);
writeSimpleElement(xmlStreamWriter, TH, "Name");
writeSimpleElement(xmlStreamWriter, TH, "Description");
xmlStreamWriter.writeEndElement();
for (ReadsAttribute attribute : attributesRead) {
xmlStreamWriter.writeStartElement("tr");
writeSimpleElement(xmlStreamWriter, "td",
xmlStreamWriter.writeStartElement(TR);
writeSimpleElement(xmlStreamWriter, TD,
defaultIfBlank(attribute.attribute(), "Not Specified"));
// TODO allow for HTML characters here.
writeSimpleElement(xmlStreamWriter, "td",
writeSimpleElement(xmlStreamWriter, TD,
defaultIfBlank(attribute.description(), "Not Specified"));
xmlStreamWriter.writeEndElement();
@ -121,20 +121,20 @@ public class HtmlProcessorDocumentationWriter extends HtmlDocumentationWriter {
throws XMLStreamException {
List<WritesAttribute> attributesRead = getWritesAttributes(processor);
writeSimpleElement(xmlStreamWriter, "h3", "Writes Attributes: ");
if (attributesRead.size() > 0) {
xmlStreamWriter.writeStartElement("table");
xmlStreamWriter.writeAttribute("id", "writes-attributes");
xmlStreamWriter.writeStartElement("tr");
writeSimpleElement(xmlStreamWriter, "th", "Name");
writeSimpleElement(xmlStreamWriter, "th", "Description");
writeSimpleElement(xmlStreamWriter, H3, "Writes Attributes: ");
if (!attributesRead.isEmpty()) {
xmlStreamWriter.writeStartElement(TABLE);
xmlStreamWriter.writeAttribute(ID, "writes-attributes");
xmlStreamWriter.writeStartElement(TR);
writeSimpleElement(xmlStreamWriter, TH, "Name");
writeSimpleElement(xmlStreamWriter, TH, "Description");
xmlStreamWriter.writeEndElement();
for (WritesAttribute attribute : attributesRead) {
xmlStreamWriter.writeStartElement("tr");
writeSimpleElement(xmlStreamWriter, "td",
xmlStreamWriter.writeStartElement(TR);
writeSimpleElement(xmlStreamWriter, TD,
defaultIfBlank(attribute.attribute(), "Not Specified"));
// TODO allow for HTML characters here.
writeSimpleElement(xmlStreamWriter, "td",
writeSimpleElement(xmlStreamWriter, TD,
defaultIfBlank(attribute.description(), "Not Specified"));
xmlStreamWriter.writeEndElement();
}
@ -199,20 +199,20 @@ public class HtmlProcessorDocumentationWriter extends HtmlDocumentationWriter {
private void writeRelationships(final Processor processor, final XMLStreamWriter xmlStreamWriter)
throws XMLStreamException {
writeSimpleElement(xmlStreamWriter, "h3", "Relationships: ");
writeSimpleElement(xmlStreamWriter, H3, "Relationships: ");
if (processor.getRelationships().size() > 0) {
xmlStreamWriter.writeStartElement("table");
xmlStreamWriter.writeAttribute("id", "relationships");
xmlStreamWriter.writeStartElement("tr");
writeSimpleElement(xmlStreamWriter, "th", "Name");
writeSimpleElement(xmlStreamWriter, "th", "Description");
if (!processor.getRelationships().isEmpty()) {
xmlStreamWriter.writeStartElement(TABLE);
xmlStreamWriter.writeAttribute(ID, "relationships");
xmlStreamWriter.writeStartElement(TR);
writeSimpleElement(xmlStreamWriter, TH, "Name");
writeSimpleElement(xmlStreamWriter, TH, "Description");
xmlStreamWriter.writeEndElement();
for (Relationship relationship : processor.getRelationships()) {
xmlStreamWriter.writeStartElement("tr");
writeSimpleElement(xmlStreamWriter, "td", relationship.getName());
writeSimpleElement(xmlStreamWriter, "td", relationship.getDescription());
xmlStreamWriter.writeStartElement(TR);
writeSimpleElement(xmlStreamWriter, TD, relationship.getName());
writeSimpleElement(xmlStreamWriter, TD, relationship.getDescription());
xmlStreamWriter.writeEndElement();
}
xmlStreamWriter.writeEndElement();
@ -225,21 +225,21 @@ public class HtmlProcessorDocumentationWriter extends HtmlDocumentationWriter {
List<DynamicRelationship> dynamicRelationships = getDynamicRelationships(processor);
if (dynamicRelationships.size() > 0) {
writeSimpleElement(xmlStreamWriter, "h3", "Dynamic Relationships: ");
xmlStreamWriter.writeStartElement("p");
if (!dynamicRelationships.isEmpty()) {
writeSimpleElement(xmlStreamWriter, H3, "Dynamic Relationships: ");
xmlStreamWriter.writeStartElement(P);
xmlStreamWriter.writeCharacters("A Dynamic Relationship may be created based on how the user configures the Processor.");
xmlStreamWriter.writeStartElement("table");
xmlStreamWriter.writeAttribute("id", "dynamic-relationships");
xmlStreamWriter.writeStartElement("tr");
writeSimpleElement(xmlStreamWriter, "th", "Name");
writeSimpleElement(xmlStreamWriter, "th", "Description");
xmlStreamWriter.writeStartElement(TABLE);
xmlStreamWriter.writeAttribute(ID, "dynamic-relationships");
xmlStreamWriter.writeStartElement(TR);
writeSimpleElement(xmlStreamWriter, TH, "Name");
writeSimpleElement(xmlStreamWriter, TH, "Description");
xmlStreamWriter.writeEndElement();
for (DynamicRelationship dynamicRelationship : dynamicRelationships) {
xmlStreamWriter.writeStartElement("tr");
writeSimpleElement(xmlStreamWriter, "td", dynamicRelationship.name());
writeSimpleElement(xmlStreamWriter, "td", dynamicRelationship.description());
xmlStreamWriter.writeStartElement(TR);
writeSimpleElement(xmlStreamWriter, TD, dynamicRelationship.name());
writeSimpleElement(xmlStreamWriter, TD, dynamicRelationship.description());
xmlStreamWriter.writeEndElement();
}
xmlStreamWriter.writeEndElement();

View File

@ -0,0 +1,280 @@
/*
* 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.html;
import org.apache.nifi.expression.ExpressionLanguageScope;
import org.apache.nifi.python.PythonProcessorDetails;
import org.apache.nifi.python.processor.documentation.MultiProcessorUseCaseDetails;
import org.apache.nifi.python.processor.documentation.ProcessorConfigurationDetails;
import org.apache.nifi.python.processor.documentation.PropertyDescription;
import org.apache.nifi.python.processor.documentation.UseCaseDetails;
import org.apache.nifi.util.StringUtils;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.util.List;
import static org.apache.nifi.expression.ExpressionLanguageScope.NONE;
public class HtmlPythonProcessorDocumentationWriter extends AbstractHtmlDocumentationWriter<PythonProcessorDetails> {
@Override
String getTitle(final PythonProcessorDetails processorDetails) {
return processorDetails.getProcessorType();
}
@Override
void writeDeprecationWarning(final PythonProcessorDetails processorDetails, XMLStreamWriter xmlStreamWriter) {
// Not supported
}
@Override
String getDescription(final PythonProcessorDetails processorDetails) {
return processorDetails.getCapabilityDescription() != null ? processorDetails.getCapabilityDescription() : NO_DESCRIPTION;
}
@Override
void writeInputRequirementInfo(final PythonProcessorDetails processorDetails, XMLStreamWriter xmlStreamWriter) {
// Not supported
}
@Override
void writeStatefulInfo(final PythonProcessorDetails processorDetails, XMLStreamWriter xmlStreamWriter) {
// Not supported
}
@Override
void writeRestrictedInfo(final PythonProcessorDetails processorDetails, XMLStreamWriter xmlStreamWriter) {
// Not supported
}
@Override
void writeSeeAlso(final PythonProcessorDetails processorDetails, XMLStreamWriter xmlStreamWriter) {
// Not supported
}
@Override
void writeTags(final PythonProcessorDetails processorDetails, XMLStreamWriter xmlStreamWriter) throws XMLStreamException {
final List<String> tags = processorDetails.getTags();
xmlStreamWriter.writeStartElement(H3);
xmlStreamWriter.writeCharacters("Tags: ");
xmlStreamWriter.writeEndElement();
xmlStreamWriter.writeStartElement(P);
if (tags != null && !tags.isEmpty()) {
final String tagString = String.join(", ", tags);
xmlStreamWriter.writeCharacters(tagString);
} else {
xmlStreamWriter.writeCharacters(NO_TAGS);
}
xmlStreamWriter.writeEndElement();
}
@Override
void writeUseCases(final PythonProcessorDetails processorDetails, XMLStreamWriter xmlStreamWriter) throws XMLStreamException {
final List<UseCaseDetails> useCaseDetailsList = processorDetails.getUseCases();
if (useCaseDetailsList.isEmpty()) {
return;
}
writeSimpleElement(xmlStreamWriter, H2, "Example Use Cases:");
for (final UseCaseDetails useCaseDetails : useCaseDetailsList) {
writeSimpleElement(xmlStreamWriter, H3, "Use Case:");
writeSimpleElement(xmlStreamWriter, P, useCaseDetails.getDescription());
final String notes = useCaseDetails.getNotes();
if (!StringUtils.isEmpty(notes)) {
writeSimpleElement(xmlStreamWriter, H4, "Notes:");
final String[] splits = notes.split("\\n");
for (final String split : splits) {
writeSimpleElement(xmlStreamWriter, P, split);
}
}
final List<String> keywords = useCaseDetails.getKeywords();
if (!keywords.isEmpty()) {
writeSimpleElement(xmlStreamWriter, H4, "Keywords:");
xmlStreamWriter.writeCharacters(String.join(", ", keywords));
}
final String configuration = useCaseDetails.getConfiguration();
writeUseCaseConfiguration(configuration, xmlStreamWriter);
writeSimpleElement(xmlStreamWriter, BR, null);
}
}
@Override
void writeMultiComponentUseCases(final PythonProcessorDetails processorDetails, XMLStreamWriter xmlStreamWriter) throws XMLStreamException {
final List<MultiProcessorUseCaseDetails> useCaseDetailsList = processorDetails.getMultiProcessorUseCases();
if (useCaseDetailsList.isEmpty()) {
return;
}
writeSimpleElement(xmlStreamWriter, H2, "Example Use Cases Involving Other Components:");
for (final MultiProcessorUseCaseDetails useCase : useCaseDetailsList) {
writeSimpleElement(xmlStreamWriter, H3, "Use Case:");
writeSimpleElement(xmlStreamWriter, P, useCase.getDescription());
final String notes = useCase.getNotes();
if (!StringUtils.isEmpty(notes)) {
writeSimpleElement(xmlStreamWriter, H4, "Notes:");
final String[] splits = notes.split("\\n");
for (final String split : splits) {
writeSimpleElement(xmlStreamWriter, P, split);
}
}
final List<String> keywords = useCase.getKeywords();
if (!keywords.isEmpty()) {
writeSimpleElement(xmlStreamWriter, H4, "Keywords:");
xmlStreamWriter.writeCharacters(String.join(", ", keywords));
}
writeSimpleElement(xmlStreamWriter, H4, "Components involved:");
final List<ProcessorConfigurationDetails> processorConfigurations = useCase.getConfigurations();
for (final ProcessorConfigurationDetails processorConfiguration : processorConfigurations) {
writeSimpleElement(xmlStreamWriter, STRONG, "Component Type: ");
writeSimpleElement(xmlStreamWriter, SPAN, processorConfiguration.getProcessorType());
final String configuration = processorConfiguration.getConfiguration();
writeUseCaseConfiguration(configuration, xmlStreamWriter);
writeSimpleElement(xmlStreamWriter, BR, null);
}
writeSimpleElement(xmlStreamWriter, BR, null);
}
}
@Override
void writeProperties(final PythonProcessorDetails processorDetails, XMLStreamWriter xmlStreamWriter) throws XMLStreamException {
final List<PropertyDescription> properties = processorDetails.getPropertyDescriptions();
writeSimpleElement(xmlStreamWriter, H3, "Properties: ");
if (!properties.isEmpty()) {
final boolean containsExpressionLanguage = containsExpressionLanguage(processorDetails);
xmlStreamWriter.writeStartElement(P);
xmlStreamWriter.writeCharacters("In the list below, the names of required properties appear in ");
writeSimpleElement(xmlStreamWriter, STRONG, "bold");
xmlStreamWriter.writeCharacters(". Any other properties (not in bold) are considered optional. " +
"The table also indicates any default values");
if (containsExpressionLanguage) {
xmlStreamWriter.writeCharacters(", and whether a property supports the ");
writeLink(xmlStreamWriter, "NiFi Expression Language", "../../../../../html/expression-language-guide.html");
}
xmlStreamWriter.writeCharacters(".");
xmlStreamWriter.writeEndElement();
xmlStreamWriter.writeStartElement(TABLE);
xmlStreamWriter.writeAttribute(ID, "properties");
// write the header row
xmlStreamWriter.writeStartElement(TR);
writeSimpleElement(xmlStreamWriter, TH, "Display Name");
writeSimpleElement(xmlStreamWriter, TH, "API Name");
writeSimpleElement(xmlStreamWriter, TH, "Default Value");
writeSimpleElement(xmlStreamWriter, TH, "Description");
xmlStreamWriter.writeEndElement();
// write the individual properties
for (PropertyDescription property : properties) {
xmlStreamWriter.writeStartElement(TR);
xmlStreamWriter.writeStartElement(TD);
xmlStreamWriter.writeAttribute(ID, "name");
if (property.isRequired()) {
writeSimpleElement(xmlStreamWriter, STRONG, property.getDisplayName());
} else {
xmlStreamWriter.writeCharacters(property.getDisplayName());
}
xmlStreamWriter.writeEndElement();
writeSimpleElement(xmlStreamWriter, TD, property.getName());
writeSimpleElement(xmlStreamWriter, TD, property.getDefaultValue(), "default-value");
xmlStreamWriter.writeStartElement(TD);
xmlStreamWriter.writeAttribute(ID, "description");
if (property.getDescription() != null && !property.getDescription().trim().isEmpty()) {
xmlStreamWriter.writeCharacters(property.getDescription());
} else {
xmlStreamWriter.writeCharacters(NO_DESCRIPTION);
}
if (property.isSensitive()) {
xmlStreamWriter.writeEmptyElement(BR);
writeSimpleElement(xmlStreamWriter, STRONG, "Sensitive Property: true");
}
final ExpressionLanguageScope expressionLanguageScope = ExpressionLanguageScope.valueOf(property.getExpressionLanguageScope());
if (!expressionLanguageScope.equals(NONE)) {
xmlStreamWriter.writeEmptyElement(BR);
String text = "Supports Expression Language: true";
final String perFF = " (will be evaluated using flow file attributes and Environment variables)";
final String registry = " (will be evaluated using Environment variables only)";
final String undefined = " (undefined scope)";
switch (expressionLanguageScope) {
case FLOWFILE_ATTRIBUTES -> text += perFF;
case ENVIRONMENT -> text += registry;
default -> text += undefined;
}
writeSimpleElement(xmlStreamWriter, STRONG, text);
}
xmlStreamWriter.writeEndElement();
xmlStreamWriter.writeEndElement();
}
xmlStreamWriter.writeEndElement();
} else {
writeSimpleElement(xmlStreamWriter, P, NO_PROPERTIES);
}
}
@Override
void writeDynamicProperties(final PythonProcessorDetails processorDetails, XMLStreamWriter xmlStreamWriter) {
// Not supported
}
@Override
void writeSystemResourceConsiderationInfo(final PythonProcessorDetails processorDetails, XMLStreamWriter xmlStreamWriter) {
// Not supported
}
/**
* Indicates whether the component contains at least one property that supports Expression Language.
*
* @param processorDetails the component to interrogate
* @return whether the component contains at least one sensitive property.
*/
private boolean containsExpressionLanguage(final PythonProcessorDetails processorDetails) {
for (PropertyDescription description : processorDetails.getPropertyDescriptions()) {
if (!ExpressionLanguageScope.valueOf(description.getExpressionLanguageScope()).equals(NONE)) {
return true;
}
}
return false;
}
}

View File

@ -81,6 +81,7 @@ public class DocGeneratorTest {
.bundle(bundle)
.extensionType(Processor.class)
.implementationClassName(PROCESSOR_CLASS.getName())
.runtime(ExtensionDefinition.ExtensionRuntime.JAVA)
.build();
final Set<ExtensionDefinition> extensions = Collections.singleton(definition);
when(extensionManager.getExtensions(eq(Processor.class))).thenReturn(extensions);

View File

@ -0,0 +1,187 @@
/*
* 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.html;
import org.apache.nifi.documentation.DocumentationWriter;
import org.apache.nifi.python.PythonProcessorDetails;
import org.apache.nifi.python.processor.documentation.MultiProcessorUseCaseDetails;
import org.apache.nifi.python.processor.documentation.ProcessorConfigurationDetails;
import org.apache.nifi.python.processor.documentation.PropertyDescription;
import org.apache.nifi.python.processor.documentation.UseCaseDetails;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import static org.apache.nifi.documentation.html.AbstractHtmlDocumentationWriter.NO_DESCRIPTION;
import static org.apache.nifi.documentation.html.AbstractHtmlDocumentationWriter.NO_PROPERTIES;
import static org.apache.nifi.documentation.html.AbstractHtmlDocumentationWriter.NO_TAGS;
import static org.apache.nifi.documentation.html.XmlValidator.assertContains;
import static org.apache.nifi.documentation.html.XmlValidator.assertNotContains;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class HtmlPythonProcessorDocumentationWriterTest {
@Test
public void testProcessorDocumentation() throws IOException {
final PythonProcessorDetails processorDetails = getPythonProcessorDetails();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final DocumentationWriter<PythonProcessorDetails> writer = new HtmlPythonProcessorDocumentationWriter();
writer.write(processorDetails, outputStream, false);
final String results = outputStream.toString();
XmlValidator.assertXmlValid(results);
assertContains(results, "This is a test capability description");
assertContains(results, "tag1, tag2, tag3");
final List<PropertyDescription> propertyDescriptions = getPropertyDescriptions();
propertyDescriptions.forEach(propertyDescription -> {
assertContains(results, propertyDescription.getDisplayName());
assertContains(results, propertyDescription.getDescription());
assertContains(results, propertyDescription.getDefaultValue());
});
assertContains(results, "Supports Expression Language: true (will be evaluated using Environment variables only)");
assertContains(results, "Supports Expression Language: true (will be evaluated using flow file attributes and Environment variables)");
final List<UseCaseDetails> useCases = getUseCases();
useCases.forEach(useCase -> {
assertContains(results, useCase.getDescription());
assertContains(results, useCase.getNotes());
assertContains(results, String.join(", ", useCase.getKeywords()));
assertContains(results, useCase.getConfiguration());
});
final List<MultiProcessorUseCaseDetails> multiProcessorUseCases = getMultiProcessorUseCases();
multiProcessorUseCases.forEach(multiProcessorUseCase -> {
assertContains(results, multiProcessorUseCase.getDescription());
assertContains(results, multiProcessorUseCase.getNotes());
assertContains(results, String.join(", ", multiProcessorUseCase.getKeywords()));
multiProcessorUseCase.getConfigurations().forEach(configuration -> {
assertContains(results, configuration.getProcessorType());
assertContains(results, configuration.getConfiguration());
});
});
assertNotContains(results, NO_PROPERTIES);
assertNotContains(results, "No description provided.");
assertNotContains(results, NO_TAGS);
}
@Test
public void testEmptyProcessor() throws IOException {
final PythonProcessorDetails processorDetails = mock(PythonProcessorDetails.class);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final DocumentationWriter<PythonProcessorDetails> writer = new HtmlPythonProcessorDocumentationWriter();
writer.write(processorDetails, outputStream, false);
final String results = outputStream.toString();
XmlValidator.assertXmlValid(results);
assertContains(results, NO_DESCRIPTION);
assertContains(results, NO_TAGS);
assertContains(results, NO_PROPERTIES);
}
private PythonProcessorDetails getPythonProcessorDetails() {
final PythonProcessorDetails processorDetails = mock(PythonProcessorDetails.class);
when(processorDetails.getProcessorType()).thenReturn("TestPythonProcessor");
when(processorDetails.getProcessorVersion()).thenReturn("1.0.0");
when(processorDetails.getSourceLocation()).thenReturn("/source/location/TestPythonProcessor.py");
when(processorDetails.getCapabilityDescription()).thenReturn("This is a test capability description");
when(processorDetails.getTags()).thenReturn(List.of("tag1", "tag2", "tag3"));
when(processorDetails.getDependencies()).thenReturn(List.of("dependency1==0.1", "dependency2==0.2"));
when(processorDetails.getInterface()).thenReturn("org.apache.nifi.python.processor.FlowFileTransform");
when(processorDetails.getUseCases()).thenAnswer(invocation -> getUseCases());
when(processorDetails.getMultiProcessorUseCases()).thenAnswer(invocation -> getMultiProcessorUseCases());
when(processorDetails.getPropertyDescriptions()).thenAnswer(invocation -> getPropertyDescriptions());
return processorDetails;
}
private List<UseCaseDetails> getUseCases() {
final UseCaseDetails useCaseDetails = mock(UseCaseDetails.class);
when(useCaseDetails.getDescription()).thenReturn("Test use case description");
when(useCaseDetails.getNotes()).thenReturn("Test use case notes");
when(useCaseDetails.getKeywords()).thenReturn(List.of("use case keyword1", "use case keyword2"));
when(useCaseDetails.getConfiguration()).thenReturn("Test use case configuration");
return List.of(useCaseDetails);
}
private List<MultiProcessorUseCaseDetails> getMultiProcessorUseCases() {
final ProcessorConfigurationDetails configurationDetails1 = mock(ProcessorConfigurationDetails.class);
when(configurationDetails1.getProcessorType()).thenReturn("Test processor type 1");
when(configurationDetails1.getConfiguration()).thenReturn("Test configuration 1");
final ProcessorConfigurationDetails configurationDetails2 = mock(ProcessorConfigurationDetails.class);
when(configurationDetails2.getProcessorType()).thenReturn("Test processor type 2");
when(configurationDetails2.getConfiguration()).thenReturn("Test configuration 2");
final MultiProcessorUseCaseDetails useCaseDetails1 = mock(MultiProcessorUseCaseDetails.class);
when(useCaseDetails1.getDescription()).thenReturn("Test description 1");
when(useCaseDetails1.getNotes()).thenReturn("Test notes 1");
when(useCaseDetails1.getKeywords()).thenReturn(List.of("keyword1", "keyword2"));
when(useCaseDetails1.getConfigurations()).thenReturn(List.of(configurationDetails1, configurationDetails2));
final MultiProcessorUseCaseDetails useCaseDetails2 = mock(MultiProcessorUseCaseDetails.class);
when(useCaseDetails2.getDescription()).thenReturn("Test description 2");
when(useCaseDetails2.getNotes()).thenReturn("Test notes 2");
when(useCaseDetails2.getKeywords()).thenReturn(List.of("keyword3", "keyword4"));
when(useCaseDetails2.getConfigurations()).thenReturn(List.of(configurationDetails1, configurationDetails2));
return List.of(useCaseDetails1, useCaseDetails2);
}
private List<PropertyDescription> getPropertyDescriptions() {
final PropertyDescription description1 = mock(PropertyDescription.class);
when(description1.getName()).thenReturn("Property Description 1");
when(description1.getDisplayName()).thenReturn("Property Description Display name 1");
when(description1.getDescription()).thenReturn("This is a test description for Property Description 1");
when(description1.getExpressionLanguageScope()).thenReturn("FLOWFILE_ATTRIBUTES");
when(description1.getDefaultValue()).thenReturn("Test default value 1");
when(description1.isRequired()).thenReturn(true);
when(description1.isSensitive()).thenReturn(false);
final PropertyDescription description2 = mock(PropertyDescription.class);
when(description2.getName()).thenReturn("Property Description 2");
when(description2.getDisplayName()).thenReturn("Property Description Display name 2");
when(description2.getDescription()).thenReturn("This is a test description for Property Description 2");
when(description2.getExpressionLanguageScope()).thenReturn("ENVIRONMENT");
when(description2.getDefaultValue()).thenReturn("Test default value 2");
when(description2.isRequired()).thenReturn(false);
when(description2.isSensitive()).thenReturn(true);
final PropertyDescription description3 = mock(PropertyDescription.class);
when(description3.getName()).thenReturn("Property Description 3");
when(description3.getDisplayName()).thenReturn("Property Description Display name 3");
when(description3.getDescription()).thenReturn("This is a test description for Property Description 3");
when(description3.getExpressionLanguageScope()).thenReturn("NONE");
when(description3.getDefaultValue()).thenReturn("Test default value 3");
when(description3.isRequired()).thenReturn(true);
when(description3.isSensitive()).thenReturn(true);
return List.of(description1, description2, description3);
}
}

View File

@ -19,6 +19,7 @@ package org.apache.nifi.documentation.html;
import org.apache.nifi.annotation.behavior.SystemResource;
import org.apache.nifi.annotation.behavior.SystemResourceConsideration;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.components.ConfigurableComponent;
import org.apache.nifi.components.RequiredPermission;
import org.apache.nifi.documentation.DocumentationWriter;
import org.apache.nifi.documentation.example.DeprecatedProcessor;
@ -33,6 +34,9 @@ import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import static org.apache.nifi.documentation.html.AbstractHtmlDocumentationWriter.NO_DESCRIPTION;
import static org.apache.nifi.documentation.html.AbstractHtmlDocumentationWriter.NO_PROPERTIES;
import static org.apache.nifi.documentation.html.AbstractHtmlDocumentationWriter.NO_TAGS;
import static org.apache.nifi.documentation.html.XmlValidator.assertContains;
import static org.apache.nifi.documentation.html.XmlValidator.assertNotContains;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -47,7 +51,7 @@ public class ProcessorDocumentationWriterTest {
ProcessorInitializer initializer = new ProcessorInitializer(extensionManager);
initializer.initialize(processor);
DocumentationWriter writer = new HtmlProcessorDocumentationWriter(extensionManager);
DocumentationWriter<ConfigurableComponent> writer = new HtmlProcessorDocumentationWriter(extensionManager);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@ -84,10 +88,10 @@ public class ProcessorDocumentationWriterTest {
assertNotContains(results, "iconSecure.png");
assertContains(results, FullyDocumentedProcessor.class.getAnnotation(CapabilityDescription.class)
.value());
assertNotContains(results, "This component has no required or optional properties.");
assertNotContains(results, "No description provided.");
assertNotContains(results, NO_PROPERTIES);
assertNotContains(results, NO_DESCRIPTION);
assertNotContains(results, "No tags provided.");
assertNotContains(results, NO_TAGS);
assertNotContains(results, "Additional Details...");
// check expression language scope
@ -129,7 +133,7 @@ public class ProcessorDocumentationWriterTest {
ProcessorInitializer initializer = new ProcessorInitializer(extensionManager);
initializer.initialize(processor);
DocumentationWriter writer = new HtmlProcessorDocumentationWriter(extensionManager);
DocumentationWriter<ConfigurableComponent> writer = new HtmlProcessorDocumentationWriter(extensionManager);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@ -140,13 +144,13 @@ public class ProcessorDocumentationWriterTest {
XmlValidator.assertXmlValid(results);
// no description
assertContains(results, "No description provided.");
assertContains(results, NO_DESCRIPTION);
// no tags
assertContains(results, "No tags provided.");
assertContains(results, NO_TAGS);
// properties
assertContains(results, "This component has no required or optional properties.");
assertContains(results, NO_PROPERTIES);
// relationships
assertContains(results, "This processor has no relationships.");
@ -169,7 +173,7 @@ public class ProcessorDocumentationWriterTest {
ProcessorInitializer initializer = new ProcessorInitializer(extensionManager);
initializer.initialize(processor);
DocumentationWriter writer = new HtmlProcessorDocumentationWriter(extensionManager);
DocumentationWriter<ConfigurableComponent> writer = new HtmlProcessorDocumentationWriter(extensionManager);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@ -189,7 +193,7 @@ public class ProcessorDocumentationWriterTest {
ProcessorInitializer initializer = new ProcessorInitializer(extensionManager);
initializer.initialize(processor);
DocumentationWriter writer = new HtmlProcessorDocumentationWriter(extensionManager);
DocumentationWriter<ConfigurableComponent> writer = new HtmlProcessorDocumentationWriter(extensionManager);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
@ -229,9 +233,9 @@ public class ProcessorDocumentationWriterTest {
assertContains(results, "Deprecation notice: ");
// assertContains(results, DeprecatedProcessor.class.getAnnotation(DeprecationNotice.class.));
assertNotContains(results, "This component has no required or optional properties.");
assertNotContains(results, "No description provided.");
assertNotContains(results, "No tags provided.");
assertNotContains(results, NO_PROPERTIES);
assertNotContains(results, NO_DESCRIPTION);
assertNotContains(results, NO_TAGS);
assertNotContains(results, "Additional Details...");
// verify the right OnRemoved and OnShutdown methods were called

View File

@ -38,6 +38,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -54,6 +55,7 @@ import jakarta.servlet.ServletContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.NiFiServer;
import org.apache.nifi.bundle.Bundle;
import org.apache.nifi.bundle.BundleCoordinate;
import org.apache.nifi.bundle.BundleDetails;
import org.apache.nifi.cluster.ClusterDetailsFactory;
import org.apache.nifi.controller.DecommissionTask;
@ -73,6 +75,7 @@ import org.apache.nifi.flow.resource.ExternalResourceProviderService;
import org.apache.nifi.flow.resource.ExternalResourceProviderServiceBuilder;
import org.apache.nifi.flow.resource.PropertyBasedExternalResourceProviderInitializationContext;
import org.apache.nifi.lifecycle.LifeCycleStartException;
import org.apache.nifi.nar.ExtensionDefinition;
import org.apache.nifi.nar.ExtensionManager;
import org.apache.nifi.nar.ExtensionManagerHolder;
import org.apache.nifi.nar.ExtensionMapping;
@ -84,6 +87,7 @@ import org.apache.nifi.nar.NarThreadContextClassLoader;
import org.apache.nifi.nar.NarUnpackMode;
import org.apache.nifi.nar.StandardExtensionDiscoveringManager;
import org.apache.nifi.nar.StandardNarLoader;
import org.apache.nifi.processor.Processor;
import org.apache.nifi.security.util.TlsException;
import org.apache.nifi.services.FlowService;
import org.apache.nifi.ui.extension.UiExtension;
@ -122,6 +126,8 @@ import org.springframework.context.ApplicationContext;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import static org.apache.nifi.nar.ExtensionDefinition.ExtensionRuntime.PYTHON;
/**
* Encapsulates the Jetty instance.
*/
@ -746,9 +752,6 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
// Set the extension manager into the holder which makes it available to the Spring context via a factory bean
ExtensionManagerHolder.init(extensionManager);
// Generate docs for extensions
DocGenerator.generate(props, extensionManager, extensionMapping);
// Additionally loaded NARs and collected flow resources must be in place before starting the flows
narProviderService = new ExternalResourceProviderServiceBuilder("NAR Auto-Loader Provider", extensionManager)
.providers(buildExternalResourceProviders(extensionManager, NAR_PROVIDER_PREFIX, descriptor -> descriptor.getLocation().toLowerCase().endsWith(".nar")))
@ -826,10 +829,26 @@ public class JettyServer implements NiFiServer, ExtensionUiLoader {
clusterDetailsFactory = webApplicationContext.getBean("clusterDetailsFactory", ClusterDetailsFactory.class);
}
// Generate docs for extensions
DocGenerator.generate(props, extensionManager, extensionMapping);
// ensure the web document war was loaded and provide the extension mapping
if (webDocsContext != null) {
final Map<String, Set<BundleCoordinate>> pythonExtensionMapping = new HashMap<>();
final Set<ExtensionDefinition> extensionDefinitions = extensionManager.getExtensions(Processor.class)
.stream()
.filter(extension -> extension.getRuntime().equals(PYTHON))
.collect(Collectors.toSet());
extensionDefinitions.forEach(
extensionDefinition ->
pythonExtensionMapping.computeIfAbsent(extensionDefinition.getImplementationClassName(),
name -> new HashSet<>()).add(extensionDefinition.getBundle().getBundleDetails().getCoordinate()));
final ServletContext webDocsServletContext = webDocsContext.getServletHandler().getServletContext();
webDocsServletContext.setAttribute("nifi-extension-mapping", extensionMapping);
webDocsServletContext.setAttribute("nifi-python-extension-mapping", pythonExtensionMapping);
}
// if this nifi is a node in a cluster, start the flow service and load the flow - the

View File

@ -17,6 +17,7 @@
package org.apache.nifi.web.docs;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.bundle.BundleCoordinate;
import org.apache.nifi.nar.ExtensionMapping;
import jakarta.servlet.ServletConfig;
@ -27,8 +28,10 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.text.Collator;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
@ -58,11 +61,16 @@ public class DocumentationController extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
final ExtensionMapping extensionMappings = (ExtensionMapping) servletContext.getAttribute("nifi-extension-mapping");
final Map<String, Set<BundleCoordinate>> pythonExtensionMappings = (Map<String, Set<BundleCoordinate>>) servletContext.getAttribute("nifi-python-extension-mapping");
final Map<String, Set<BundleCoordinate>> processorNames = new HashMap<>();
processorNames.putAll(extensionMappings.getProcessorNames());
processorNames.putAll(pythonExtensionMappings);
final Collator collator = Collator.getInstance(Locale.US);
// create the processors lookup
final Map<String, String> processors = new TreeMap<>(collator);
for (final String processorClass : extensionMappings.getProcessorNames().keySet()) {
for (final String processorClass : processorNames.keySet()) {
processors.put(StringUtils.substringAfterLast(processorClass, "."), processorClass);
}
@ -92,7 +100,7 @@ public class DocumentationController extends HttpServlet {
// make the available components available to the documentation jsp
request.setAttribute("processors", processors);
request.setAttribute("processorBundleLookup", extensionMappings.getProcessorNames());
request.setAttribute("processorBundleLookup", processorNames);
request.setAttribute("controllerServices", controllerServices);
request.setAttribute("controllerServiceBundleLookup", extensionMappings.getControllerServiceNames());
request.setAttribute("reportingTasks", reportingTasks);