blob: e0b7a547109f1a72185d5bfc7b520fdc686b2415 [file] [log] [blame]
/*
* 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.xml;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import org.apache.nifi.annotation.behavior.DynamicProperty;
import org.apache.nifi.annotation.behavior.DynamicRelationship;
import org.apache.nifi.annotation.behavior.InputRequirement;
import org.apache.nifi.annotation.behavior.PrimaryNodeOnly;
import org.apache.nifi.annotation.behavior.ReadsAttribute;
import org.apache.nifi.annotation.behavior.Restricted;
import org.apache.nifi.annotation.behavior.Restriction;
import org.apache.nifi.annotation.behavior.SideEffectFree;
import org.apache.nifi.annotation.behavior.Stateful;
import org.apache.nifi.annotation.behavior.SupportsBatching;
import org.apache.nifi.annotation.behavior.SupportsSensitiveDynamicProperties;
import org.apache.nifi.annotation.behavior.SystemResourceConsideration;
import org.apache.nifi.annotation.behavior.TriggerSerially;
import org.apache.nifi.annotation.behavior.TriggerWhenAnyDestinationAvailable;
import org.apache.nifi.annotation.behavior.TriggerWhenEmpty;
import org.apache.nifi.annotation.behavior.WritesAttribute;
import org.apache.nifi.annotation.configuration.DefaultSchedule;
import org.apache.nifi.annotation.configuration.DefaultSettings;
import org.apache.nifi.annotation.documentation.DeprecationNotice;
import org.apache.nifi.annotation.documentation.MultiProcessorUseCase;
import org.apache.nifi.annotation.documentation.ProcessorConfiguration;
import org.apache.nifi.annotation.documentation.SeeAlso;
import org.apache.nifi.annotation.documentation.UseCase;
import org.apache.nifi.components.AllowableValue;
import org.apache.nifi.components.ConfigurableComponent;
import org.apache.nifi.components.PropertyDependency;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.RequiredPermission;
import org.apache.nifi.components.resource.ResourceDefinition;
import org.apache.nifi.components.resource.ResourceType;
import org.apache.nifi.documentation.AbstractDocumentationWriter;
import org.apache.nifi.documentation.ExtensionType;
import org.apache.nifi.documentation.ServiceAPI;
import org.apache.nifi.processor.Relationship;
/**
* XML-based implementation of DocumentationWriter
*
* Please note that while this class lives within the nifi-api, it is provided primarily as a means for documentation components within
* the NiFi NAR Maven Plugin. Its home is the nifi-api, however, because the API is needed in order to extract the relevant information and
* the NAR Maven Plugin cannot have a direct dependency on nifi-api (doing so would cause a circular dependency). By having this homed within
* the nifi-api, the Maven plugin is able to discover the class dynamically and invoke the one or two methods necessary to create the documentation.
*
* This is a new capability in 1.9.0 in preparation for the Extension Registry and therefore, you should
* <b>NOTE WELL:</b> At this time, while this class is part of nifi-api, it is still evolving and may change in a non-backward-compatible manner or even be
* removed from one incremental release to the next. Use at your own risk!
*/
public class XmlDocumentationWriter extends AbstractDocumentationWriter {
private final XMLStreamWriter writer;
public XmlDocumentationWriter(final OutputStream out) throws XMLStreamException {
this.writer = XMLOutputFactory.newInstance().createXMLStreamWriter(out, "UTF-8");
}
public XmlDocumentationWriter(final XMLStreamWriter writer) {
this.writer = writer;
}
@Override
protected void writeHeader(final ConfigurableComponent component) throws IOException {
writeStartElement("extension");
}
@Override
protected void writeExtensionName(final String extensionName) throws IOException {
writeTextElement("name", extensionName);
}
@Override
protected void writeExtensionType(final ExtensionType extensionType) throws IOException {
writeTextElement("type", extensionType.name());
}
@Override
protected void writeDeprecationNotice(final DeprecationNotice deprecationNotice) throws IOException {
if (deprecationNotice == null) {
return;
}
final Class[] classes = deprecationNotice.alternatives();
final String[] classNames = deprecationNotice.classNames();
final Set<String> alternatives = new LinkedHashSet<>();
if (classes != null) {
for (final Class alternativeClass : classes) {
alternatives.add(alternativeClass.getName());
}
}
if (classNames != null) {
Collections.addAll(alternatives, classNames);
}
writeDeprecationNotice(deprecationNotice.reason(), alternatives);
}
private void writeDeprecationNotice(final String reason, final Set<String> alternatives) throws IOException {
writeStartElement("deprecationNotice");
writeTextElement("reason", reason);
writeTextArray("alternatives", "alternative", alternatives);
writeEndElement();
}
@Override
protected void writeDescription(final String description) throws IOException {
if (description == null) {
return;
}
writeTextElement("description", description);
}
@Override
protected void writeTags(final List<String> tags) throws IOException {
if (tags == null) {
return;
}
writeTextArray("tags", "tag", tags);
}
@Override
protected void writeProperties(final List<PropertyDescriptor> properties, Map<String, ServiceAPI> propertyServices) throws IOException {
if (properties == null || properties.isEmpty()) {
return;
}
writeStartElement("properties");
for (final PropertyDescriptor property : properties) {
writeProperty(property, propertyServices);
}
writeEndElement();
}
private void writeProperty(final PropertyDescriptor property, Map<String, ServiceAPI> propertyServices) throws IOException {
writeStartElement("property");
writeTextElement("name", property.getName());
writeTextElement("displayName", property.getDisplayName());
writeTextElement("description", property.getDescription());
if (property.getDefaultValue() != null) {
writeTextElement("defaultValue", property.getDefaultValue());
}
if (property.getControllerServiceDefinition() != null) {
writeStartElement("controllerServiceDefinition");
final ServiceAPI serviceAPI = propertyServices.get(property.getName());
if (serviceAPI != null) {
writeTextElement("className", serviceAPI.getClassName());
writeTextElement("groupId", serviceAPI.getGroupId());
writeTextElement("artifactId", serviceAPI.getArtifactId());
writeTextElement("version", serviceAPI.getVersion());
} else {
writeTextElement("className", property.getControllerServiceDefinition().getName());
writeTextElement("groupId", "unknown");
writeTextElement("artifactId", "unknown");
writeTextElement("version", "unknown");
}
writeEndElement();
}
if (property.getAllowableValues() != null && !property.getAllowableValues().isEmpty()) {
writeArray("allowableValues", property.getAllowableValues(), this::writeAllowableValue);
}
writeBooleanElement("required", property.isRequired());
writeBooleanElement("sensitive", property.isSensitive());
writeBooleanElement("expressionLanguageSupported", property.isExpressionLanguageSupported());
if (property.getExpressionLanguageScope() != null) {
writeTextElement("expressionLanguageScope", property.getExpressionLanguageScope().name());
}
writeBooleanElement("dynamicallyModifiesClasspath", property.isDynamicClasspathModifier());
writeBooleanElement("dynamic", property.isDynamic());
writeResourceDefinition(property.getResourceDefinition());
writeDependencies(property);
writeEndElement();
}
private void writeResourceDefinition(final ResourceDefinition resourceDefinition) throws IOException {
if (resourceDefinition == null) {
return;
}
writeStartElement("resourceDefinition");
writeTextElement("cardinality", resourceDefinition.getCardinality().name());
writeArray("resourceTypes", resourceDefinition.getResourceTypes(), this::writeResourceType);
writeEndElement();
}
private void writeResourceType(final ResourceType resourceType) throws IOException {
writeTextElement("resourceType", resourceType.name());
}
private void writeAllowableValue(final AllowableValue allowableValue) throws IOException {
writeStartElement("allowableValue");
writeTextElement("displayName", allowableValue.getDisplayName());
writeTextElement("value", allowableValue.getValue());
writeTextElement("description", allowableValue.getDescription());
writeEndElement();
}
private void writeDependencies(final PropertyDescriptor propertyDescriptor) throws IOException {
final Set<PropertyDependency> dependencies = propertyDescriptor.getDependencies();
if (dependencies == null || dependencies.isEmpty()) {
return;
}
writeStartElement("dependencies");
for (final PropertyDependency dependency : dependencies) {
writeStartElement("dependency");
writeTextElement("propertyName", dependency.getPropertyName());
writeTextElement("propertyDisplayName", dependency.getPropertyDisplayName());
final Set<String> dependentValues = dependency.getDependentValues();
if (dependentValues != null) {
writeStartElement("dependentValues");
for (final String dependentValue : dependentValues) {
writeTextElement("dependentValue", dependentValue);
}
writeEndElement();
}
writeEndElement();
}
writeEndElement();
}
@Override
protected void writeDynamicProperties(final List<DynamicProperty> dynamicProperties) throws IOException {
if (dynamicProperties == null || dynamicProperties.isEmpty()) {
return;
}
writeArray("dynamicProperties", dynamicProperties, this::writeDynamicProperty);
}
private void writeDynamicProperty(final DynamicProperty property) throws IOException {
writeStartElement("dynamicProperty");
writeTextElement("name", property.name());
writeTextElement("value", property.value());
writeTextElement("description", property.description());
writeTextElement("expressionLanguageScope", property.expressionLanguageScope() == null ? null : property.expressionLanguageScope().name());
writeEndElement();
}
@Override
protected void writeStatefulInfo(final Stateful stateful) throws IOException {
if (stateful == null) {
return;
}
writeStartElement("stateful");
writeTextElement("description", stateful.description());
writeArray("scopes", Arrays.asList(stateful.scopes()), scope -> writeTextElement("scope", scope.name()));
writeEndElement();
}
@Override
protected void writeRestrictedInfo(final Restricted restricted) throws IOException {
if (restricted == null) {
return;
}
writeStartElement("restricted");
if (restricted.value() != null && !restricted.value().isEmpty()) {
writeTextElement("generalRestrictionExplanation", restricted.value());
}
final Restriction[] restrictions = restricted.restrictions();
if (restrictions != null) {
writeArray("restrictions", Arrays.asList(restrictions), this::writeRestriction);
}
writeEndElement();
}
private void writeRestriction(final Restriction restriction) throws IOException {
writeStartElement("restriction");
final RequiredPermission permission = restriction.requiredPermission();
final String label = permission == null ? null : permission.getPermissionLabel();
writeTextElement("requiredPermission", label);
writeTextElement("explanation", restriction.explanation());
writeEndElement();
}
@Override
protected void writeInputRequirementInfo(final InputRequirement.Requirement requirement) throws IOException {
if (requirement == null) {
return;
}
writeTextElement("inputRequirement", requirement.name());
}
@Override
protected void writeSystemResourceConsiderationInfo(final List<SystemResourceConsideration> considerations) throws IOException {
if (considerations == null || considerations.isEmpty()) {
return;
}
writeArray("systemResourceConsiderations", considerations, this::writeSystemResourceConsideration);
}
private void writeSystemResourceConsideration(final SystemResourceConsideration consideration) throws IOException {
writeStartElement("systemResourceConsideration");
writeTextElement("resource", consideration.resource() == null ? null : consideration.resource().name());
writeTextElement("description", consideration.description());
writeEndElement();
}
@Override
protected void writeSeeAlso(final SeeAlso seeAlso) throws IOException {
if (seeAlso == null) {
return;
}
final Class[] classes = seeAlso.value();
final String[] classNames = seeAlso.classNames();
final Set<String> toSee = new LinkedHashSet<>();
if (classes != null) {
for (final Class classToSee : classes) {
toSee.add(classToSee.getName());
}
}
if (classNames != null) {
Collections.addAll(toSee, classNames);
}
writeTextArray("seeAlso", "see", toSee);
}
@Override
protected void writeUseCases(final List<UseCase> useCases) throws IOException {
if (useCases.isEmpty()) {
return;
}
writeArray("useCases", useCases, this::writeUseCase);
}
private void writeUseCase(final UseCase useCase) throws IOException {
writeStartElement("useCase");
writeTextElement("description", useCase.description());
writeTextElement("notes", useCase.notes());
writeTextArray("keywords", "keyword", Arrays.asList(useCase.keywords()));
writeTextElement("inputRequirement", useCase.inputRequirement().name());
writeTextElement("configuration", useCase.configuration());
writeEndElement();
}
protected void writeMultiProcessorUseCases(final List<MultiProcessorUseCase> multiProcessorUseCases) throws IOException {
if (multiProcessorUseCases.isEmpty()) {
return;
}
writeArray("multiProcessorUseCases", multiProcessorUseCases, this::writeMultiProcessorUseCase);
}
private void writeMultiProcessorUseCase(final MultiProcessorUseCase useCase) throws IOException {
writeStartElement("multiProcessorUseCase");
writeTextElement("description", useCase.description());
writeTextElement("notes", useCase.notes());
writeTextArray("keywords", "keyword", Arrays.asList(useCase.keywords()));
writeArray("processorConfigurations", Arrays.asList(useCase.configurations()), this::writeUseCaseComponent);
writeEndElement();
}
private void writeUseCaseComponent(final ProcessorConfiguration processorConfig) throws IOException {
writeStartElement("processorConfiguration");
String processorClassName = processorConfig.processorClassName();
if (processorClassName.isEmpty()) {
processorClassName = processorConfig.processorClass().getName();
}
writeTextElement("processorClassName", processorClassName);
writeTextElement("configuration", processorConfig.configuration());
writeEndElement();
}
@Override
protected void writeRelationships(final Set<Relationship> relationships) throws IOException {
if (relationships == null || relationships.isEmpty()) {
return;
}
writeArray("relationships", relationships, rel -> {
writeStartElement("relationship");
writeTextElement("name", rel.getName());
writeTextElement("description", rel.getDescription());
writeBooleanElement("autoTerminated", rel.isAutoTerminated());
writeEndElement();
});
}
@Override
protected void writeDynamicRelationship(final DynamicRelationship dynamicRelationship) throws IOException {
if (dynamicRelationship == null) {
return;
}
writeStartElement("dynamicRelationship");
writeTextElement("name", dynamicRelationship.name());
writeTextElement("description", dynamicRelationship.description());
writeEndElement();
}
@Override
protected void writeReadsAttributes(final List<ReadsAttribute> attributes) throws IOException {
if (attributes == null || attributes.isEmpty()) {
return;
}
writeArray("readsAttributes", attributes, this::writeReadsAttribute);
}
private void writeReadsAttribute(final ReadsAttribute attribute) throws IOException {
writeStartElement("readsAttribute");
writeTextElement("name", attribute.attribute());
writeTextElement("description", attribute.description());
writeEndElement();
}
@Override
protected void writeWritesAttributes(final List<WritesAttribute> attributes) throws IOException {
if (attributes == null) {
return;
}
writeArray("writesAttributes", attributes, this::writeWritesAttribute);
}
private void writeWritesAttribute(final WritesAttribute attribute) throws IOException {
writeStartElement("writesAttribute");
writeTextElement("name", attribute.attribute());
writeTextElement("description", attribute.description());
writeEndElement();
}
@Override
protected void writeTriggerSerially(TriggerSerially triggerSerially) throws IOException {
if (triggerSerially == null) {
return;
}
writeBooleanElement("triggerSerially", true);
}
@Override
protected void writeTriggerWhenEmpty(TriggerWhenEmpty triggerWhenEmpty) throws IOException {
if (triggerWhenEmpty == null) {
return;
}
writeBooleanElement("triggerWhenEmpty", true);
}
@Override
protected void writeTriggerWhenAnyDestinationAvailable(TriggerWhenAnyDestinationAvailable triggerWhenAnyDestinationAvailable) throws IOException {
if (triggerWhenAnyDestinationAvailable == null) {
return;
}
writeBooleanElement("triggerWhenAnyDestinationAvailable", true);
}
@Override
protected void writeSupportsBatching(SupportsBatching supportsBatching) throws IOException {
if (supportsBatching == null) {
return;
}
writeBooleanElement("supportsBatching", true);
}
@Override
protected void writeSupportsSensitiveDynamicProperties(final SupportsSensitiveDynamicProperties supportsSensitiveDynamicProperties) throws IOException {
if (supportsSensitiveDynamicProperties == null) {
return;
}
writeBooleanElement("supportsSensitiveDynamicProperties", true);
}
@Override
protected void writePrimaryNodeOnly(PrimaryNodeOnly primaryNodeOnly) throws IOException {
if (primaryNodeOnly == null) {
return;
}
writeBooleanElement("primaryNodeOnly", true);
}
@Override
protected void writeSideEffectFree(SideEffectFree sideEffectFree) throws IOException {
if (sideEffectFree == null) {
return;
}
writeBooleanElement("sideEffectFree", true);
}
@Override
protected void writeDefaultSchedule(DefaultSchedule defaultSchedule) throws IOException {
if (defaultSchedule == null) {
return;
}
writeStartElement("defaultSchedule");
writeTextElement("strategy", defaultSchedule.strategy().name());
writeTextElement("period", defaultSchedule.period());
writeTextElement("concurrentTasks", String.valueOf(defaultSchedule.concurrentTasks()));
writeEndElement();
}
@Override
protected void writeDefaultSettings(DefaultSettings defaultSettings) throws IOException {
if (defaultSettings == null) {
return;
}
writeStartElement("defaultSettings");
writeTextElement("yieldDuration", defaultSettings.yieldDuration());
writeTextElement("penaltyDuration", defaultSettings.penaltyDuration());
writeTextElement("bulletinLevel", defaultSettings.bulletinLevel().name());
writeEndElement();
}
@Override
protected void writeFooter(final ConfigurableComponent component) throws IOException {
writeEndElement();
}
@Override
protected void writeProvidedServices(final Collection<ServiceAPI> providedServices) throws IOException {
if (providedServices == null || providedServices.isEmpty()) {
return;
}
writeArray("providedServiceAPIs", providedServices, this::writeProvidedService);
}
private void writeProvidedService(final ServiceAPI service) throws IOException {
writeStartElement("providedServiceAPI");
writeTextElement("className", service.getClassName());
writeTextElement("groupId", service.getGroupId());
writeTextElement("artifactId", service.getArtifactId());
writeTextElement("version", service.getVersion());
writeEndElement();
}
private <T> void writeArray(final String tagName, final Collection<T> values, final ElementWriter<T> writer) throws IOException {
writeStartElement(tagName);
if (values != null) {
for (final T value : values) {
writer.write(value);
}
}
writeEndElement();
}
private void writeTextArray(final String outerTagName, final String elementTagName, final Collection<String> values) throws IOException {
writeTextArray(outerTagName, elementTagName, values, String::toString);
}
private <T> void writeTextArray(final String outerTagName, final String elementTagName, final Collection<T> values, final Function<T, String> transform) throws IOException {
writeStartElement(outerTagName);
if (values != null) {
for (final T value : values) {
writeStartElement(elementTagName);
if (value != null) {
writeText(transform.apply(value));
}
writeEndElement();
}
}
writeEndElement();
}
private void writeText(final String text) throws IOException {
if (text == null) {
return;
}
try {
writer.writeCharacters(text);
} catch (XMLStreamException e) {
throw new IOException(e);
}
}
private void writeStartElement(final String elementName) throws IOException {
try {
writer.writeStartElement(elementName);
} catch (final XMLStreamException e) {
throw new IOException(e);
}
}
private void writeEndElement() throws IOException {
try {
writer.writeEndElement();
} catch (final XMLStreamException e) {
throw new IOException(e);
}
}
private void writeTextElement(final String name, final String text) throws IOException {
writeStartElement(name);
writeText(text);
writeEndElement();
}
private void writeBooleanElement(final String name, final boolean value) throws IOException {
writeTextElement(name, String.valueOf(value));
}
private interface ElementWriter<T> {
void write(T value) throws IOException;
}
}