mirror of https://github.com/apache/nifi.git
NIFI-12970 Generate documentation for Python Processors
This closes #8579 Signed-off-by: David Handermann <exceptionfactory@apache.org>
This commit is contained in:
parent
d3ff1f53c4
commit
26e5f5a565
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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: <p
|
||||
* id="p-id">text</p>
|
||||
*
|
||||
* @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: <p>text</p>
|
||||
*
|
||||
* @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;
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue