This is an automated email from the ASF dual-hosted git repository.
pvillard pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi-api.git
The following commit(s) were added to refs/heads/main by this push:
new 91d6291 NIFI-14374 Set Deterministic Order for Manifest Documentation
91d6291 is described below
commit 91d6291a0e53f0679c101559025b464f80bf4356
Author: exceptionfactory <[email protected]>
AuthorDate: Mon Mar 17 16:15:26 2025 -0500
NIFI-14374 Set Deterministic Order for Manifest Documentation
- Sorted Relationship documentation elements based on name field
- Sorted Resource Type documentation elements based on enumeration name
method
- Sorted Property Dependencies based on dependent Property Name
- Sorted Dependent Values based on natural String ordering
Signed-off-by: Pierre Villard <[email protected]>
This closes #4.
---
.../documentation/xml/XmlDocumentationWriter.java | 44 ++-
.../xml/XmlDocumentationWriterTest.java | 371 +++++++++++++++++++++
2 files changed, 398 insertions(+), 17 deletions(-)
diff --git
a/src/main/java/org/apache/nifi/documentation/xml/XmlDocumentationWriter.java
b/src/main/java/org/apache/nifi/documentation/xml/XmlDocumentationWriter.java
index e0b7a54..178fec5 100644
---
a/src/main/java/org/apache/nifi/documentation/xml/XmlDocumentationWriter.java
+++
b/src/main/java/org/apache/nifi/documentation/xml/XmlDocumentationWriter.java
@@ -21,10 +21,12 @@ import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.TreeSet;
import java.util.function.Function;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
@@ -65,13 +67,11 @@ import org.apache.nifi.documentation.ServiceAPI;
import org.apache.nifi.processor.Relationship;
/**
- * XML-based implementation of DocumentationWriter
- *
+ * 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!
@@ -108,19 +108,15 @@ public class XmlDocumentationWriter extends
AbstractDocumentationWriter {
return;
}
- final Class[] classes = deprecationNotice.alternatives();
+ 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());
- }
+ for (final Class<?> alternativeClass : classes) {
+ alternatives.add(alternativeClass.getName());
}
- if (classNames != null) {
- Collections.addAll(alternatives, classNames);
- }
+ Collections.addAll(alternatives, classNames);
writeDeprecationNotice(deprecationNotice.reason(), alternatives);
}
@@ -218,7 +214,16 @@ public class XmlDocumentationWriter extends
AbstractDocumentationWriter {
writeStartElement("resourceDefinition");
writeTextElement("cardinality",
resourceDefinition.getCardinality().name());
- writeArray("resourceTypes", resourceDefinition.getResourceTypes(),
this::writeResourceType);
+
+ final Set<ResourceType> resourceTypes =
resourceDefinition.getResourceTypes();
+ if (resourceTypes == null) {
+ writeArray("resourceTypes", null, this::writeResourceType);
+ } else {
+ final Set<ResourceType> orderedResourceTypes = new
TreeSet<>(Comparator.comparing(ResourceType::name));
+ orderedResourceTypes.addAll(resourceTypes);
+ writeArray("resourceTypes", orderedResourceTypes,
this::writeResourceType);
+ }
+
writeEndElement();
}
@@ -242,7 +247,9 @@ public class XmlDocumentationWriter extends
AbstractDocumentationWriter {
writeStartElement("dependencies");
- for (final PropertyDependency dependency : dependencies) {
+ final Set<PropertyDependency> orderedDependencies = new
TreeSet<>(Comparator.comparing(PropertyDependency::getPropertyName));
+ orderedDependencies.addAll(dependencies);
+ for (final PropertyDependency dependency : orderedDependencies) {
writeStartElement("dependency");
writeTextElement("propertyName", dependency.getPropertyName());
writeTextElement("propertyDisplayName",
dependency.getPropertyDisplayName());
@@ -250,7 +257,8 @@ public class XmlDocumentationWriter extends
AbstractDocumentationWriter {
final Set<String> dependentValues =
dependency.getDependentValues();
if (dependentValues != null) {
writeStartElement("dependentValues");
- for (final String dependentValue : dependentValues) {
+ final Collection<String> orderedDependentValues = new
TreeSet<>(dependentValues);
+ for (final String dependentValue : orderedDependentValues) {
writeTextElement("dependentValue", dependentValue);
}
writeEndElement();
@@ -355,12 +363,12 @@ public class XmlDocumentationWriter extends
AbstractDocumentationWriter {
return;
}
- final Class[] classes = seeAlso.value();
+ final Class<?>[] classes = seeAlso.value();
final String[] classNames = seeAlso.classNames();
final Set<String> toSee = new LinkedHashSet<>();
if (classes != null) {
- for (final Class classToSee : classes) {
+ for (final Class<?> classToSee : classes) {
toSee.add(classToSee.getName());
}
}
@@ -434,7 +442,9 @@ public class XmlDocumentationWriter extends
AbstractDocumentationWriter {
return;
}
- writeArray("relationships", relationships, rel -> {
+ final Set<Relationship> ordered = new
TreeSet<>(Comparator.comparing(Relationship::getName));
+ ordered.addAll(relationships);
+ writeArray("relationships", ordered, rel -> {
writeStartElement("relationship");
writeTextElement("name", rel.getName());
diff --git
a/src/test/java/org/apache/nifi/documentation/xml/XmlDocumentationWriterTest.java
b/src/test/java/org/apache/nifi/documentation/xml/XmlDocumentationWriterTest.java
new file mode 100644
index 0000000..71e0833
--- /dev/null
+++
b/src/test/java/org/apache/nifi/documentation/xml/XmlDocumentationWriterTest.java
@@ -0,0 +1,371 @@
+/*
+ * 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 org.apache.nifi.annotation.documentation.DeprecationNotice;
+import org.apache.nifi.components.ConfigurableComponent;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.resource.ResourceCardinality;
+import org.apache.nifi.components.resource.ResourceType;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ControllerService;
+import org.apache.nifi.documentation.ExtensionDocumentationWriter;
+import org.apache.nifi.documentation.ExtensionType;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Processor;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamWriter;
+import javax.xml.transform.dom.DOMResult;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+@ExtendWith(MockitoExtension.class)
+class XmlDocumentationWriterTest {
+
+ private static final Relationship SUCCESS_RELATIONSHIP = new
Relationship.Builder()
+ .name("success")
+ .description("FlowFile processed without errors")
+ .build();
+
+ private static final Relationship FAILURE_RELATIONSHIP = new
Relationship.Builder()
+ .name("failure")
+ .description("FlowFile processed with errors")
+ .build();
+
+ private static final Relationship ORIGINAL_RELATIONSHIP = new
Relationship.Builder()
+ .name("original")
+ .description("FlowFile processed without changes")
+ .build();
+
+ private static final Set<Relationship> UNORDERED_RELATIONSHIPS = Set.of(
+ SUCCESS_RELATIONSHIP,
+ FAILURE_RELATIONSHIP,
+ ORIGINAL_RELATIONSHIP
+ );
+
+ private static final List<String> EXPECTED_RELATIONSHIP_NAMES = List.of(
+ FAILURE_RELATIONSHIP.getName(),
+ ORIGINAL_RELATIONSHIP.getName(),
+ SUCCESS_RELATIONSHIP.getName()
+ );
+
+ private static final String FIRST_DEPENDENT_VALUE = "First";
+
+ private static final String SECOND_DEPENDENT_VALUE = "Second";
+
+ private static final String THIRD_DEPENDENT_VALUE = "Third";
+
+ private static final PropertyDescriptor FIRST_PROPERTY = new
PropertyDescriptor.Builder()
+ .name("First Property")
+ .build();
+
+ private static final PropertyDescriptor SECOND_PROPERTY = new
PropertyDescriptor.Builder()
+ .name("Second Property")
+ .identifiesExternalResource(ResourceCardinality.SINGLE,
ResourceType.URL, ResourceType.TEXT, ResourceType.FILE)
+ .dependsOn(FIRST_PROPERTY, THIRD_DEPENDENT_VALUE,
SECOND_DEPENDENT_VALUE, FIRST_DEPENDENT_VALUE)
+ .build();
+
+ private static final PropertyDescriptor THIRD_PROPERTY = new
PropertyDescriptor.Builder()
+ .name("Third Property")
+ .dependsOn(SECOND_PROPERTY)
+ .dependsOn(FIRST_PROPERTY)
+ .build();
+
+ private static final List<PropertyDescriptor> PROPERTY_DESCRIPTORS =
List.of(
+ FIRST_PROPERTY,
+ SECOND_PROPERTY,
+ THIRD_PROPERTY
+ );
+
+ private static final List<String> EXPECTED_PROPERTY_NAMES = List.of(
+ FIRST_PROPERTY.getName(),
+ SECOND_PROPERTY.getName(),
+ THIRD_PROPERTY.getName()
+ );
+
+ private static final List<String> EXPECTED_DEPENDENT_PROPERTY_NAMES =
List.of(
+ FIRST_PROPERTY.getName(),
+ SECOND_PROPERTY.getName()
+ );
+
+
+ private static final List<String> EXPECTED_DEPENDENT_VALUES = List.of(
+ FIRST_DEPENDENT_VALUE,
+ SECOND_DEPENDENT_VALUE,
+ THIRD_DEPENDENT_VALUE
+ );
+
+ private static final List<String> EXPECTED_RESOURCE_TYPE_NAMES = List.of(
+ ResourceType.FILE.name(),
+ ResourceType.TEXT.name(),
+ ResourceType.URL.name()
+ );
+
+ @Test
+ void testWriteMinimalProcessor() throws Exception {
+ final Processor processor = new MinimalProcessor();
+ final Document document = writeDocumentation(processor);
+
+ assertExtensionNameTypeFound(processor, ExtensionType.PROCESSOR,
document);
+ }
+
+ @Test
+ void testWriteMinimalControllerService() throws Exception {
+ final ControllerService controllerService = new
MinimalControllerService();
+ final Document document = writeDocumentation(controllerService);
+
+ assertExtensionNameTypeFound(controllerService,
ExtensionType.CONTROLLER_SERVICE, document);
+ }
+
+ @Test
+ void testWriteDeprecatedControllerService() throws Exception {
+ final ControllerService controllerService = new
DeprecatedControllerService();
+ final Document document = writeDocumentation(controllerService);
+
+ assertExtensionNameTypeFound(controllerService,
ExtensionType.CONTROLLER_SERVICE, document);
+
+ final Node deprecationNoticeReason =
findNode("/extension/deprecationNotice/reason", document);
+ assertNotNull(deprecationNoticeReason);
+ final Node reasonChildNode = deprecationNoticeReason.getFirstChild();
+ assertNull(reasonChildNode.getFirstChild());
+
+ final Node deprecationNoticeAlternatives =
findNode("/extension/deprecationNotice/alternatives", document);
+ assertNotNull(deprecationNoticeAlternatives);
+
+ assertNull(deprecationNoticeAlternatives.getFirstChild());
+ }
+
+ @Test
+ void testWriteRelationships() throws Exception {
+ final Processor processor = new RelationshipProcessor();
+ final Document document = writeDocumentation(processor);
+
+ assertExtensionNameTypeFound(processor, ExtensionType.PROCESSOR,
document);
+ assertRelationshipsMatched(document);
+ }
+
+ @Test
+ void testWritePropertyDescriptors() throws Exception {
+ final Processor processor = new PropertyDescriptorProcessor();
+ final Document document = writeDocumentation(processor);
+
+ assertExtensionNameTypeFound(processor, ExtensionType.PROCESSOR,
document);
+ assertPropertyDescriptorsMatched(document);
+ assertDependentPropertyNamesMatched(document);
+ assertDependentPropertyValuesMatched(document);
+ assertResourceTypesMatched(document);
+ }
+
+ private void assertRelationshipsMatched(final Document document) throws
XPathExpressionException {
+ final Node relationshipsNode = findNode("/extension/relationships",
document);
+ assertNotNull(relationshipsNode);
+
+ final List<String> relationshipNames = new ArrayList<>();
+ final NodeList relationships = relationshipsNode.getChildNodes();
+ for (int i = 0; i < relationships.getLength(); i++) {
+ final Node relationshipNode = relationships.item(i);
+ assertEquals("relationship", relationshipNode.getNodeName());
+
+ final Node relationshipNameNode = relationshipNode.getFirstChild();
+ assertEquals("name", relationshipNameNode.getNodeName());
+
+ final String relationshipName =
relationshipNameNode.getTextContent();
+ relationshipNames.add(relationshipName);
+ }
+
+ assertEquals(EXPECTED_RELATIONSHIP_NAMES, relationshipNames);
+ }
+
+ private void assertPropertyDescriptorsMatched(final Document document)
throws XPathExpressionException {
+ final Node propertiesNode = findNode("/extension/properties",
document);
+ assertNotNull(propertiesNode);
+
+ final List<String> propertyNames = new ArrayList<>();
+ final NodeList properties = propertiesNode.getChildNodes();
+ for (int i = 0; i < properties.getLength(); i++) {
+ final Node propertyNode = properties.item(i);
+ assertEquals("property", propertyNode.getNodeName());
+
+ final Node propertyNameNode = propertyNode.getFirstChild();
+ assertEquals("name", propertyNameNode.getNodeName());
+
+ final String propertyName = propertyNameNode.getTextContent();
+ propertyNames.add(propertyName);
+ }
+
+ assertEquals(EXPECTED_PROPERTY_NAMES, propertyNames);
+ }
+
+ private void assertDependentPropertyNamesMatched(final Document document)
throws XPathExpressionException {
+ final Node dependenciesNode =
findNode("/extension/properties/property[name='Third Property']/dependencies",
document);
+ assertNotNull(dependenciesNode);
+
+ final NodeList dependencies = dependenciesNode.getChildNodes();
+ final List<String> dependentPropertyNames = new ArrayList<>();
+ for (int i = 0; i < dependencies.getLength(); i++) {
+ final Node dependencyNode = dependencies.item(i);
+ assertEquals("dependency", dependencyNode.getNodeName());
+
+ final Node propertyNode = dependencyNode.getFirstChild();
+ assertEquals("propertyName", propertyNode.getNodeName());
+
+ final String propertyName = propertyNode.getTextContent();
+ dependentPropertyNames.add(propertyName);
+ }
+
+ assertEquals(EXPECTED_DEPENDENT_PROPERTY_NAMES,
dependentPropertyNames);
+ }
+
+ private void assertDependentPropertyValuesMatched(final Document document)
throws XPathExpressionException {
+ final Node dependentValuesNode =
findNode("/extension/properties/property[name='Second
Property']/dependencies/dependency/dependentValues", document);
+ assertNotNull(dependentValuesNode);
+
+ final NodeList dependentValues = dependentValuesNode.getChildNodes();
+ final List<String> values = new ArrayList<>();
+ for (int i = 0; i < dependentValues.getLength(); i++) {
+ final Node valueNode = dependentValues.item(i);
+ final String value = valueNode.getTextContent();
+ values.add(value);
+ }
+
+ assertEquals(EXPECTED_DEPENDENT_VALUES, values);
+ }
+
+ private void assertResourceTypesMatched(final Document document) throws
XPathExpressionException {
+ final Node resourceTypesNode =
findNode("/extension/properties/property[name='Second
Property']/resourceDefinition/resourceTypes", document);
+ assertNotNull(resourceTypesNode);
+
+ final NodeList resourceTypes = resourceTypesNode.getChildNodes();
+ final List<String> resourceTypeNames = new ArrayList<>();
+ for (int i = 0; i < resourceTypes.getLength(); i++) {
+ final Node resourceTypeNode = resourceTypes.item(i);
+ assertEquals("resourceType", resourceTypeNode.getNodeName());
+ final String resourceType = resourceTypeNode.getTextContent();
+ resourceTypeNames.add(resourceType);
+ }
+
+ assertEquals(EXPECTED_RESOURCE_TYPE_NAMES, resourceTypeNames);
+ }
+
+
+ private Node findNode(final String expression, final Node node) throws
XPathExpressionException {
+ final XPathFactory factory = XPathFactory.newInstance();
+ final XPath path = factory.newXPath();
+
+ return path.evaluateExpression(expression, node, Node.class);
+ }
+
+ private void assertExtensionNameTypeFound(final ConfigurableComponent
component, final ExtensionType expectedExtensionType, final Document document) {
+ assertNotNull(document);
+
+ final Node extensionNode = document.getFirstChild();
+ assertEquals("extension", extensionNode.getNodeName());
+
+ final Node nameNode = extensionNode.getFirstChild();
+ assertEquals("name", nameNode.getNodeName());
+ assertEquals(component.getClass().getName(),
nameNode.getTextContent());
+
+ final Node typeNode = nameNode.getNextSibling();
+ assertEquals("type", typeNode.getNodeName());
+ assertEquals(expectedExtensionType.name(), typeNode.getTextContent());
+ }
+
+ private Document writeDocumentation(final ConfigurableComponent component)
throws Exception {
+ final XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
+ final DocumentBuilderFactory documentBuilderFactory =
DocumentBuilderFactory.newInstance();
+ final DocumentBuilder documentBuilder =
documentBuilderFactory.newDocumentBuilder();
+ final Document document = documentBuilder.newDocument();
+
+ final DOMResult result = new DOMResult(document);
+ final XMLStreamWriter streamWriter =
outputFactory.createXMLStreamWriter(result);
+
+ final ExtensionDocumentationWriter documentationWriter = new
XmlDocumentationWriter(streamWriter);
+
+ try {
+ documentationWriter.write(component);
+ } finally {
+ streamWriter.close();
+ }
+
+ return document;
+ }
+
+ private static class MinimalProcessor extends AbstractProcessor {
+
+ @Override
+ public void onTrigger(final ProcessContext context, final
ProcessSession session) throws ProcessException {
+
+ }
+ }
+
+ private static class MinimalControllerService extends
AbstractControllerService {
+
+ }
+
+ @DeprecationNotice
+ private static class DeprecatedControllerService extends
AbstractControllerService {
+
+ }
+
+ private static class RelationshipProcessor extends AbstractProcessor {
+
+ @Override
+ public void onTrigger(final ProcessContext context, final
ProcessSession session) throws ProcessException {
+
+ }
+
+ @Override
+ public Set<Relationship> getRelationships() {
+ return UNORDERED_RELATIONSHIPS;
+ }
+ }
+
+ private static class PropertyDescriptorProcessor extends AbstractProcessor
{
+
+ @Override
+ public void onTrigger(final ProcessContext context, final
ProcessSession session) throws ProcessException {
+
+ }
+
+ @Override
+ protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+ return PROPERTY_DESCRIPTORS;
+ }
+ }
+}