/* * 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.annotation.behavior.DynamicProperties; import org.apache.nifi.annotation.behavior.DynamicProperty; import org.apache.nifi.annotation.behavior.Restricted; import org.apache.nifi.annotation.behavior.Stateful; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.DeprecationNotice; import org.apache.nifi.annotation.documentation.SeeAlso; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.bundle.Bundle; import org.apache.nifi.bundle.BundleCoordinate; import org.apache.nifi.components.AllowableValue; import org.apache.nifi.components.ConfigurableComponent; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.controller.ControllerService; import org.apache.nifi.documentation.DocumentationWriter; 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.List; import java.util.Set; /** * Generates HTML documentation for a ConfigurableComponent. This class is used * to generate documentation for ControllerService and ReportingTask because * they have no additional information. * * */ public class HtmlDocumentationWriter implements DocumentationWriter { 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"; @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", "/nifi-docs/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); 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(); } /** * 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 { final Stateful stateful = configurableComponent.getClass().getAnnotation(Stateful.class); 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"); xmlStreamWriter.writeEndElement(); xmlStreamWriter.writeStartElement("tr"); writeSimpleElement(xmlStreamWriter, "td", join(stateful.scopes(), ", ")); writeSimpleElement(xmlStreamWriter, "td", stateful.description()); xmlStreamWriter.writeEndElement(); xmlStreamWriter.writeEndElement(); } else { xmlStreamWriter.writeCharacters("This component does not store state."); } } /** * 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 { final Restricted restricted = configurableComponent.getClass().getAnnotation(Restricted.class); writeSimpleElement(xmlStreamWriter, "h3", "Restricted: "); if(restricted != null) { xmlStreamWriter.writeCharacters(restricted.value()); } else { xmlStreamWriter.writeCharacters("This component is not restricted."); } } /** * 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 { final DeprecationNotice deprecationNotice = configurableComponent.getClass().getAnnotation(DeprecationNotice.class); if (deprecationNotice != null) { xmlStreamWriter.writeStartElement("h2"); xmlStreamWriter.writeCharacters("Deprecation notice: "); xmlStreamWriter.writeEndElement(); 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.writeEndElement(); xmlStreamWriter.writeStartElement("p"); xmlStreamWriter.writeCharacters("Please consider using one the following alternatives: "); Class<? extends ConfigurableComponent>[] componentNames = deprecationNotice.alternatives(); String[] classNames = deprecationNotice.classNames(); if (componentNames.length > 0 || classNames.length > 0) { // Write alternatives iterateAndLinkComponents(xmlStreamWriter, componentNames, classNames, ","); } else { xmlStreamWriter.writeCharacters("No alternative components suggested."); } xmlStreamWriter.writeEndElement(); } } /** * 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 { final SeeAlso seeAlso = configurableComponent.getClass().getAnnotation(SeeAlso.class); if (seeAlso != null) { writeSimpleElement(xmlStreamWriter, "h3", "See Also:"); xmlStreamWriter.writeStartElement("p"); Class<? extends ConfigurableComponent>[] componentNames = seeAlso.value(); String[] classNames = seeAlso.classNames(); if (componentNames.length > 0 || classNames.length > 0) { // Write alternatives iterateAndLinkComponents(xmlStreamWriter, componentNames, classNames, ", "); } else { xmlStreamWriter.writeCharacters("No tags provided."); } 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 { final Tags tags = configurableComponent.getClass().getAnnotation(Tags.class); xmlStreamWriter.writeStartElement("h3"); xmlStreamWriter.writeCharacters("Tags: "); xmlStreamWriter.writeEndElement(); xmlStreamWriter.writeStartElement("p"); if (tags != null) { final String tagString = join(tags.value(), ", "); xmlStreamWriter.writeCharacters(tagString); } else { xmlStreamWriter.writeCharacters("No tags provided."); } xmlStreamWriter.writeEndElement(); } static String join(final Object[] objects, final String delimiter) { final StringBuilder sb = new StringBuilder(); for (int i = 0; i < objects.length; i++) { sb.append(objects[i].toString()); if (i < objects.length - 1) { sb.append(delimiter); } } return sb.toString(); } /** * 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); final String description; if (capabilityDescription != null) { description = capabilityDescription.value(); } else { description = "No description provided."; } return description; } /** * 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 { final List<PropertyDescriptor> properties = configurableComponent.getPropertyDescriptors(); writeSimpleElement(xmlStreamWriter, "h3", "Properties: "); if (properties.size() > 0) { final boolean containsExpressionLanguage = containsExpressionLanguage(configurableComponent); final boolean containsSensitiveProperties = containsSensitiveProperties(configurableComponent); 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) { if (!containsSensitiveProperties) { xmlStreamWriter.writeCharacters(", and "); } else { xmlStreamWriter.writeCharacters(", "); } xmlStreamWriter.writeCharacters("whether a property supports the "); writeLink(xmlStreamWriter, "NiFi Expression Language", "/nifi-docs/html/expression-language-guide.html"); } if (containsSensitiveProperties) { xmlStreamWriter.writeCharacters(", and whether a property is considered " + "\"sensitive\", meaning that its value will be encrypted. Before entering a " + "value in a sensitive property, ensure that the "); writeSimpleElement(xmlStreamWriter, "strong", "nifi.properties"); xmlStreamWriter.writeCharacters(" file has " + "an entry for the property "); writeSimpleElement(xmlStreamWriter, "strong", "nifi.sensitive.props.key"); } xmlStreamWriter.writeCharacters("."); xmlStreamWriter.writeEndElement(); xmlStreamWriter.writeStartElement("table"); xmlStreamWriter.writeAttribute("id", "properties"); // write the header row xmlStreamWriter.writeStartElement("tr"); writeSimpleElement(xmlStreamWriter, "th", "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"); if (property.isRequired()) { writeSimpleElement(xmlStreamWriter, "strong", property.getDisplayName()); } else { xmlStreamWriter.writeCharacters(property.getDisplayName()); } xmlStreamWriter.writeEndElement(); writeSimpleElement(xmlStreamWriter, "td", property.getDefaultValue(), false, "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.writeCharacters(property.getDescription()); } else { xmlStreamWriter.writeCharacters("No Description Provided."); } if (property.isSensitive()) { xmlStreamWriter.writeEmptyElement("br"); writeSimpleElement(xmlStreamWriter, "strong", "Sensitive Property: true"); } if (property.isExpressionLanguageSupported()) { xmlStreamWriter.writeEmptyElement("br"); writeSimpleElement(xmlStreamWriter, "strong", "Supports Expression Language: true"); } xmlStreamWriter.writeEndElement(); xmlStreamWriter.writeEndElement(); } // TODO support dynamic properties... xmlStreamWriter.writeEndElement(); } else { writeSimpleElement(xmlStreamWriter, "p", "This component has no required or optional properties."); } } /** * Indicates whether or not the component contains at least one sensitive property. * * @param component the component to interogate * @return whether or not the component contains at least one sensitive property. */ private boolean containsSensitiveProperties(final ConfigurableComponent component) { for (PropertyDescriptor descriptor : component.getPropertyDescriptors()) { if (descriptor.isSensitive()) { return true; } } return false; } /** * Indicates whether or not the component contains at least one property that supports Expression Language. * * @param component the component to interogate * @return whether or not the component contains at least one sensitive property. */ private boolean containsExpressionLanguage(final ConfigurableComponent component) { for (PropertyDescriptor descriptor : component.getPropertyDescriptors()) { if (descriptor.isExpressionLanguageSupported()) { return true; } } return false; } private void writeDynamicProperties(final ConfigurableComponent configurableComponent, final XMLStreamWriter xmlStreamWriter) throws XMLStreamException { final List<DynamicProperty> dynamicProperties = getDynamicProperties(configurableComponent); if (dynamicProperties != null && dynamicProperties.size() > 0) { writeSimpleElement(xmlStreamWriter, "h3", "Dynamic Properties: "); 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(), false, "name"); writeSimpleElement(xmlStreamWriter, "td", dynamicProperty.value(), false, "value"); xmlStreamWriter.writeStartElement("td"); xmlStreamWriter.writeCharacters(dynamicProperty.description()); if (dynamicProperty.supportsExpressionLanguage()) { xmlStreamWriter.writeEmptyElement("br"); writeSimpleElement(xmlStreamWriter, "strong", "Supports Expression Language: true"); } xmlStreamWriter.writeEndElement(); xmlStreamWriter.writeEndElement(); } xmlStreamWriter.writeEndElement(); xmlStreamWriter.writeEndElement(); } } private List<DynamicProperty> getDynamicProperties(ConfigurableComponent configurableComponent) { final List<DynamicProperty> dynamicProperties = new ArrayList<>(); final DynamicProperties dynProps = configurableComponent.getClass().getAnnotation(DynamicProperties.class); if (dynProps != null) { for (final DynamicProperty dynProp : dynProps.value()) { dynamicProperties.add(dynProp); } } final DynamicProperty dynProp = configurableComponent.getClass().getAnnotation(DynamicProperty.class); if (dynProp != null) { dynamicProperties.add(dynProp); } return dynamicProperties; } private void writeValidValueDescription(XMLStreamWriter xmlStreamWriter, String description) throws XMLStreamException { xmlStreamWriter.writeCharacters(" "); xmlStreamWriter.writeStartElement("img"); xmlStreamWriter.writeAttribute("src", "/nifi-docs/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. * * @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 */ protected void writeValidValues(XMLStreamWriter xmlStreamWriter, PropertyDescriptor property) throws XMLStreamException { if (property.getAllowableValues() != null && property.getAllowableValues().size() > 0) { xmlStreamWriter.writeStartElement("ul"); for (AllowableValue value : property.getAllowableValues()) { xmlStreamWriter.writeStartElement("li"); xmlStreamWriter.writeCharacters(value.getDisplayName()); if (value.getDescription() != null) { writeValidValueDescription(xmlStreamWriter, value.getDescription()); } xmlStreamWriter.writeEndElement(); } xmlStreamWriter.writeEndElement(); } else if (property.getControllerServiceDefinition() != null) { Class<? extends ControllerService> controllerServiceClass = property.getControllerServiceDefinition(); writeSimpleElement(xmlStreamWriter, "strong", "Controller Service API: "); xmlStreamWriter.writeEmptyElement("br"); xmlStreamWriter.writeCharacters(controllerServiceClass.getSimpleName()); final List<Class<? extends ControllerService>> implementationList = lookupControllerServiceImpls(controllerServiceClass); // Convert it into an array before proceeding Class<? extends ControllerService>[] implementations = implementationList.stream().toArray(Class[]::new); xmlStreamWriter.writeEmptyElement("br"); if (implementations.length > 0) { final String title = implementations.length > 1 ? "Implementations: " : "Implementation:"; writeSimpleElement(xmlStreamWriter, "strong", title); iterateAndLinkComponents(xmlStreamWriter, implementations, null, "<br>"); } else { xmlStreamWriter.writeCharacters("No implementations found."); } } } /** * Writes a begin element, then text, then end element for the element of a * users choosing. Example: <p>text</p> * * @param writer the stream writer to use * @param elementName the name of the element * @param characters the characters to insert into the element * @param strong whether the characters should be strong or not. * @throws XMLStreamException thrown if there was a problem writing to the * stream. */ protected final static void writeSimpleElement(final XMLStreamWriter writer, final String elementName, final String characters, boolean strong) throws XMLStreamException { writeSimpleElement(writer, elementName, characters, strong, null); } /** * Writes a begin element, an id attribute(if specified), then text, then * end element for element of the users choosing. Example: <p * id="p-id">text</p> * * @param writer the stream writer to use * @param elementName the name of the element * @param characters the text of the element * @param strong whether to bold the text of the element or not * @param id the id of the element. specifying null will cause no element to * be written. * @throws XMLStreamException xse */ protected final static void writeSimpleElement(final XMLStreamWriter writer, final String elementName, final String characters, boolean strong, String id) throws XMLStreamException { writer.writeStartElement(elementName); if (id != null) { writer.writeAttribute("id", id); } if (strong) { writer.writeStartElement("strong"); } writer.writeCharacters(characters); if (strong) { writer.writeEndElement(); } writer.writeEndElement(); } /** * Writes a begin element, then text, then end element for the element of a * users choosing. Example: <p>text</p> * * @param writer the stream writer to use * @param elementName the name of the element * @param characters the characters to insert into the element * @throws XMLStreamException thrown if there was a problem writing to the * stream */ protected final static void writeSimpleElement(final XMLStreamWriter writer, final String elementName, final String characters) throws XMLStreamException { writeSimpleElement(writer, elementName, characters, false); } /** * 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(); } /** * Uses the {@link ExtensionManager} to discover any {@link ControllerService} implementations that implement a specific * ControllerService API. * * @param parent the controller service API * @return a list of controller services that implement the controller service API */ private List<Class<? extends ControllerService>> lookupControllerServiceImpls( final Class<? extends ControllerService> parent) { final List<Class<? extends ControllerService>> implementations = new ArrayList<>(); // first get all ControllerService implementations final Set<Class> controllerServices = ExtensionManager.getExtensions(ControllerService.class); // then iterate over all controller services looking for any that is a child of the parent // ControllerService API that was passed in as a parameter for (final Class<? extends ControllerService> controllerServiceClass : controllerServices) { if (parent.isAssignableFrom(controllerServiceClass)) { implementations.add(controllerServiceClass); } } return implementations; } /** * 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 * @throws XMLStreamException thrown if there is a problem writing the XML */ protected void iterateAndLinkComponents(final XMLStreamWriter xmlStreamWriter, final Class<? extends ConfigurableComponent>[] linkedComponents, String[] classNames, String separator) throws XMLStreamException { // Treat the the possible separators boolean separatorIsElement; if (separator.startsWith("<") && separator.endsWith(">")) { separatorIsElement = true; } else { separatorIsElement = false; } // Whatever the result, strip the possible < and > characters separator = separator.replaceAll("\\<([^>]*)>","$1"); int index = 0; 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); 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(separator); } else { xmlStreamWriter.writeCharacters(separator); } } writeLink(xmlStreamWriter, linkedComponent.getSimpleName(), "/nifi-docs/components/" + group + "/" + id + "/" + version + "/" + linkedComponent.getCanonicalName() + "/index.html"); ++index; } else { LOGGER.warn("Could not link to {} because no bundles were found", new Object[] {linkedComponentName}); } } if (classNames!= null) { for (final String className : classNames) { if (index != 0) { if (separatorIsElement) { xmlStreamWriter.writeEmptyElement(separator); } else { xmlStreamWriter.writeCharacters(separator); } } final List<Bundle> linkedComponentBundles = ExtensionManager.getBundles(className); if (linkedComponentBundles != null && linkedComponentBundles.size() > 0) { final Bundle firstBundle = linkedComponentBundles.get(0); final BundleCoordinate firstCoordinate = firstBundle.getBundleDetails().getCoordinate(); final String group = firstCoordinate.getGroup(); final String id = firstCoordinate.getId(); final String version = firstCoordinate.getVersion(); final String link = "/nifi-docs/components/" + group + "/" + id + "/" + version + "/" + className + "/index.html"; final int indexOfLastPeriod = className.lastIndexOf(".") + 1; writeLink(xmlStreamWriter, className.substring(indexOfLastPeriod), link); ++index; } else { LOGGER.warn("Could not link to {} because no bundles were found", new Object[] {className}); } } } } }