This is an automated email from the ASF dual-hosted git repository. markap14 pushed a commit to branch NIFI-15258 in repository https://gitbox.apache.org/repos/asf/nifi-api.git
commit 6705411cc94b859e46b9931da92a92a92fd2e916 Author: Matt Gilman <[email protected]> AuthorDate: Tue Dec 23 11:50:26 2025 -0500 NIFI-15353: Adding documentation writers for connectors. (#40) This closes #40 --- .../AbstractConnectorDocumentationWriter.java | 106 +++ .../ConnectorDocumentationWriter.java | 52 ++ .../apache/nifi/documentation/ExtensionType.java | 4 +- ...ocumentationConnectorInitializationContext.java | 54 ++ .../xml/XmlConnectorDocumentationWriter.java | 344 ++++++++ ...entationConnectorInitializationContextTest.java | 77 ++ .../xml/XmlConnectorDocumentationWriterTest.java | 866 +++++++++++++++++++++ 7 files changed, 1502 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/apache/nifi/documentation/AbstractConnectorDocumentationWriter.java b/src/main/java/org/apache/nifi/documentation/AbstractConnectorDocumentationWriter.java new file mode 100644 index 0000000..f10fcd1 --- /dev/null +++ b/src/main/java/org/apache/nifi/documentation/AbstractConnectorDocumentationWriter.java @@ -0,0 +1,106 @@ +/* + * 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; + +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.components.connector.ConfigurationStep; +import org.apache.nifi.components.connector.Connector; +import org.apache.nifi.documentation.init.DocumentationConnectorInitializationContext; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Base class for ConnectorDocumentationWriter that simplifies iterating over all information for a Connector, + * creating a separate method for each piece of documentation, to ensure that implementations properly override + * all methods and therefore properly account for all information about a Connector. + * + * 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 for Connectors 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 abstract class AbstractConnectorDocumentationWriter implements ConnectorDocumentationWriter { + + @Override + public void initialize(final Connector connector) { + connector.initialize(new DocumentationConnectorInitializationContext()); + } + + @Override + public final void write(final Connector connector) throws IOException { + writeHeader(connector); + writeBody(connector); + writeFooter(connector); + } + + protected void writeBody(final Connector connector) throws IOException { + writeExtensionName(connector.getClass().getName()); + writeExtensionType(ExtensionType.CONNECTOR); + writeDeprecationNotice(connector.getClass().getAnnotation(DeprecationNotice.class)); + writeDescription(getDescription(connector)); + writeTags(getTags(connector)); + writeConfigurationSteps(connector.getConfigurationSteps()); + writeSeeAlso(connector.getClass().getAnnotation(SeeAlso.class)); + } + + protected String getDescription(final Connector connector) { + final CapabilityDescription capabilityDescription = connector.getClass().getAnnotation(CapabilityDescription.class); + if (capabilityDescription == null) { + return null; + } + return capabilityDescription.value(); + } + + protected List<String> getTags(final Connector connector) { + final Tags tags = connector.getClass().getAnnotation(Tags.class); + if (tags == null) { + return Collections.emptyList(); + } + final String[] tagValues = tags.value(); + return tagValues == null ? Collections.emptyList() : Arrays.asList(tagValues); + } + + protected abstract void writeHeader(Connector connector) throws IOException; + + protected abstract void writeExtensionName(String extensionName) throws IOException; + + protected abstract void writeExtensionType(ExtensionType extensionType) throws IOException; + + protected abstract void writeDeprecationNotice(DeprecationNotice deprecationNotice) throws IOException; + + protected abstract void writeDescription(String description) throws IOException; + + protected abstract void writeTags(List<String> tags) throws IOException; + + protected abstract void writeConfigurationSteps(List<ConfigurationStep> configurationSteps) throws IOException; + + protected abstract void writeSeeAlso(SeeAlso seeAlso) throws IOException; + + protected abstract void writeFooter(Connector connector) throws IOException; + +} + diff --git a/src/main/java/org/apache/nifi/documentation/ConnectorDocumentationWriter.java b/src/main/java/org/apache/nifi/documentation/ConnectorDocumentationWriter.java new file mode 100644 index 0000000..a7edb41 --- /dev/null +++ b/src/main/java/org/apache/nifi/documentation/ConnectorDocumentationWriter.java @@ -0,0 +1,52 @@ +/* + * 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; + +import java.io.IOException; +import org.apache.nifi.components.connector.Connector; + +/** + * Generates documentation for an instance of a Connector. + * + * 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 for Connectors 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 interface ConnectorDocumentationWriter { + + /** + * Calls initialize on the connector. Must be called before calling any write methods. + * + * @param connector the connector to initialize + */ + void initialize(Connector connector); + + /** + * Write the documentation for the given connector. + * + * @param connector the connector to document + * @throws IOException if an error occurs writing the documentation + */ + void write(Connector connector) throws IOException; + +} + diff --git a/src/main/java/org/apache/nifi/documentation/ExtensionType.java b/src/main/java/org/apache/nifi/documentation/ExtensionType.java index 7f6fbab..160966e 100644 --- a/src/main/java/org/apache/nifi/documentation/ExtensionType.java +++ b/src/main/java/org/apache/nifi/documentation/ExtensionType.java @@ -27,5 +27,7 @@ public enum ExtensionType { PARAMETER_PROVIDER, - FLOW_REGISTRY_CLIENT; + FLOW_REGISTRY_CLIENT, + + CONNECTOR; } diff --git a/src/main/java/org/apache/nifi/documentation/init/DocumentationConnectorInitializationContext.java b/src/main/java/org/apache/nifi/documentation/init/DocumentationConnectorInitializationContext.java new file mode 100644 index 0000000..3529a9f --- /dev/null +++ b/src/main/java/org/apache/nifi/documentation/init/DocumentationConnectorInitializationContext.java @@ -0,0 +1,54 @@ +/* + * 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.init; + +import java.util.UUID; +import org.apache.nifi.components.connector.ConnectorInitializationContext; +import org.apache.nifi.components.connector.FlowUpdateException; +import org.apache.nifi.components.connector.components.FlowContext; +import org.apache.nifi.flow.VersionedExternalFlow; +import org.apache.nifi.logging.ComponentLog; + +/** + * A ConnectorInitializationContext implementation for use during documentation generation. + * This context provides minimal functionality needed to initialize a Connector for the purposes + * of extracting its configuration steps and other documentation metadata. + */ +public class DocumentationConnectorInitializationContext implements ConnectorInitializationContext { + private final String uuid = UUID.randomUUID().toString(); + + @Override + public String getIdentifier() { + return uuid; + } + + @Override + public String getName() { + return "DocumentationConnector"; + } + + @Override + public ComponentLog getLogger() { + return new NopComponentLog(); + } + + @Override + public void updateFlow(final FlowContext flowContext, final VersionedExternalFlow versionedExternalFlow) throws FlowUpdateException { + // No-op for documentation purposes - we don't actually update any flows + } +} + diff --git a/src/main/java/org/apache/nifi/documentation/xml/XmlConnectorDocumentationWriter.java b/src/main/java/org/apache/nifi/documentation/xml/XmlConnectorDocumentationWriter.java new file mode 100644 index 0000000..9fdcc19 --- /dev/null +++ b/src/main/java/org/apache/nifi/documentation/xml/XmlConnectorDocumentationWriter.java @@ -0,0 +1,344 @@ +/* + * 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.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +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.documentation.DeprecationNotice; +import org.apache.nifi.annotation.documentation.SeeAlso; +import org.apache.nifi.components.DescribedValue; +import org.apache.nifi.components.connector.ConfigurationStep; +import org.apache.nifi.components.connector.ConfigurationStepDependency; +import org.apache.nifi.components.connector.Connector; +import org.apache.nifi.components.connector.ConnectorPropertyDependency; +import org.apache.nifi.components.connector.ConnectorPropertyDescriptor; +import org.apache.nifi.components.connector.ConnectorPropertyGroup; +import org.apache.nifi.documentation.AbstractConnectorDocumentationWriter; +import org.apache.nifi.documentation.ExtensionType; + +/** + * XML-based implementation of ConnectorDocumentationWriter. + * 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 for Connectors 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 XmlConnectorDocumentationWriter extends AbstractConnectorDocumentationWriter { + private final XMLStreamWriter writer; + + public XmlConnectorDocumentationWriter(final OutputStream out) throws XMLStreamException { + this.writer = XMLOutputFactory.newInstance().createXMLStreamWriter(out, "UTF-8"); + } + + public XmlConnectorDocumentationWriter(final XMLStreamWriter writer) { + this.writer = writer; + } + + @Override + protected void writeHeader(final Connector connector) 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<>(); + for (final Class<?> alternativeClass : classes) { + alternatives.add(alternativeClass.getName()); + } + + Collections.addAll(alternatives, classNames); + + writeStartElement("deprecationNotice"); + writeTextElement("reason", deprecationNotice.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 || tags.isEmpty()) { + return; + } + writeTextArray("tags", "tag", tags); + } + + @Override + protected void writeConfigurationSteps(final List<ConfigurationStep> configurationSteps) throws IOException { + if (configurationSteps == null || configurationSteps.isEmpty()) { + return; + } + + writeStartElement("configurationSteps"); + for (final ConfigurationStep step : configurationSteps) { + writeConfigurationStep(step); + } + writeEndElement(); + } + + private void writeConfigurationStep(final ConfigurationStep step) throws IOException { + writeStartElement("configurationStep"); + + writeTextElement("name", step.getName()); + if (step.getDescription() != null) { + writeTextElement("description", step.getDescription()); + } + + // Write step dependencies + final Set<ConfigurationStepDependency> stepDependencies = step.getDependencies(); + if (stepDependencies != null && !stepDependencies.isEmpty()) { + writeStartElement("stepDependencies"); + for (final ConfigurationStepDependency dependency : stepDependencies) { + writeConfigurationStepDependency(dependency); + } + writeEndElement(); + } + + // Write property groups + final List<ConnectorPropertyGroup> propertyGroups = step.getPropertyGroups(); + if (propertyGroups != null && !propertyGroups.isEmpty()) { + writeStartElement("propertyGroups"); + for (final ConnectorPropertyGroup group : propertyGroups) { + writePropertyGroup(group); + } + writeEndElement(); + } + + writeEndElement(); + } + + private void writeConfigurationStepDependency(final ConfigurationStepDependency dependency) throws IOException { + writeStartElement("stepDependency"); + + writeTextElement("stepName", dependency.getStepName()); + writeTextElement("propertyName", dependency.getPropertyName()); + + final Set<String> dependentValues = dependency.getDependentValues(); + if (dependentValues != null && !dependentValues.isEmpty()) { + writeTextArray("dependentValues", "dependentValue", dependentValues); + } + + writeEndElement(); + } + + private void writePropertyGroup(final ConnectorPropertyGroup group) throws IOException { + writeStartElement("propertyGroup"); + + if (group.getName() != null) { + writeTextElement("name", group.getName()); + } + if (group.getDescription() != null) { + writeTextElement("description", group.getDescription()); + } + + final List<ConnectorPropertyDescriptor> properties = group.getProperties(); + if (properties != null && !properties.isEmpty()) { + writeStartElement("properties"); + for (final ConnectorPropertyDescriptor property : properties) { + writeConnectorProperty(property); + } + writeEndElement(); + } + + writeEndElement(); + } + + private void writeConnectorProperty(final ConnectorPropertyDescriptor property) throws IOException { + writeStartElement("property"); + + writeTextElement("name", property.getName()); + if (property.getDescription() != null) { + writeTextElement("description", property.getDescription()); + } + if (property.getDefaultValue() != null) { + writeTextElement("defaultValue", property.getDefaultValue()); + } + writeBooleanElement("required", property.isRequired()); + writeTextElement("propertyType", property.getType().name()); + writeBooleanElement("allowableValuesFetchable", property.isAllowableValuesFetchable()); + + // Write allowable values + final List<DescribedValue> allowableValues = property.getAllowableValues(); + if (allowableValues != null && !allowableValues.isEmpty()) { + writeStartElement("allowableValues"); + for (final DescribedValue value : allowableValues) { + writeAllowableValue(value); + } + writeEndElement(); + } + + // Write property dependencies + final Set<ConnectorPropertyDependency> dependencies = property.getDependencies(); + if (dependencies != null && !dependencies.isEmpty()) { + writeStartElement("dependencies"); + for (final ConnectorPropertyDependency dependency : dependencies) { + writePropertyDependency(dependency); + } + writeEndElement(); + } + + writeEndElement(); + } + + private void writeAllowableValue(final DescribedValue value) throws IOException { + writeStartElement("allowableValue"); + writeTextElement("displayName", value.getDisplayName()); + writeTextElement("value", value.getValue()); + if (value.getDescription() != null) { + writeTextElement("description", value.getDescription()); + } + writeEndElement(); + } + + private void writePropertyDependency(final ConnectorPropertyDependency dependency) throws IOException { + writeStartElement("dependency"); + writeTextElement("propertyName", dependency.getPropertyName()); + + final Set<String> dependentValues = dependency.getDependentValues(); + if (dependentValues != null && !dependentValues.isEmpty()) { + writeTextArray("dependentValues", "dependentValue", dependentValues); + } + + 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 writeFooter(final Connector connector) throws IOException { + writeEndElement(); + } + + // Utility methods for XML writing + + 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 void writeText(final String text) throws IOException { + if (text == null) { + return; + } + try { + writer.writeCharacters(text); + } catch (final XMLStreamException e) { + throw new IOException(e); + } + } + + 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(); + } + +} + diff --git a/src/test/java/org/apache/nifi/documentation/init/DocumentationConnectorInitializationContextTest.java b/src/test/java/org/apache/nifi/documentation/init/DocumentationConnectorInitializationContextTest.java new file mode 100644 index 0000000..96c4dcb --- /dev/null +++ b/src/test/java/org/apache/nifi/documentation/init/DocumentationConnectorInitializationContextTest.java @@ -0,0 +1,77 @@ +/* + * 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.init; + +import org.apache.nifi.logging.ComponentLog; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class DocumentationConnectorInitializationContextTest { + + private DocumentationConnectorInitializationContext context; + + @BeforeEach + void setUp() { + context = new DocumentationConnectorInitializationContext(); + } + + @Test + void testGetIdentifierReturnsNonNullUUID() { + final String identifier = context.getIdentifier(); + + assertNotNull(identifier); + } + + @Test + void testGetIdentifierReturnsConsistentValue() { + final String identifier1 = context.getIdentifier(); + final String identifier2 = context.getIdentifier(); + + assertEquals(identifier1, identifier2); + } + + @Test + void testDifferentInstancesHaveDifferentIdentifiers() { + final DocumentationConnectorInitializationContext otherContext = new DocumentationConnectorInitializationContext(); + + assertNotEquals(context.getIdentifier(), otherContext.getIdentifier()); + } + + @Test + void testGetNameReturnsExpectedValue() { + assertEquals("DocumentationConnector", context.getName()); + } + + @Test + void testGetLoggerReturnsNopComponentLog() { + final ComponentLog logger = context.getLogger(); + + assertNotNull(logger); + assertEquals(NopComponentLog.class, logger.getClass()); + } + + @Test + void testUpdateFlowDoesNotThrow() throws Exception { + // updateFlow is a no-op for documentation purposes, should not throw + context.updateFlow(null, null); + } +} + diff --git a/src/test/java/org/apache/nifi/documentation/xml/XmlConnectorDocumentationWriterTest.java b/src/test/java/org/apache/nifi/documentation/xml/XmlConnectorDocumentationWriterTest.java new file mode 100644 index 0000000..dc5912d --- /dev/null +++ b/src/test/java/org/apache/nifi/documentation/xml/XmlConnectorDocumentationWriterTest.java @@ -0,0 +1,866 @@ +/* + * 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.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.components.AllowableValue; +import org.apache.nifi.components.ConfigVerificationResult; +import org.apache.nifi.components.connector.AbstractConnector; +import org.apache.nifi.components.connector.ConfigurationStep; +import org.apache.nifi.components.connector.Connector; +import org.apache.nifi.components.connector.ConnectorPropertyDescriptor; +import org.apache.nifi.components.connector.ConnectorPropertyGroup; +import org.apache.nifi.components.connector.PropertyType; +import org.apache.nifi.components.connector.components.FlowContext; +import org.apache.nifi.documentation.ExtensionType; +import org.apache.nifi.flow.VersionedExternalFlow; +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.Collections; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(MockitoExtension.class) +class XmlConnectorDocumentationWriterTest { + + @Test + void testWriteMinimalConnector() throws Exception { + final Connector connector = new MinimalConnector(); + final Document document = writeDocumentation(connector); + + assertExtensionNameTypeFound(connector, ExtensionType.CONNECTOR, document); + } + + @Test + void testWriteConnectorWithDescription() throws Exception { + final Connector connector = new DescribedConnector(); + final Document document = writeDocumentation(connector); + + assertExtensionNameTypeFound(connector, ExtensionType.CONNECTOR, document); + + final Node descriptionNode = findNode("/extension/description", document); + assertNotNull(descriptionNode); + assertEquals("A connector with a description", descriptionNode.getTextContent()); + } + + @Test + void testWriteConnectorWithTags() throws Exception { + final Connector connector = new TaggedConnector(); + final Document document = writeDocumentation(connector); + + assertExtensionNameTypeFound(connector, ExtensionType.CONNECTOR, document); + + final Node tagsNode = findNode("/extension/tags", document); + assertNotNull(tagsNode); + + final List<String> tagValues = new ArrayList<>(); + final NodeList tagNodes = tagsNode.getChildNodes(); + for (int i = 0; i < tagNodes.getLength(); i++) { + final Node tagNode = tagNodes.item(i); + assertEquals("tag", tagNode.getNodeName()); + tagValues.add(tagNode.getTextContent()); + } + + assertEquals(List.of("test", "documentation", "connector"), tagValues); + } + + @Test + void testWriteDeprecatedConnector() throws Exception { + final Connector connector = new DeprecatedConnector(); + final Document document = writeDocumentation(connector); + + assertExtensionNameTypeFound(connector, ExtensionType.CONNECTOR, document); + + final Node deprecationNoticeNode = findNode("/extension/deprecationNotice", document); + assertNotNull(deprecationNoticeNode); + + final Node reasonNode = findNode("/extension/deprecationNotice/reason", document); + assertNotNull(reasonNode); + assertEquals("Use a different connector", reasonNode.getTextContent()); + } + + @Test + void testWriteConnectorWithSeeAlso() throws Exception { + final Connector connector = new SeeAlsoConnector(); + final Document document = writeDocumentation(connector); + + assertExtensionNameTypeFound(connector, ExtensionType.CONNECTOR, document); + + final Node seeAlsoNode = findNode("/extension/seeAlso", document); + assertNotNull(seeAlsoNode); + + final List<String> seeAlsoValues = new ArrayList<>(); + final NodeList seeNodes = seeAlsoNode.getChildNodes(); + for (int i = 0; i < seeNodes.getLength(); i++) { + final Node seeNode = seeNodes.item(i); + assertEquals("see", seeNode.getNodeName()); + seeAlsoValues.add(seeNode.getTextContent()); + } + + assertEquals(1, seeAlsoValues.size()); + assertEquals("org.apache.nifi.documentation.xml.XmlConnectorDocumentationWriterTest$MinimalConnector", seeAlsoValues.getFirst()); + } + + @Test + void testWriteConnectorWithConfigurationSteps() throws Exception { + final Connector connector = new ConnectorWithConfigurationSteps(); + final Document document = writeDocumentation(connector); + + assertExtensionNameTypeFound(connector, ExtensionType.CONNECTOR, document); + + final Node configStepsNode = findNode("/extension/configurationSteps", document); + assertNotNull(configStepsNode); + + final Node firstStepNode = findNode("/extension/configurationSteps/configurationStep[name='Step One']", document); + assertNotNull(firstStepNode); + + final Node stepNameNode = findNode("/extension/configurationSteps/configurationStep/name", document); + assertNotNull(stepNameNode); + assertEquals("Step One", stepNameNode.getTextContent()); + + final Node stepDescriptionNode = findNode("/extension/configurationSteps/configurationStep/description", document); + assertNotNull(stepDescriptionNode); + assertEquals("First configuration step", stepDescriptionNode.getTextContent()); + } + + @Test + void testWriteConnectorWithConfigurationStepWithoutDescription() throws Exception { + final Connector connector = new ConnectorWithStepWithoutDescription(); + final Document document = writeDocumentation(connector); + + assertExtensionNameTypeFound(connector, ExtensionType.CONNECTOR, document); + + final Node stepNameNode = findNode("/extension/configurationSteps/configurationStep/name", document); + assertNotNull(stepNameNode); + assertEquals("Minimal Step", stepNameNode.getTextContent()); + + // Step description is optional - should not be present when not provided + final Node stepDescriptionNode = findNode("/extension/configurationSteps/configurationStep/description", document); + assertNull(stepDescriptionNode); + } + + @Test + void testWriteConnectorWithPropertyGroups() throws Exception { + final Connector connector = new ConnectorWithPropertyGroups(); + final Document document = writeDocumentation(connector); + + assertExtensionNameTypeFound(connector, ExtensionType.CONNECTOR, document); + + final Node propertyGroupsNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups", document); + assertNotNull(propertyGroupsNode); + + final Node propertyGroupNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup", document); + assertNotNull(propertyGroupNode); + + final Node groupNameNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/name", document); + assertNotNull(groupNameNode); + assertEquals("Connection Settings", groupNameNode.getTextContent()); + } + + @Test + void testWriteConnectorWithProperties() throws Exception { + final Connector connector = new ConnectorWithProperties(); + final Document document = writeDocumentation(connector); + + assertExtensionNameTypeFound(connector, ExtensionType.CONNECTOR, document); + + final Node propertiesNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties", document); + assertNotNull(propertiesNode); + + final Node propertyNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property", document); + assertNotNull(propertyNode); + + final Node propertyNameNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property/name", document); + assertNotNull(propertyNameNode); + assertEquals("Hostname", propertyNameNode.getTextContent()); + + final Node propertyDescriptionNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property/description", document); + assertNotNull(propertyDescriptionNode); + assertEquals("The hostname to connect to", propertyDescriptionNode.getTextContent()); + + final Node propertyRequiredNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property/required", document); + assertNotNull(propertyRequiredNode); + assertEquals("true", propertyRequiredNode.getTextContent()); + + final Node propertyTypeNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property/propertyType", document); + assertNotNull(propertyTypeNode); + assertEquals("STRING", propertyTypeNode.getTextContent()); + } + + @Test + void testWriteConnectorWithPropertyDefaultValue() throws Exception { + final Connector connector = new ConnectorWithPropertyDefaultValue(); + final Document document = writeDocumentation(connector); + + final Node defaultValueNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property/defaultValue", document); + assertNotNull(defaultValueNode); + assertEquals("localhost", defaultValueNode.getTextContent()); + } + + @Test + void testWriteConnectorWithAllowableValues() throws Exception { + final Connector connector = new ConnectorWithAllowableValues(); + final Document document = writeDocumentation(connector); + + final Node allowableValuesNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property/allowableValues", document); + assertNotNull(allowableValuesNode); + + final List<String> allowableValues = new ArrayList<>(); + final NodeList valueNodes = allowableValuesNode.getChildNodes(); + for (int i = 0; i < valueNodes.getLength(); i++) { + final Node valueNode = valueNodes.item(i); + if ("allowableValue".equals(valueNode.getNodeName())) { + final Node valueTextNode = findNode("value", valueNode); + if (valueTextNode != null) { + allowableValues.add(valueTextNode.getTextContent()); + } + } + } + + assertEquals(List.of("TCP", "UDP"), allowableValues); + } + + @Test + void testWriteConnectorWithPropertyDependencies() throws Exception { + final Connector connector = new ConnectorWithPropertyDependencies(); + final Document document = writeDocumentation(connector); + + final Node dependenciesNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property[name='Port']/dependencies", document); + assertNotNull(dependenciesNode); + + final Node dependencyNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property[name='Port']/dependencies/dependency", document); + assertNotNull(dependencyNode); + + final Node dependencyPropertyNameNode = findNode( + "/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property[name='Port']/dependencies/dependency/propertyName", + document); + assertNotNull(dependencyPropertyNameNode); + assertEquals("Hostname", dependencyPropertyNameNode.getTextContent()); + } + + @Test + void testWriteConnectorWithStepDependencies() throws Exception { + final Connector connector = new ConnectorWithStepDependencies(); + final Document document = writeDocumentation(connector); + + final Node stepDependenciesNode = findNode("/extension/configurationSteps/configurationStep[name='Step Two']/stepDependencies", document); + assertNotNull(stepDependenciesNode); + + final Node stepDependencyNode = findNode("/extension/configurationSteps/configurationStep[name='Step Two']/stepDependencies/stepDependency", document); + assertNotNull(stepDependencyNode); + + final Node stepNameNode = findNode("/extension/configurationSteps/configurationStep[name='Step Two']/stepDependencies/stepDependency/stepName", document); + assertNotNull(stepNameNode); + assertEquals("Step One", stepNameNode.getTextContent()); + + final Node propertyNameNode = findNode("/extension/configurationSteps/configurationStep[name='Step Two']/stepDependencies/stepDependency/propertyName", document); + assertNotNull(propertyNameNode); + assertEquals("Enable Advanced", propertyNameNode.getTextContent()); + } + + @Test + void testWriteConnectorWithFetchableAllowableValues() throws Exception { + final Connector connector = new ConnectorWithFetchableAllowableValues(); + final Document document = writeDocumentation(connector); + + final Node allowableValuesFetchableNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property/allowableValuesFetchable", document); + assertNotNull(allowableValuesFetchableNode); + assertEquals("true", allowableValuesFetchableNode.getTextContent()); + } + + @Test + void testWriteConnectorWithNoDescription() throws Exception { + final Connector connector = new MinimalConnector(); + final Document document = writeDocumentation(connector); + + final Node descriptionNode = findNode("/extension/description", document); + assertNull(descriptionNode); + } + + @Test + void testWriteConnectorWithNoTags() throws Exception { + final Connector connector = new MinimalConnector(); + final Document document = writeDocumentation(connector); + + final Node tagsNode = findNode("/extension/tags", document); + assertNull(tagsNode); + } + + @Test + void testWriteConnectorWithPropertyDependencyValues() throws Exception { + final Connector connector = new ConnectorWithPropertyDependencyValues(); + final Document document = writeDocumentation(connector); + + final String xpath = "/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup" + + "/properties/property[name='Advanced Setting']/dependencies/dependency/dependentValues"; + final Node dependentValuesNode = findNode(xpath, document); + assertNotNull(dependentValuesNode); + + final List<String> dependentValues = new ArrayList<>(); + final NodeList valueNodes = dependentValuesNode.getChildNodes(); + for (int i = 0; i < valueNodes.getLength(); i++) { + final Node valueNode = valueNodes.item(i); + if ("dependentValue".equals(valueNode.getNodeName())) { + dependentValues.add(valueNode.getTextContent()); + } + } + + assertEquals(2, dependentValues.size()); + assertTrue(dependentValues.contains("advanced")); + assertTrue(dependentValues.contains("expert")); + } + + @Test + void testWriteConnectorWithStepDependencyValues() throws Exception { + final Connector connector = new ConnectorWithStepDependencyValues(); + final Document document = writeDocumentation(connector); + + final Node dependentValuesNode = findNode("/extension/configurationSteps/configurationStep[name='Step Two']/stepDependencies/stepDependency/dependentValues", document); + assertNotNull(dependentValuesNode); + + final List<String> dependentValues = new ArrayList<>(); + final NodeList valueNodes = dependentValuesNode.getChildNodes(); + for (int i = 0; i < valueNodes.getLength(); i++) { + final Node valueNode = valueNodes.item(i); + if ("dependentValue".equals(valueNode.getNodeName())) { + dependentValues.add(valueNode.getTextContent()); + } + } + + assertEquals(1, dependentValues.size()); + assertEquals("true", dependentValues.getFirst()); + } + + @Test + void testWriteConnectorWithMultipleConfigurationSteps() throws Exception { + final Connector connector = new ConnectorWithMultipleSteps(); + final Document document = writeDocumentation(connector); + + final Node configStepsNode = findNode("/extension/configurationSteps", document); + assertNotNull(configStepsNode); + + final List<String> stepNames = new ArrayList<>(); + final NodeList stepNodes = configStepsNode.getChildNodes(); + for (int i = 0; i < stepNodes.getLength(); i++) { + final Node stepNode = stepNodes.item(i); + if ("configurationStep".equals(stepNode.getNodeName())) { + final Node nameNode = findNode("name", stepNode); + if (nameNode != null) { + stepNames.add(nameNode.getTextContent()); + } + } + } + + assertEquals(List.of("Connection", "Authentication", "Advanced"), stepNames); + } + + @Test + void testWriteConnectorWithIntegerPropertyType() throws Exception { + final Connector connector = new ConnectorWithIntegerProperty(); + final Document document = writeDocumentation(connector); + + final Node propertyTypeNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property/propertyType", document); + assertNotNull(propertyTypeNode); + assertEquals("INTEGER", propertyTypeNode.getTextContent()); + } + + @Test + void testWriteConnectorWithBooleanPropertyType() throws Exception { + final Connector connector = new ConnectorWithBooleanProperty(); + final Document document = writeDocumentation(connector); + + final Node propertyTypeNode = findNode("/extension/configurationSteps/configurationStep/propertyGroups/propertyGroup/properties/property/propertyType", document); + assertNotNull(propertyTypeNode); + assertEquals("BOOLEAN", propertyTypeNode.getTextContent()); + } + + 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 Connector connector, 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(connector.getClass().getName(), nameNode.getTextContent()); + + final Node typeNode = nameNode.getNextSibling(); + assertEquals("type", typeNode.getNodeName()); + assertEquals(expectedExtensionType.name(), typeNode.getTextContent()); + } + + private Document writeDocumentation(final Connector connector) 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 XmlConnectorDocumentationWriter documentationWriter = new XmlConnectorDocumentationWriter(streamWriter); + + try { + documentationWriter.initialize(connector); + documentationWriter.write(connector); + } finally { + streamWriter.close(); + } + + return document; + } + + // Test Connector implementations + + private static class MinimalConnector extends AbstractConnector { + @Override + public VersionedExternalFlow getInitialFlow() { + return null; + } + + @Override + public List<ConfigurationStep> getConfigurationSteps() { + return Collections.emptyList(); + } + + @Override + protected void onStepConfigured(final String stepName, final FlowContext workingContext) { + } + + @Override + public void applyUpdate(final FlowContext workingFlowContext, final FlowContext activeFlowContext) { + } + + @Override + public List<ConfigVerificationResult> verifyConfigurationStep(final String stepName, final Map<String, String> overrides, final FlowContext flowContext) { + return Collections.emptyList(); + } + } + + @CapabilityDescription("A connector with a description") + private static class DescribedConnector extends MinimalConnector { + } + + @Tags({"test", "documentation", "connector"}) + private static class TaggedConnector extends MinimalConnector { + } + + @DeprecationNotice(reason = "Use a different connector") + private static class DeprecatedConnector extends MinimalConnector { + } + + @SeeAlso(classNames = "org.apache.nifi.documentation.xml.XmlConnectorDocumentationWriterTest$MinimalConnector") + private static class SeeAlsoConnector extends MinimalConnector { + } + + private static class ConnectorWithConfigurationSteps extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + return List.of( + new ConfigurationStep.Builder() + .name("Step One") + .description("First configuration step") + .build() + ); + } + } + + private static class ConnectorWithStepWithoutDescription extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + return List.of( + new ConfigurationStep.Builder() + .name("Minimal Step") + .build() + ); + } + } + + private static class ConnectorWithPropertyGroups extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyGroup group = ConnectorPropertyGroup.builder() + .name("Connection Settings") + .description("Settings for connection") + .build(); + + return List.of( + new ConfigurationStep.Builder() + .name("Connection Step") + .propertyGroups(List.of(group)) + .build() + ); + } + } + + private static class ConnectorWithProperties extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyDescriptor property = new ConnectorPropertyDescriptor.Builder() + .name("Hostname") + .description("The hostname to connect to") + .required(true) + .type(PropertyType.STRING) + .build(); + + final ConnectorPropertyGroup group = ConnectorPropertyGroup.builder() + .name("Connection Settings") + .addProperty(property) + .build(); + + return List.of( + new ConfigurationStep.Builder() + .name("Connection Step") + .propertyGroups(List.of(group)) + .build() + ); + } + } + + private static class ConnectorWithPropertyDefaultValue extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyDescriptor property = new ConnectorPropertyDescriptor.Builder() + .name("Hostname") + .description("The hostname to connect to") + .defaultValue("localhost") + .type(PropertyType.STRING) + .build(); + + final ConnectorPropertyGroup group = ConnectorPropertyGroup.builder() + .name("Connection Settings") + .addProperty(property) + .build(); + + return List.of( + new ConfigurationStep.Builder() + .name("Connection Step") + .propertyGroups(List.of(group)) + .build() + ); + } + } + + private static class ConnectorWithAllowableValues extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyDescriptor property = new ConnectorPropertyDescriptor.Builder() + .name("Protocol") + .description("The protocol to use") + .allowableValues( + new AllowableValue("TCP", "TCP", "Transmission Control Protocol"), + new AllowableValue("UDP", "UDP", "User Datagram Protocol") + ) + .type(PropertyType.STRING) + .build(); + + final ConnectorPropertyGroup group = ConnectorPropertyGroup.builder() + .name("Protocol Settings") + .addProperty(property) + .build(); + + return List.of( + new ConfigurationStep.Builder() + .name("Protocol Step") + .propertyGroups(List.of(group)) + .build() + ); + } + } + + private static class ConnectorWithPropertyDependencies extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyDescriptor hostnameProperty = new ConnectorPropertyDescriptor.Builder() + .name("Hostname") + .description("The hostname to connect to") + .type(PropertyType.STRING) + .build(); + + final ConnectorPropertyDescriptor portProperty = new ConnectorPropertyDescriptor.Builder() + .name("Port") + .description("The port to connect to") + .type(PropertyType.INTEGER) + .dependsOn(hostnameProperty) + .build(); + + final ConnectorPropertyGroup group = ConnectorPropertyGroup.builder() + .name("Connection Settings") + .addProperty(hostnameProperty) + .addProperty(portProperty) + .build(); + + return List.of( + new ConfigurationStep.Builder() + .name("Connection Step") + .propertyGroups(List.of(group)) + .build() + ); + } + } + + private static class ConnectorWithStepDependencies extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyDescriptor enableAdvanced = new ConnectorPropertyDescriptor.Builder() + .name("Enable Advanced") + .description("Enable advanced settings") + .type(PropertyType.BOOLEAN) + .build(); + + final ConnectorPropertyGroup group1 = ConnectorPropertyGroup.builder() + .name("Basic Settings") + .addProperty(enableAdvanced) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Step One") + .propertyGroups(List.of(group1)) + .build(); + + final ConnectorPropertyDescriptor advancedProperty = new ConnectorPropertyDescriptor.Builder() + .name("Advanced Setting") + .description("An advanced setting") + .type(PropertyType.STRING) + .build(); + + final ConnectorPropertyGroup group2 = ConnectorPropertyGroup.builder() + .name("Advanced Settings") + .addProperty(advancedProperty) + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Step Two") + .propertyGroups(List.of(group2)) + .dependsOn(step1, enableAdvanced, "true") + .build(); + + return List.of(step1, step2); + } + } + + private static class ConnectorWithFetchableAllowableValues extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyDescriptor property = new ConnectorPropertyDescriptor.Builder() + .name("Dynamic Options") + .description("Options that are fetched dynamically") + .allowableValuesFetchable(true) + .type(PropertyType.STRING) + .build(); + + final ConnectorPropertyGroup group = ConnectorPropertyGroup.builder() + .name("Dynamic Settings") + .addProperty(property) + .build(); + + return List.of( + new ConfigurationStep.Builder() + .name("Dynamic Step") + .propertyGroups(List.of(group)) + .build() + ); + } + } + + private static class ConnectorWithPropertyDependencyValues extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyDescriptor modeProperty = new ConnectorPropertyDescriptor.Builder() + .name("Mode") + .description("The mode of operation") + .type(PropertyType.STRING) + .allowableValues("basic", "advanced", "expert") + .build(); + + final ConnectorPropertyDescriptor advancedProperty = new ConnectorPropertyDescriptor.Builder() + .name("Advanced Setting") + .description("An advanced setting") + .type(PropertyType.STRING) + .dependsOn(modeProperty, "advanced", "expert") + .build(); + + final ConnectorPropertyGroup group = ConnectorPropertyGroup.builder() + .name("Settings") + .addProperty(modeProperty) + .addProperty(advancedProperty) + .build(); + + return List.of( + new ConfigurationStep.Builder() + .name("Settings Step") + .propertyGroups(List.of(group)) + .build() + ); + } + } + + private static class ConnectorWithStepDependencyValues extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyDescriptor enableAdvanced = new ConnectorPropertyDescriptor.Builder() + .name("Enable Advanced") + .description("Enable advanced settings") + .type(PropertyType.BOOLEAN) + .allowableValues("true", "false") + .defaultValue("false") + .build(); + + final ConnectorPropertyGroup group1 = ConnectorPropertyGroup.builder() + .name("Basic Settings") + .addProperty(enableAdvanced) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Step One") + .propertyGroups(List.of(group1)) + .build(); + + final ConnectorPropertyDescriptor advancedProperty = new ConnectorPropertyDescriptor.Builder() + .name("Advanced Setting") + .description("An advanced setting") + .type(PropertyType.STRING) + .build(); + + final ConnectorPropertyGroup group2 = ConnectorPropertyGroup.builder() + .name("Advanced Settings") + .addProperty(advancedProperty) + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Step Two") + .propertyGroups(List.of(group2)) + .dependsOn(step1, enableAdvanced, "true") + .build(); + + return List.of(step1, step2); + } + } + + private static class ConnectorWithMultipleSteps extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyGroup connectionGroup = ConnectorPropertyGroup.builder() + .name("Connection") + .build(); + + final ConnectorPropertyGroup authGroup = ConnectorPropertyGroup.builder() + .name("Auth") + .build(); + + final ConnectorPropertyGroup advancedGroup = ConnectorPropertyGroup.builder() + .name("Options") + .build(); + + return List.of( + new ConfigurationStep.Builder() + .name("Connection") + .description("Connection settings") + .propertyGroups(List.of(connectionGroup)) + .build(), + new ConfigurationStep.Builder() + .name("Authentication") + .description("Authentication settings") + .propertyGroups(List.of(authGroup)) + .build(), + new ConfigurationStep.Builder() + .name("Advanced") + .description("Advanced settings") + .propertyGroups(List.of(advancedGroup)) + .build() + ); + } + } + + private static class ConnectorWithIntegerProperty extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyDescriptor property = new ConnectorPropertyDescriptor.Builder() + .name("Port") + .description("The port number") + .type(PropertyType.INTEGER) + .defaultValue("8080") + .build(); + + final ConnectorPropertyGroup group = ConnectorPropertyGroup.builder() + .name("Connection") + .addProperty(property) + .build(); + + return List.of( + new ConfigurationStep.Builder() + .name("Connection Step") + .propertyGroups(List.of(group)) + .build() + ); + } + } + + private static class ConnectorWithBooleanProperty extends MinimalConnector { + @Override + public List<ConfigurationStep> getConfigurationSteps() { + final ConnectorPropertyDescriptor property = new ConnectorPropertyDescriptor.Builder() + .name("Enabled") + .description("Whether the feature is enabled") + .type(PropertyType.BOOLEAN) + .defaultValue("true") + .build(); + + final ConnectorPropertyGroup group = ConnectorPropertyGroup.builder() + .name("Settings") + .addProperty(property) + .build(); + + return List.of( + new ConfigurationStep.Builder() + .name("Settings Step") + .propertyGroups(List.of(group)) + .build() + ); + } + } +} +
