This is an automated email from the ASF dual-hosted git repository. exceptionfactory pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/nifi.git
The following commit(s) were added to refs/heads/main by this push: new 26e5f5a565 NIFI-12970 Generate documentation for Python Processors 26e5f5a565 is described below commit 26e5f5a565950a4de8ded985df549c5b659a68a1 Author: Mark Bathori <mbath...@apache.org> AuthorDate: Thu Mar 28 18:34:11 2024 +0100 NIFI-12970 Generate documentation for Python Processors This closes #8579 Signed-off-by: David Handermann <exceptionfact...@apache.org> --- .../nifi-framework/nifi-documentation/pom.xml | 5 + .../apache/nifi/documentation/DocGenerator.java | 55 +- .../nifi/documentation/DocumentationWriter.java | 7 +- .../html/AbstractHtmlDocumentationWriter.java | 355 +++++++++++ .../html/HtmlDocumentationWriter.java | 685 ++++++--------------- .../html/HtmlProcessorDocumentationWriter.java | 82 +-- .../HtmlPythonProcessorDocumentationWriter.java | 280 +++++++++ .../nifi/documentation/DocGeneratorTest.java | 1 + ...HtmlPythonProcessorDocumentationWriterTest.java | 187 ++++++ .../html/ProcessorDocumentationWriterTest.java | 30 +- .../org/apache/nifi/web/server/JettyServer.java | 25 +- .../nifi/web/docs/DocumentationController.java | 12 +- 12 files changed, 1152 insertions(+), 572 deletions(-) diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/pom.xml index 9f9bdc60e3..86bacb3071 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/pom.xml +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/pom.xml @@ -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> diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java index e9cef0b5cb..e934302c0b 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocGenerator.java @@ -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 ) { diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocumentationWriter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocumentationWriter.java index 391873d4aa..068658d67f 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocumentationWriter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/DocumentationWriter.java @@ -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; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/AbstractHtmlDocumentationWriter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/AbstractHtmlDocumentationWriter.java new file mode 100644 index 0000000000..dc526c9db8 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/AbstractHtmlDocumentationWriter.java @@ -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; + +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlDocumentationWriter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlDocumentationWriter.java index 9bda0d5519..0d70b0ac13 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlDocumentationWriter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlDocumentationWriter.java @@ -42,26 +42,19 @@ import org.apache.nifi.components.resource.ResourceCardinality; import org.apache.nifi.components.resource.ResourceDefinition; import org.apache.nifi.components.resource.ResourceType; import org.apache.nifi.controller.ControllerService; -import org.apache.nifi.documentation.DocumentationWriter; import org.apache.nifi.nar.ExtensionDefinition; import org.apache.nifi.nar.ExtensionManager; import org.apache.nifi.util.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -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.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -70,15 +63,10 @@ import java.util.stream.Collectors; * they have no additional information. * */ -public class HtmlDocumentationWriter implements DocumentationWriter { +public class HtmlDocumentationWriter extends AbstractHtmlDocumentationWriter<ConfigurableComponent> { public static final Logger LOGGER = LoggerFactory.getLogger(HtmlDocumentationWriter.class); - /** - * The filename where additional user specified information may be stored. - */ - public static final String ADDITIONAL_DETAILS_HTML = "additionalDetails.html"; - private final ExtensionManager extensionManager; public HtmlDocumentationWriter(final ExtensionManager extensionManager) { @@ -86,128 +74,16 @@ public class HtmlDocumentationWriter implements DocumentationWriter { } @Override - public void write(final ConfigurableComponent configurableComponent, final OutputStream streamToWriteTo, - final boolean includesAdditionalDocumentation) throws IOException { - - try { - XMLStreamWriter xmlStreamWriter = XMLOutputFactory.newInstance().createXMLStreamWriter( - streamToWriteTo, "UTF-8"); - xmlStreamWriter.writeDTD("<!DOCTYPE html>"); - xmlStreamWriter.writeStartElement("html"); - xmlStreamWriter.writeAttribute("lang", "en"); - writeHead(configurableComponent, xmlStreamWriter); - writeBody(configurableComponent, 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 configurableComponent 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 ConfigurableComponent configurableComponent, - final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { - xmlStreamWriter.writeStartElement("head"); - xmlStreamWriter.writeStartElement("meta"); - xmlStreamWriter.writeAttribute("charset", "utf-8"); - xmlStreamWriter.writeEndElement(); - writeSimpleElement(xmlStreamWriter, "title", getTitle(configurableComponent)); - - 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(); - - } - - /** - * Gets the class name of the component. - * - * @param configurableComponent the component to describe - * @return the class name of the component - */ protected String getTitle(final ConfigurableComponent configurableComponent) { return configurableComponent.getClass().getSimpleName(); } - /** - * Writes the body section of the documentation, this consists of the - * component description, the tags, and the PropertyDescriptors. - * - * @param configurableComponent 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 - */ - private void writeBody(final ConfigurableComponent configurableComponent, - final XMLStreamWriter xmlStreamWriter, final boolean hasAdditionalDetails) - throws XMLStreamException { - xmlStreamWriter.writeStartElement("body"); - writeHeader(configurableComponent, xmlStreamWriter); - writeDeprecationWarning(configurableComponent, xmlStreamWriter); - writeDescription(configurableComponent, xmlStreamWriter, hasAdditionalDetails); - writeTags(configurableComponent, xmlStreamWriter); - writeProperties(configurableComponent, xmlStreamWriter); - writeDynamicProperties(configurableComponent, xmlStreamWriter); - writeAdditionalBodyInfo(configurableComponent, xmlStreamWriter); - writeStatefulInfo(configurableComponent, xmlStreamWriter); - writeRestrictedInfo(configurableComponent, xmlStreamWriter); - writeInputRequirementInfo(configurableComponent, xmlStreamWriter); - writeUseCases(configurableComponent, xmlStreamWriter); - writeMultiComponentUseCases(configurableComponent, xmlStreamWriter); - writeSystemResourceConsiderationInfo(configurableComponent, xmlStreamWriter); - writeSeeAlso(configurableComponent, xmlStreamWriter); - xmlStreamWriter.writeEndElement(); - } - - /** - * Write the header to be displayed when loaded outside an iframe. - * - * @param configurableComponent 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(ConfigurableComponent configurableComponent, 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(configurableComponent)); - xmlStreamWriter.writeEndElement(); - } - - /** - * Add in the documentation information regarding the component whether it accepts an - * incoming relationship or not. - * - * @param configurableComponent the component to describe - * @param xmlStreamWriter the stream writer to use - * @throws XMLStreamException thrown if there was a problem writing the XML - */ - private void writeInputRequirementInfo(ConfigurableComponent configurableComponent, XMLStreamWriter xmlStreamWriter) - throws XMLStreamException { + @Override + void writeInputRequirementInfo(final ConfigurableComponent configurableComponent, XMLStreamWriter xmlStreamWriter) throws XMLStreamException { final InputRequirement inputRequirement = configurableComponent.getClass().getAnnotation(InputRequirement.class); - if(inputRequirement != null) { - writeSimpleElement(xmlStreamWriter, "h3", "Input requirement: "); + if (inputRequirement != null) { + writeSimpleElement(xmlStreamWriter, H3, "Input requirement: "); switch (inputRequirement.value()) { case INPUT_FORBIDDEN: xmlStreamWriter.writeCharacters("This component does not allow an incoming relationship."); @@ -225,30 +101,23 @@ public class HtmlDocumentationWriter implements DocumentationWriter { } } - /** - * Write the description of the Stateful annotation if provided in this component. - * - * @param configurableComponent the component to describe - * @param xmlStreamWriter the stream writer to use - * @throws XMLStreamException thrown if there was a problem writing the XML - */ - private void writeStatefulInfo(ConfigurableComponent configurableComponent, XMLStreamWriter xmlStreamWriter) - throws XMLStreamException { + @Override + void writeStatefulInfo(final ConfigurableComponent configurableComponent, XMLStreamWriter xmlStreamWriter) throws XMLStreamException { final Stateful stateful = configurableComponent.getClass().getAnnotation(Stateful.class); - writeSimpleElement(xmlStreamWriter, "h3", "State management: "); + writeSimpleElement(xmlStreamWriter, H3, "State management: "); - if(stateful != null) { - xmlStreamWriter.writeStartElement("table"); - xmlStreamWriter.writeAttribute("id", "stateful"); - xmlStreamWriter.writeStartElement("tr"); - writeSimpleElement(xmlStreamWriter, "th", "Scope"); - writeSimpleElement(xmlStreamWriter, "th", "Description"); + if (stateful != null) { + xmlStreamWriter.writeStartElement(TABLE); + xmlStreamWriter.writeAttribute(ID, "stateful"); + xmlStreamWriter.writeStartElement(TR); + writeSimpleElement(xmlStreamWriter, TH, "Scope"); + writeSimpleElement(xmlStreamWriter, TH, "Description"); xmlStreamWriter.writeEndElement(); - xmlStreamWriter.writeStartElement("tr"); - writeSimpleElement(xmlStreamWriter, "td", join(stateful.scopes())); - writeSimpleElement(xmlStreamWriter, "td", stateful.description()); + xmlStreamWriter.writeStartElement(TR); + writeSimpleElement(xmlStreamWriter, TD, join(stateful.scopes())); + writeSimpleElement(xmlStreamWriter, TD, stateful.description()); xmlStreamWriter.writeEndElement(); xmlStreamWriter.writeEndElement(); @@ -257,20 +126,13 @@ public class HtmlDocumentationWriter implements DocumentationWriter { } } - /** - * Write the description of the Restricted annotation if provided in this component. - * - * @param configurableComponent the component to describe - * @param xmlStreamWriter the stream writer to use - * @throws XMLStreamException thrown if there was a problem writing the XML - */ - private void writeRestrictedInfo(ConfigurableComponent configurableComponent, XMLStreamWriter xmlStreamWriter) - throws XMLStreamException { + @Override + void writeRestrictedInfo(final ConfigurableComponent configurableComponent, XMLStreamWriter xmlStreamWriter) throws XMLStreamException { final Restricted restricted = configurableComponent.getClass().getAnnotation(Restricted.class); - writeSimpleElement(xmlStreamWriter, "h3", "Restricted: "); + writeSimpleElement(xmlStreamWriter, H3, "Restricted: "); - if(restricted != null) { + if (restricted != null) { final String value = restricted.value(); if (!StringUtils.isBlank(value)) { @@ -279,17 +141,17 @@ public class HtmlDocumentationWriter implements DocumentationWriter { final Restriction[] restrictions = restricted.restrictions(); if (restrictions != null && restrictions.length > 0) { - xmlStreamWriter.writeStartElement("table"); - xmlStreamWriter.writeAttribute("id", "restrictions"); - xmlStreamWriter.writeStartElement("tr"); - writeSimpleElement(xmlStreamWriter, "th", "Required Permission"); - writeSimpleElement(xmlStreamWriter, "th", "Explanation"); + xmlStreamWriter.writeStartElement(TABLE); + xmlStreamWriter.writeAttribute(ID, "restrictions"); + xmlStreamWriter.writeStartElement(TR); + writeSimpleElement(xmlStreamWriter, TH, "Required Permission"); + writeSimpleElement(xmlStreamWriter, TH, "Explanation"); xmlStreamWriter.writeEndElement(); for (Restriction restriction : restrictions) { - xmlStreamWriter.writeStartElement("tr"); - writeSimpleElement(xmlStreamWriter, "td", restriction.requiredPermission().getPermissionLabel()); - writeSimpleElement(xmlStreamWriter, "td", restriction.explanation()); + xmlStreamWriter.writeStartElement(TR); + writeSimpleElement(xmlStreamWriter, TD, restriction.requiredPermission().getPermissionLabel()); + writeSimpleElement(xmlStreamWriter, TD, restriction.explanation()); xmlStreamWriter.writeEndElement(); } @@ -302,36 +164,27 @@ public class HtmlDocumentationWriter implements DocumentationWriter { } } - /** - * Writes a warning about the deprecation of a component. - * - * @param configurableComponent the component to describe - * @param xmlStreamWriter the stream writer - * @throws XMLStreamException thrown if there was a problem writing to the - * XML stream - */ - private void writeDeprecationWarning(final ConfigurableComponent configurableComponent, - final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + @Override + void writeDeprecationWarning(final ConfigurableComponent configurableComponent, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { final DeprecationNotice deprecationNotice = configurableComponent.getClass().getAnnotation(DeprecationNotice.class); + if (deprecationNotice != null) { - xmlStreamWriter.writeStartElement("h2"); + xmlStreamWriter.writeStartElement(H2); xmlStreamWriter.writeCharacters("Deprecation notice: "); xmlStreamWriter.writeEndElement(); - xmlStreamWriter.writeStartElement("p"); + xmlStreamWriter.writeStartElement(P); xmlStreamWriter.writeCharacters(""); if (!StringUtils.isEmpty(deprecationNotice.reason())) { xmlStreamWriter.writeCharacters(deprecationNotice.reason()); } else { // Write a default note - xmlStreamWriter.writeCharacters("Please be aware this processor is deprecated and may be removed in " + - "the near future."); + xmlStreamWriter.writeCharacters("Please be aware this processor is deprecated and may be removed in the near future."); } xmlStreamWriter.writeEndElement(); - xmlStreamWriter.writeStartElement("p"); + xmlStreamWriter.writeStartElement(P); xmlStreamWriter.writeCharacters("Please consider using one the following alternatives: "); - Class<? extends ConfigurableComponent>[] componentNames = deprecationNotice.alternatives(); String[] classNames = deprecationNotice.classNames(); @@ -346,19 +199,13 @@ public class HtmlDocumentationWriter implements DocumentationWriter { } } - /** - * Writes the list of components that may be linked from this component. - * - * @param configurableComponent the component to describe - * @param xmlStreamWriter the stream writer to use - * @throws XMLStreamException thrown if there was a problem writing the XML - */ - private void writeSeeAlso(ConfigurableComponent configurableComponent, XMLStreamWriter xmlStreamWriter) - throws XMLStreamException { + @Override + void writeSeeAlso(final ConfigurableComponent configurableComponent, XMLStreamWriter xmlStreamWriter) throws XMLStreamException { final SeeAlso seeAlso = configurableComponent.getClass().getAnnotation(SeeAlso.class); + if (seeAlso != null) { - writeSimpleElement(xmlStreamWriter, "h3", "See Also:"); - xmlStreamWriter.writeStartElement("p"); + writeSimpleElement(xmlStreamWriter, H3, "See Also:"); + xmlStreamWriter.writeStartElement(P); Class<? extends ConfigurableComponent>[] componentNames = seeAlso.value(); String[] classNames = seeAlso.classNames(); @@ -366,40 +213,29 @@ public class HtmlDocumentationWriter implements DocumentationWriter { // Write alternatives iterateAndLinkComponents(xmlStreamWriter, componentNames, classNames, ", ", configurableComponent.getClass().getSimpleName()); } else { - xmlStreamWriter.writeCharacters("No tags provided."); + xmlStreamWriter.writeCharacters(NO_TAGS); } xmlStreamWriter.writeEndElement(); } } - /** - * This method may be overridden by sub classes to write additional - * information to the body of the documentation. - * - * @param configurableComponent 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 ConfigurableComponent configurableComponent, - final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { - - } - - private void writeTags(final ConfigurableComponent configurableComponent, - final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + @Override + void writeTags(final ConfigurableComponent configurableComponent, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { final Tags tags = configurableComponent.getClass().getAnnotation(Tags.class); - xmlStreamWriter.writeStartElement("h3"); + + xmlStreamWriter.writeStartElement(H3); xmlStreamWriter.writeCharacters("Tags: "); xmlStreamWriter.writeEndElement(); - xmlStreamWriter.writeStartElement("p"); + xmlStreamWriter.writeStartElement(P); + if (tags != null) { final String tagString = join(tags.value()); xmlStreamWriter.writeCharacters(tagString); } else { - xmlStreamWriter.writeCharacters("No tags provided."); + xmlStreamWriter.writeCharacters(NO_TAGS); } + xmlStreamWriter.writeEndElement(); } @@ -409,124 +245,95 @@ public class HtmlDocumentationWriter implements DocumentationWriter { .collect(Collectors.joining(", ")); } - /** - * Writes a description of the configurable component. - * - * @param configurableComponent 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 ConfigurableComponent configurableComponent, - final XMLStreamWriter xmlStreamWriter, final boolean hasAdditionalDetails) - throws XMLStreamException { - writeSimpleElement(xmlStreamWriter, "h2", "Description: "); - writeSimpleElement(xmlStreamWriter, "p", getDescription(configurableComponent)); - if (hasAdditionalDetails) { - xmlStreamWriter.writeStartElement("p"); - - writeLink(xmlStreamWriter, "Additional Details...", ADDITIONAL_DETAILS_HTML); - - xmlStreamWriter.writeEndElement(); - } - } - - /** - * Gets a description of the ConfigurableComponent using the - * CapabilityDescription annotation. - * - * @param configurableComponent the component to describe - * @return a description of the configurableComponent - */ - protected String getDescription(final ConfigurableComponent configurableComponent) { - final CapabilityDescription capabilityDescription = configurableComponent.getClass().getAnnotation( - CapabilityDescription.class); + @Override + String getDescription(final ConfigurableComponent configurableComponent) { + final CapabilityDescription capabilityDescription = configurableComponent.getClass().getAnnotation(CapabilityDescription.class); final String description; if (capabilityDescription != null) { description = capabilityDescription.value(); } else { - description = "No description provided."; + description = NO_DESCRIPTION; } return description; } - protected void writeUseCases(final ConfigurableComponent component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + @Override + void writeUseCases(final ConfigurableComponent component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { final UseCase[] useCases = component.getClass().getAnnotationsByType(UseCase.class); if (useCases.length == 0) { return; } - writeSimpleElement(xmlStreamWriter, "h2", "Example Use Cases:"); + writeSimpleElement(xmlStreamWriter, H2, "Example Use Cases:"); for (final UseCase useCase : useCases) { - writeSimpleElement(xmlStreamWriter, "h3", "Use Case:"); - writeSimpleElement(xmlStreamWriter, "p", useCase.description()); + writeSimpleElement(xmlStreamWriter, H3, "Use Case:"); + writeSimpleElement(xmlStreamWriter, P, useCase.description()); final String notes = useCase.notes(); if (!StringUtils.isEmpty(notes)) { - writeSimpleElement(xmlStreamWriter, "h4", "Notes:"); + writeSimpleElement(xmlStreamWriter, H4, "Notes:"); final String[] splits = notes.split("\\n"); for (final String split : splits) { - writeSimpleElement(xmlStreamWriter, "p", split); + writeSimpleElement(xmlStreamWriter, P, split); } } final String[] keywords = useCase.keywords(); if (keywords.length > 0) { - writeSimpleElement(xmlStreamWriter, "h4", "Keywords:"); + writeSimpleElement(xmlStreamWriter, H4, "Keywords:"); xmlStreamWriter.writeCharacters(String.join(", ", keywords)); } final Requirement inputRequirement = useCase.inputRequirement(); if (inputRequirement != Requirement.INPUT_ALLOWED) { - writeSimpleElement(xmlStreamWriter, "h4", "Input Requirement:"); + writeSimpleElement(xmlStreamWriter, H4, "Input Requirement:"); xmlStreamWriter.writeCharacters(inputRequirement.toString()); } final String configuration = useCase.configuration(); writeUseCaseConfiguration(configuration, xmlStreamWriter); - writeSimpleElement(xmlStreamWriter, "br", null); + writeSimpleElement(xmlStreamWriter, BR, null); } } - protected void writeMultiComponentUseCases(final ConfigurableComponent component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { + @Override + void writeMultiComponentUseCases(final ConfigurableComponent component, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { final MultiProcessorUseCase[] useCases = component.getClass().getAnnotationsByType(MultiProcessorUseCase.class); if (useCases.length == 0) { return; } - writeSimpleElement(xmlStreamWriter, "h2", "Example Use Cases Involving Other Components:"); + writeSimpleElement(xmlStreamWriter, H2, "Example Use Cases Involving Other Components:"); for (final MultiProcessorUseCase useCase : useCases) { - writeSimpleElement(xmlStreamWriter, "h3", "Use Case:"); - writeSimpleElement(xmlStreamWriter, "p", useCase.description()); + writeSimpleElement(xmlStreamWriter, H3, "Use Case:"); + writeSimpleElement(xmlStreamWriter, P, useCase.description()); final String notes = useCase.notes(); if (!StringUtils.isEmpty(notes)) { - writeSimpleElement(xmlStreamWriter, "h4", "Notes:"); + writeSimpleElement(xmlStreamWriter, H4, "Notes:"); final String[] splits = notes.split("\\n"); for (final String split : splits) { - writeSimpleElement(xmlStreamWriter, "p", split); + writeSimpleElement(xmlStreamWriter, P, split); } } final String[] keywords = useCase.keywords(); if (keywords.length > 0) { - writeSimpleElement(xmlStreamWriter, "h4", "Keywords:"); + writeSimpleElement(xmlStreamWriter, H4, "Keywords:"); xmlStreamWriter.writeCharacters(String.join(", ", keywords)); } - writeSimpleElement(xmlStreamWriter, "h4", "Components involved:"); + writeSimpleElement(xmlStreamWriter, H4, "Components involved:"); final ProcessorConfiguration[] processorConfigurations = useCase.configurations(); for (final ProcessorConfiguration processorConfiguration : processorConfigurations) { - writeSimpleElement(xmlStreamWriter, "strong", "Component Type: "); + writeSimpleElement(xmlStreamWriter, STRONG, "Component Type: "); final String extensionClassName; if (processorConfiguration.processorClassName().isEmpty()) { @@ -535,73 +342,29 @@ public class HtmlDocumentationWriter implements DocumentationWriter { extensionClassName = processorConfiguration.processorClassName(); } - writeSimpleElement(xmlStreamWriter, "span", extensionClassName); + writeSimpleElement(xmlStreamWriter, SPAN, extensionClassName); final String configuration = processorConfiguration.configuration(); writeUseCaseConfiguration(configuration, xmlStreamWriter); - writeSimpleElement(xmlStreamWriter, "br", null); + writeSimpleElement(xmlStreamWriter, BR, null); } - writeSimpleElement(xmlStreamWriter, "br", null); - } - } - - private 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(); + writeSimpleElement(xmlStreamWriter, BR, null); } } - /** - * Writes the PropertyDescriptors out as a table. - * - * @param configurableComponent the component to describe - * @param xmlStreamWriter the stream writer - * @throws XMLStreamException thrown if there was a problem writing to the - * XML Stream - */ - protected void writeProperties(final ConfigurableComponent configurableComponent, - final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { - + @Override + void writeProperties(final ConfigurableComponent configurableComponent, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { final List<PropertyDescriptor> properties = configurableComponent.getPropertyDescriptors(); - writeSimpleElement(xmlStreamWriter, "h3", "Properties: "); + writeSimpleElement(xmlStreamWriter, H3, "Properties: "); - if (properties.size() > 0) { + if (!properties.isEmpty()) { final boolean containsExpressionLanguage = containsExpressionLanguage(configurableComponent); - xmlStreamWriter.writeStartElement("p"); + xmlStreamWriter.writeStartElement(P); xmlStreamWriter.writeCharacters("In the list below, the names of required properties appear in "); - writeSimpleElement(xmlStreamWriter, "strong", "bold"); + writeSimpleElement(xmlStreamWriter, STRONG, "bold"); xmlStreamWriter.writeCharacters(". Any other properties (not in bold) are considered optional. " + "The table also indicates any default values"); if (containsExpressionLanguage) { @@ -611,54 +374,54 @@ public class HtmlDocumentationWriter implements DocumentationWriter { xmlStreamWriter.writeCharacters("."); xmlStreamWriter.writeEndElement(); - xmlStreamWriter.writeStartElement("table"); - xmlStreamWriter.writeAttribute("id", "properties"); + 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", "Allowable Values"); - writeSimpleElement(xmlStreamWriter, "th", "Description"); + xmlStreamWriter.writeStartElement(TR); + writeSimpleElement(xmlStreamWriter, TH, "Display Name"); + writeSimpleElement(xmlStreamWriter, TH, "API Name"); + writeSimpleElement(xmlStreamWriter, TH, "Default Value"); + writeSimpleElement(xmlStreamWriter, TH, "Allowable Values"); + writeSimpleElement(xmlStreamWriter, TH, "Description"); xmlStreamWriter.writeEndElement(); // write the individual properties for (PropertyDescriptor property : properties) { - xmlStreamWriter.writeStartElement("tr"); - xmlStreamWriter.writeStartElement("td"); - xmlStreamWriter.writeAttribute("id", "name"); + xmlStreamWriter.writeStartElement(TR); + xmlStreamWriter.writeStartElement(TD); + xmlStreamWriter.writeAttribute(ID, "name"); if (property.isRequired()) { - writeSimpleElement(xmlStreamWriter, "strong", property.getDisplayName()); + writeSimpleElement(xmlStreamWriter, STRONG, property.getDisplayName()); } else { xmlStreamWriter.writeCharacters(property.getDisplayName()); } xmlStreamWriter.writeEndElement(); - writeSimpleElement(xmlStreamWriter, "td", property.getName()); - writeSimpleElement(xmlStreamWriter, "td", getDefaultValue(property), "default-value"); - xmlStreamWriter.writeStartElement("td"); - xmlStreamWriter.writeAttribute("id", "allowable-values"); + writeSimpleElement(xmlStreamWriter, TD, property.getName()); + writeSimpleElement(xmlStreamWriter, TD, getDefaultValue(property), "default-value"); + xmlStreamWriter.writeStartElement(TD); + xmlStreamWriter.writeAttribute(ID, "allowable-values"); writeValidValues(xmlStreamWriter, property); xmlStreamWriter.writeEndElement(); - xmlStreamWriter.writeStartElement("td"); - xmlStreamWriter.writeAttribute("id", "description"); - if (property.getDescription() != null && property.getDescription().trim().length() > 0) { + xmlStreamWriter.writeStartElement(TD); + xmlStreamWriter.writeAttribute(ID, "description"); + if (property.getDescription() != null && !property.getDescription().trim().isEmpty()) { xmlStreamWriter.writeCharacters(property.getDescription()); } else { - xmlStreamWriter.writeCharacters("No Description Provided."); + xmlStreamWriter.writeCharacters(NO_DESCRIPTION); } if (property.isSensitive()) { - xmlStreamWriter.writeEmptyElement("br"); - writeSimpleElement(xmlStreamWriter, "strong", "Sensitive Property: true"); + xmlStreamWriter.writeEmptyElement(BR); + writeSimpleElement(xmlStreamWriter, STRONG, "Sensitive Property: true"); } final ResourceDefinition resourceDefinition = property.getResourceDefinition(); if (resourceDefinition != null) { - xmlStreamWriter.writeEmptyElement("br"); - xmlStreamWriter.writeEmptyElement("br"); - xmlStreamWriter.writeStartElement("strong"); + xmlStreamWriter.writeEmptyElement(BR); + xmlStreamWriter.writeEmptyElement(BR); + xmlStreamWriter.writeStartElement(STRONG); final ResourceCardinality cardinality = resourceDefinition.getCardinality(); final Set<ResourceType> resourceTypes = resourceDefinition.getResourceTypes(); @@ -667,32 +430,32 @@ public class HtmlDocumentationWriter implements DocumentationWriter { xmlStreamWriter.writeCharacters("This property expects a comma-separated list of " + resourceTypes.iterator().next() + " resources"); } else { xmlStreamWriter.writeCharacters("This property expects a comma-separated list of resources. Each of the resources may be of any of the following types: " + - StringUtils.join(resourceDefinition.getResourceTypes(), ", ")); + StringUtils.join(resourceDefinition.getResourceTypes(), ", ")); } } else { if (resourceTypes.size() == 1) { xmlStreamWriter.writeCharacters("This property requires exactly one " + resourceTypes.iterator().next() + " to be provided."); } else { xmlStreamWriter.writeCharacters("This property requires exactly one resource to be provided. That resource may be any of the following types: " + - StringUtils.join(resourceDefinition.getResourceTypes(), ", ")); + StringUtils.join(resourceDefinition.getResourceTypes(), ", ")); } } xmlStreamWriter.writeCharacters("."); xmlStreamWriter.writeEndElement(); - xmlStreamWriter.writeEmptyElement("br"); + xmlStreamWriter.writeEmptyElement(BR); } if (property.isExpressionLanguageSupported()) { - xmlStreamWriter.writeEmptyElement("br"); + 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 InputRequirement inputRequirement = configurableComponent.getClass().getAnnotation(InputRequirement.class); - switch(property.getExpressionLanguageScope()) { + switch (property.getExpressionLanguageScope()) { case FLOWFILE_ATTRIBUTES: - if(inputRequirement != null && inputRequirement.value().equals(Requirement.INPUT_FORBIDDEN)) { + if (inputRequirement != null && inputRequirement.value().equals(Requirement.INPUT_FORBIDDEN)) { text += registry; } else { text += perFF; @@ -708,21 +471,21 @@ public class HtmlDocumentationWriter implements DocumentationWriter { break; } - writeSimpleElement(xmlStreamWriter, "strong", text); + writeSimpleElement(xmlStreamWriter, STRONG, text); } final Set<PropertyDependency> dependencies = property.getDependencies(); if (!dependencies.isEmpty()) { - xmlStreamWriter.writeEmptyElement("br"); - xmlStreamWriter.writeEmptyElement("br"); + xmlStreamWriter.writeEmptyElement(BR); + xmlStreamWriter.writeEmptyElement(BR); final boolean capitalizeThe; if (dependencies.size() == 1) { - writeSimpleElement(xmlStreamWriter, "strong", "This Property is only considered if "); + writeSimpleElement(xmlStreamWriter, STRONG, "This Property is only considered if "); capitalizeThe = false; } else { - writeSimpleElement(xmlStreamWriter, "strong", "This Property is only considered if all of the following conditions are met:"); - xmlStreamWriter.writeStartElement("ul"); + writeSimpleElement(xmlStreamWriter, STRONG, "This Property is only considered if all of the following conditions are met:"); + xmlStreamWriter.writeStartElement(UL); capitalizeThe = true; } @@ -778,7 +541,7 @@ public class HtmlDocumentationWriter implements DocumentationWriter { } } - final String elementName = dependencies.size() > 1 ? "li" : "strong"; + final String elementName = dependencies.size() > 1 ? LI : STRONG; writeSimpleElement(xmlStreamWriter, elementName, prefix + suffix); } @@ -795,7 +558,7 @@ public class HtmlDocumentationWriter implements DocumentationWriter { xmlStreamWriter.writeEndElement(); } else { - writeSimpleElement(xmlStreamWriter, "p", "This component has no required or optional properties."); + writeSimpleElement(xmlStreamWriter, P, NO_PROPERTIES); } } @@ -819,10 +582,10 @@ public class HtmlDocumentationWriter implements DocumentationWriter { } /** - * Indicates whether or not the component contains at least one property that supports Expression Language. + * Indicates whether the component contains at least one property that supports Expression Language. * * @param component the component to interrogate - * @return whether or not the component contains at least one sensitive property. + * @return whether the component contains at least one sensitive property. */ private boolean containsExpressionLanguage(final ConfigurableComponent component) { for (PropertyDescriptor descriptor : component.getPropertyDescriptors()) { @@ -833,43 +596,42 @@ public class HtmlDocumentationWriter implements DocumentationWriter { return false; } - - private void writeDynamicProperties(final ConfigurableComponent configurableComponent, - final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { - + @Override + void writeDynamicProperties(final ConfigurableComponent configurableComponent, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { final List<DynamicProperty> dynamicProperties = getDynamicProperties(configurableComponent); - if (dynamicProperties.size() > 0) { - writeSimpleElement(xmlStreamWriter, "h3", "Dynamic Properties: "); + if (!dynamicProperties.isEmpty()) { + writeSimpleElement(xmlStreamWriter, H3, "Dynamic Properties: "); writeSupportsSensitiveDynamicProperties(configurableComponent, xmlStreamWriter); - xmlStreamWriter.writeStartElement("p"); - xmlStreamWriter - .writeCharacters("Dynamic Properties allow the user to specify both the name and value of a property."); - xmlStreamWriter.writeStartElement("table"); - xmlStreamWriter.writeAttribute("id", "dynamic-properties"); - xmlStreamWriter.writeStartElement("tr"); - writeSimpleElement(xmlStreamWriter, "th", "Name"); - writeSimpleElement(xmlStreamWriter, "th", "Value"); - writeSimpleElement(xmlStreamWriter, "th", "Description"); + xmlStreamWriter.writeStartElement(P); + xmlStreamWriter.writeCharacters("Dynamic Properties allow the user to specify both the name and value of a property."); + xmlStreamWriter.writeStartElement(TABLE); + xmlStreamWriter.writeAttribute(ID, "dynamic-properties"); + xmlStreamWriter.writeStartElement(TR); + writeSimpleElement(xmlStreamWriter, TH, "Name"); + writeSimpleElement(xmlStreamWriter, TH, "Value"); + writeSimpleElement(xmlStreamWriter, TH, "Description"); xmlStreamWriter.writeEndElement(); for (final DynamicProperty dynamicProperty : dynamicProperties) { - xmlStreamWriter.writeStartElement("tr"); - writeSimpleElement(xmlStreamWriter, "td", dynamicProperty.name(), "name"); - writeSimpleElement(xmlStreamWriter, "td", dynamicProperty.value(), "value"); - xmlStreamWriter.writeStartElement("td"); + xmlStreamWriter.writeStartElement(TR); + writeSimpleElement(xmlStreamWriter, TD, dynamicProperty.name(), "name"); + writeSimpleElement(xmlStreamWriter, TD, dynamicProperty.value(), "value"); + xmlStreamWriter.writeStartElement(TD); xmlStreamWriter.writeCharacters(dynamicProperty.description()); - xmlStreamWriter.writeEmptyElement("br"); + xmlStreamWriter.writeEmptyElement(BR); final String text = switch (dynamicProperty.expressionLanguageScope()) { - case FLOWFILE_ATTRIBUTES -> "Supports Expression Language: true (will be evaluated using flow file attributes and Environment variables)"; - case ENVIRONMENT -> "Supports Expression Language: true (will be evaluated using Environment variables only)"; + case FLOWFILE_ATTRIBUTES -> + "Supports Expression Language: true (will be evaluated using flow file attributes and Environment variables)"; + case ENVIRONMENT -> + "Supports Expression Language: true (will be evaluated using Environment variables only)"; default -> "Supports Expression Language: false"; }; - writeSimpleElement(xmlStreamWriter, "strong", text); + writeSimpleElement(xmlStreamWriter, STRONG, text); xmlStreamWriter.writeEndElement(); xmlStreamWriter.writeEndElement(); } @@ -883,15 +645,15 @@ public class HtmlDocumentationWriter implements DocumentationWriter { final boolean supportsSensitiveDynamicProperties = configurableComponent.getClass().isAnnotationPresent(SupportsSensitiveDynamicProperties.class); final String sensitiveDynamicPropertiesLabel = supportsSensitiveDynamicProperties ? "Yes" : "No"; - writer.writeStartElement("p"); + writer.writeStartElement(P); writer.writeCharacters("Supports Sensitive Dynamic Properties: "); - writeSimpleElement(writer, "strong", sensitiveDynamicPropertiesLabel); + writeSimpleElement(writer, STRONG, sensitiveDynamicPropertiesLabel); writer.writeEndElement(); } - private List<DynamicProperty> getDynamicProperties(ConfigurableComponent configurableComponent) { + private List<DynamicProperty> getDynamicProperties(final ConfigurableComponent configurableComponent) { final List<DynamicProperty> dynamicProperties = new ArrayList<>(); final DynamicProperties dynProps = configurableComponent.getClass().getAnnotation(DynamicProperties.class); if (dynProps != null) { @@ -906,32 +668,27 @@ public class HtmlDocumentationWriter implements DocumentationWriter { return dynamicProperties; } - private void writeValidValueDescription(XMLStreamWriter xmlStreamWriter, String description) - throws XMLStreamException { + private void writeValidValueDescription(XMLStreamWriter xmlStreamWriter, String description) throws XMLStreamException { xmlStreamWriter.writeCharacters(" "); xmlStreamWriter.writeStartElement("img"); xmlStreamWriter.writeAttribute("src", "../../../../../html/images/iconInfo.png"); xmlStreamWriter.writeAttribute("alt", description); xmlStreamWriter.writeAttribute("title", description); xmlStreamWriter.writeEndElement(); - } /** - * Interrogates a PropertyDescriptor to get a list of AllowableValues, if - * there are none, nothing is written to the stream. + * Interrogates a PropertyDescriptor to get a list of AllowableValues, if there are none, nothing is written to the stream. * * @param xmlStreamWriter the stream writer to use - * @param property the property to describe - * @throws XMLStreamException thrown if there was a problem writing to the - * XML Stream + * @param property the property to describe + * @throws XMLStreamException thrown if there was a problem writing to the XML Stream */ - protected void writeValidValues(XMLStreamWriter xmlStreamWriter, PropertyDescriptor property) - throws XMLStreamException { - if (property.getAllowableValues() != null && property.getAllowableValues().size() > 0) { - xmlStreamWriter.writeStartElement("ul"); + protected void writeValidValues(XMLStreamWriter xmlStreamWriter, PropertyDescriptor property) throws XMLStreamException { + if (property.getAllowableValues() != null && !property.getAllowableValues().isEmpty()) { + xmlStreamWriter.writeStartElement(UL); for (AllowableValue value : property.getAllowableValues()) { - xmlStreamWriter.writeStartElement("li"); + xmlStreamWriter.writeStartElement(LI); xmlStreamWriter.writeCharacters(value.getDisplayName()); if (value.getDescription() != null) { @@ -944,16 +701,16 @@ public class HtmlDocumentationWriter implements DocumentationWriter { } else if (property.getControllerServiceDefinition() != null) { Class<? extends ControllerService> controllerServiceClass = property.getControllerServiceDefinition(); - writeSimpleElement(xmlStreamWriter, "strong", "Controller Service API: "); - xmlStreamWriter.writeEmptyElement("br"); + writeSimpleElement(xmlStreamWriter, STRONG, "Controller Service API: "); + xmlStreamWriter.writeEmptyElement(BR); xmlStreamWriter.writeCharacters(controllerServiceClass.getSimpleName()); final Class<? extends ControllerService>[] serviceImplementations = lookupControllerServiceImpls(controllerServiceClass); - xmlStreamWriter.writeEmptyElement("br"); + xmlStreamWriter.writeEmptyElement(BR); if (serviceImplementations.length > 0) { final String title = serviceImplementations.length > 1 ? "Implementations: " : "Implementation: "; - writeSimpleElement(xmlStreamWriter, "strong", title); + writeSimpleElement(xmlStreamWriter, STRONG, title); iterateAndLinkComponents(xmlStreamWriter, serviceImplementations, null, "<br>", controllerServiceClass.getSimpleName()); } else { xmlStreamWriter.writeCharacters("No implementations found."); @@ -961,86 +718,22 @@ public class HtmlDocumentationWriter implements DocumentationWriter { } } - /** - * 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(); - } - - /** - * Writes all the system resource considerations for this component - * - * @param configurableComponent the component to describe - * @param xmlStreamWriter the xml stream writer to use - * @throws XMLStreamException thrown if there was a problem writing the XML - */ - private void writeSystemResourceConsiderationInfo(ConfigurableComponent configurableComponent, XMLStreamWriter xmlStreamWriter) - throws XMLStreamException { - + @Override + void writeSystemResourceConsiderationInfo(final ConfigurableComponent configurableComponent, XMLStreamWriter xmlStreamWriter) throws XMLStreamException { SystemResourceConsideration[] systemResourceConsiderations = configurableComponent.getClass().getAnnotationsByType(SystemResourceConsideration.class); - writeSimpleElement(xmlStreamWriter, "h3", "System Resource Considerations:"); + writeSimpleElement(xmlStreamWriter, H3, "System Resource Considerations:"); if (systemResourceConsiderations.length > 0) { - xmlStreamWriter.writeStartElement("table"); - xmlStreamWriter.writeAttribute("id", "system-resource-considerations"); - xmlStreamWriter.writeStartElement("tr"); - writeSimpleElement(xmlStreamWriter, "th", "Resource"); - writeSimpleElement(xmlStreamWriter, "th", "Description"); + xmlStreamWriter.writeStartElement(TABLE); + xmlStreamWriter.writeAttribute(ID, "system-resource-considerations"); + xmlStreamWriter.writeStartElement(TR); + writeSimpleElement(xmlStreamWriter, TH, "Resource"); + writeSimpleElement(xmlStreamWriter, TH, "Description"); xmlStreamWriter.writeEndElement(); for (SystemResourceConsideration systemResourceConsideration : systemResourceConsiderations) { - xmlStreamWriter.writeStartElement("tr"); - writeSimpleElement(xmlStreamWriter, "td", systemResourceConsideration.resource().name()); - writeSimpleElement(xmlStreamWriter, "td", systemResourceConsideration.description().trim().isEmpty() + xmlStreamWriter.writeStartElement(TR); + writeSimpleElement(xmlStreamWriter, TD, systemResourceConsideration.resource().name()); + writeSimpleElement(xmlStreamWriter, TD, systemResourceConsideration.description().trim().isEmpty() ? "Not Specified" : systemResourceConsideration.description()); xmlStreamWriter.writeEndElement(); } @@ -1059,9 +752,7 @@ public class HtmlDocumentationWriter implements DocumentationWriter { * @return an array of controller services that implement the controller service API */ @SuppressWarnings("unchecked") - private Class<? extends ControllerService>[] lookupControllerServiceImpls( - final Class<? extends ControllerService> parent) { - + private Class<? extends ControllerService>[] lookupControllerServiceImpls(final Class<? extends ControllerService> parent) { final List<Class<? extends ControllerService>> implementations = new ArrayList<>(); // first get all ControllerService implementations @@ -1082,38 +773,34 @@ public class HtmlDocumentationWriter implements DocumentationWriter { /** * Writes a link to another configurable component * - * @param xmlStreamWriter the xml stream writer - * @param linkedComponents the array of configurable component to link to - * @param classNames the array of class names in string format to link to - * @param separator a separator used to split the values (in case more than 1. If the separator is enclosed in - * between "<" and ">" (.e.g "<br>" it is treated as a tag and written to the xmlStreamWriter as an - * empty tag + * @param xmlStreamWriter the xml stream writer + * @param linkedComponents the array of configurable component to link to + * @param classNames the array of class names in string format to link to + * @param separator a separator used to split the values (in case more than 1. If the separator is enclosed in + * between "<" and ">" (.e.g "<br>" it is treated as a tag and written to the xmlStreamWriter as an empty tag * @param sourceContextName the source context/name of the item being linked * @throws XMLStreamException thrown if there is a problem writing the XML */ protected void iterateAndLinkComponents(final XMLStreamWriter xmlStreamWriter, final Class<? extends ConfigurableComponent>[] linkedComponents, - final String[] classNames, final String separator, final String sourceContextName) - throws XMLStreamException { + final String[] classNames, final String separator, final String sourceContextName) throws XMLStreamException { String effectiveSeparator = separator; // Treat the the possible separators final boolean separatorIsElement = effectiveSeparator.startsWith("<") && effectiveSeparator.endsWith(">"); // Whatever the result, strip the possible < and > characters - effectiveSeparator = effectiveSeparator.replaceAll("<([^>]*)>","$1"); + effectiveSeparator = effectiveSeparator.replaceAll("<([^>]*)>", "$1"); int index = 0; - for (final Class<? extends ConfigurableComponent> linkedComponent : linkedComponents ) { + for (final Class<? extends ConfigurableComponent> linkedComponent : linkedComponents) { final String linkedComponentName = linkedComponent.getName(); final List<Bundle> linkedComponentBundles = extensionManager.getBundles(linkedComponentName); - if (linkedComponentBundles != null && linkedComponentBundles.size() > 0) { - final Bundle firstLinkedComponentBundle = linkedComponentBundles.get(0); + if (linkedComponentBundles != null && !linkedComponentBundles.isEmpty()) { + final Bundle firstLinkedComponentBundle = linkedComponentBundles.getFirst(); final BundleCoordinate coordinate = firstLinkedComponentBundle.getBundleDetails().getCoordinate(); final String group = coordinate.getGroup(); final String id = coordinate.getId(); final String version = coordinate.getVersion(); - - if (index != 0) { if (separatorIsElement) { xmlStreamWriter.writeEmptyElement(effectiveSeparator); @@ -1129,7 +816,7 @@ public class HtmlDocumentationWriter implements DocumentationWriter { } } - if (classNames!= null) { + if (classNames != null) { for (final String className : classNames) { if (index != 0) { if (separatorIsElement) { @@ -1141,8 +828,8 @@ public class HtmlDocumentationWriter implements DocumentationWriter { final List<Bundle> linkedComponentBundles = extensionManager.getBundles(className); - if (linkedComponentBundles != null && linkedComponentBundles.size() > 0) { - final Bundle firstBundle = linkedComponentBundles.get(0); + if (linkedComponentBundles != null && !linkedComponentBundles.isEmpty()) { + final Bundle firstBundle = linkedComponentBundles.getFirst(); final BundleCoordinate firstCoordinate = firstBundle.getBundleDetails().getCoordinate(); final String group = firstCoordinate.getGroup(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlProcessorDocumentationWriter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlProcessorDocumentationWriter.java index 78166da3c9..1e7d7cca38 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlProcessorDocumentationWriter.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlProcessorDocumentationWriter.java @@ -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(); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlPythonProcessorDocumentationWriter.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlPythonProcessorDocumentationWriter.java new file mode 100644 index 0000000000..437b556470 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/main/java/org/apache/nifi/documentation/html/HtmlPythonProcessorDocumentationWriter.java @@ -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; + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/DocGeneratorTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/DocGeneratorTest.java index 2aa2c1c090..097bdd13c6 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/DocGeneratorTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/DocGeneratorTest.java @@ -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); diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/HtmlPythonProcessorDocumentationWriterTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/HtmlPythonProcessorDocumentationWriterTest.java new file mode 100644 index 0000000000..0e5fc964d7 --- /dev/null +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/HtmlPythonProcessorDocumentationWriterTest.java @@ -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); + } + +} diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/ProcessorDocumentationWriterTest.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/ProcessorDocumentationWriterTest.java index f89a2a7ac0..bd5135fa29 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/ProcessorDocumentationWriterTest.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-documentation/src/test/java/org/apache/nifi/documentation/html/ProcessorDocumentationWriterTest.java @@ -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 diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java index fe83b16d2b..9d8a26d830 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-jetty/src/main/java/org/apache/nifi/web/server/JettyServer.java @@ -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 diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-docs/src/main/java/org/apache/nifi/web/docs/DocumentationController.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-docs/src/main/java/org/apache/nifi/web/docs/DocumentationController.java index dc8f9b1c69..ff08393059 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-docs/src/main/java/org/apache/nifi/web/docs/DocumentationController.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-docs/src/main/java/org/apache/nifi/web/docs/DocumentationController.java @@ -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);