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: &lt;p
+     * id="p-id"&gt;text&lt;/p&gt;
+     *
+     * @param writer      the stream writer to use
+     * @param elementName the name of the element
+     * @param characters  the text of the element
+     * @param id          the id of the element. specifying null will cause no 
element to be written.
+     * @throws XMLStreamException xse
+     */
+    protected static void writeSimpleElement(final XMLStreamWriter writer, 
final String elementName, final String characters, String id) throws 
XMLStreamException {
+        writer.writeStartElement(elementName);
+
+        if (characters != null) {
+            if (id != null) {
+                writer.writeAttribute(ID, id);
+            }
+            writer.writeCharacters(characters);
+        }
+
+        writer.writeEndElement();
+    }
+
+    /**
+     * Writes a begin element, then text, then end element for the element of 
a users choosing. Example: &lt;p&gt;text&lt;/p&gt;
+     *
+     * @param writer      the stream writer to use
+     * @param elementName the name of the element
+     * @param characters  the characters to insert into the element
+     */
+    protected static void writeSimpleElement(final XMLStreamWriter writer, 
final String elementName, final String characters) throws XMLStreamException {
+        writeSimpleElement(writer, elementName, characters, null);
+    }
+
+    /**
+     * A helper method to write a link
+     *
+     * @param xmlStreamWriter the stream to write to
+     * @param text            the text of the link
+     * @param location        the location of the link
+     * @throws XMLStreamException thrown if there was a problem writing to the
+     *                            stream
+     */
+    protected void writeLink(final XMLStreamWriter xmlStreamWriter, final 
String text, final String location) throws XMLStreamException {
+        xmlStreamWriter.writeStartElement("a");
+        xmlStreamWriter.writeAttribute("href", location);
+        xmlStreamWriter.writeCharacters(text);
+        xmlStreamWriter.writeEndElement();
+    }
+
+    void writeUseCaseConfiguration(final String configuration, final 
XMLStreamWriter xmlStreamWriter) throws XMLStreamException {
+        if (StringUtils.isEmpty(configuration)) {
+            return;
+        }
+
+        writeSimpleElement(xmlStreamWriter, H4, "Configuration:");
+
+        final String[] splits = configuration.split("\\n");
+        for (final String split : splits) {
+            xmlStreamWriter.writeStartElement(P);
+
+            final Matcher matcher = Pattern.compile("`(.*?)`").matcher(split);
+            int startIndex = 0;
+            while (matcher.find()) {
+                final int start = matcher.start();
+                if (start > 0) {
+                    
xmlStreamWriter.writeCharacters(split.substring(startIndex, start));
+                }
+
+                writeSimpleElement(xmlStreamWriter, "code", matcher.group(1));
+
+                startIndex = matcher.end();
+            }
+
+            if (split.length() > startIndex) {
+                if (startIndex == 0) {
+                    xmlStreamWriter.writeCharacters(split);
+                } else {
+                    
xmlStreamWriter.writeCharacters(split.substring(startIndex));
+                }
+            }
+
+            xmlStreamWriter.writeEndElement();
+        }
+    }
+
+    /**
+     * Gets the class name of the component.
+     *
+     * @param component the component to describe
+     * @return the class name of the component
+     */
+    abstract String getTitle(final T component);
+
+    /**
+     * Writes a warning about the deprecation of a component.
+     *
+     * @param component       the component to describe
+     * @param xmlStreamWriter the stream writer
+     * @throws XMLStreamException thrown if there was a problem writing to the 
XML stream
+     */
+    abstract void writeDeprecationWarning(final T component, final 
XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
+
+    /**
+     * Gets a description of the component using the CapabilityDescription 
annotation.
+     *
+     * @param component the component to describe
+     * @return a description of the component
+     */
+    abstract String getDescription(final T component);
+
+    /**
+     * Writes the tag list of the component.
+     *
+     * @param component       the component to describe
+     * @param xmlStreamWriter the stream writer
+     * @throws XMLStreamException thrown if there was a problem writing to the 
XML stream
+     */
+    abstract void writeTags(final T component, final XMLStreamWriter 
xmlStreamWriter) throws XMLStreamException;
+
+    /**
+     * Writes the PropertyDescriptors out as a table.
+     *
+     * @param component       the component to describe
+     * @param xmlStreamWriter the stream writer
+     * @throws XMLStreamException thrown if there was a problem writing to the 
XML Stream
+     */
+    abstract void writeProperties(final T component, final XMLStreamWriter 
xmlStreamWriter) throws XMLStreamException;
+
+    abstract void writeDynamicProperties(final T component, final 
XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
+
+    /**
+     * Write the description of the Stateful annotation if provided in this 
component.
+     *
+     * @param component       the component to describe
+     * @param xmlStreamWriter the stream writer to use
+     * @throws XMLStreamException thrown if there was a problem writing the XML
+     */
+    abstract void writeStatefulInfo(final T component, XMLStreamWriter 
xmlStreamWriter) throws XMLStreamException;
+
+    /**
+     * Write the description of the Restricted annotation if provided in this 
component.
+     *
+     * @param component       the component to describe
+     * @param xmlStreamWriter the stream writer to use
+     * @throws XMLStreamException thrown if there was a problem writing the XML
+     */
+    abstract void writeRestrictedInfo(final T component, XMLStreamWriter 
xmlStreamWriter) throws XMLStreamException;
+
+    /**
+     * Add in the documentation information regarding the component whether it 
accepts an incoming relationship or not.
+     *
+     * @param component       the component to describe
+     * @param xmlStreamWriter the stream writer to use
+     * @throws XMLStreamException thrown if there was a problem writing the XML
+     */
+    abstract void writeInputRequirementInfo(final T component, XMLStreamWriter 
xmlStreamWriter) throws XMLStreamException;
+
+    abstract void writeUseCases(final T component, final XMLStreamWriter 
xmlStreamWriter) throws XMLStreamException;
+
+    abstract void writeMultiComponentUseCases(final T component, final 
XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
+
+    /**
+     * Writes the list of components that may be linked from this component.
+     *
+     * @param component       the component to describe
+     * @param xmlStreamWriter the stream writer to use
+     * @throws XMLStreamException thrown if there was a problem writing the XML
+     */
+    abstract void writeSeeAlso(final T component, XMLStreamWriter 
xmlStreamWriter) throws XMLStreamException;
+
+    /**
+     * Writes all the system resource considerations for this component
+     *
+     * @param component       the component to describe
+     * @param xmlStreamWriter the xml stream writer to use
+     * @throws XMLStreamException thrown if there was a problem writing the XML
+     */
+    abstract void writeSystemResourceConsiderationInfo(final T component, 
XMLStreamWriter xmlStreamWriter) throws XMLStreamException;
+
+}
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: &lt;p
-     * id="p-id"&gt;text&lt;/p&gt;
-     *
-     * @param writer the stream writer to use
-     * @param elementName the name of the element
-     * @param characters the text of the element
-     * @param id the id of the element. specifying null will cause no element 
to
-     * be written.
-     * @throws XMLStreamException xse
-     */
-    protected static void writeSimpleElement(final XMLStreamWriter writer, 
final String elementName,
-            final String characters, String id) throws XMLStreamException {
-        writer.writeStartElement(elementName);
-
-        if (characters != null) {
-            if (id != null) {
-                writer.writeAttribute("id", id);
-            }
-            writer.writeCharacters(characters);
-        }
-
-        writer.writeEndElement();
-    }
-
-    /**
-     * Writes a begin element, then text, then end element for the element of a
-     * users choosing. Example: &lt;p&gt;text&lt;/p&gt;
-     *
-     * @param writer the stream writer to use
-     * @param elementName the name of the element
-     * @param characters the characters to insert into the element
-     */
-    protected static void writeSimpleElement(final XMLStreamWriter writer, 
final String elementName,
-            final String characters) throws XMLStreamException {
-        writeSimpleElement(writer, elementName, characters, null);
-    }
-
-    /**
-     * A helper method to write a link
-     *
-     * @param xmlStreamWriter the stream to write to
-     * @param text the text of the link
-     * @param location the location of the link
-     * @throws XMLStreamException thrown if there was a problem writing to the
-     * stream
-     */
-    protected void writeLink(final XMLStreamWriter xmlStreamWriter, final 
String text, final String location)
-            throws XMLStreamException {
-        xmlStreamWriter.writeStartElement("a");
-        xmlStreamWriter.writeAttribute("href", location);
-        xmlStreamWriter.writeCharacters(text);
-        xmlStreamWriter.writeEndElement();
-    }
-
-    /**
-     * 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);

Reply via email to