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 cee255f0a2323996132c901be422d55acd657ae8 Author: Mark Payne <[email protected]> AuthorDate: Fri Dec 19 14:23:39 2025 -0500 NIFI-15369: Allow ConfigurationStep to depend on another (ConfigurationStep,Property) tuple (#39) --- .../org/apache/nifi/components/DescribedValue.java | 17 + .../components/connector/AbstractConnector.java | 75 +++- .../components/connector/ConfigurationStep.java | 88 ++++- .../connector/ConfigurationStepDependency.java | 106 ++++++ .../nifi/components/connector/Connector.java | 3 +- .../connector/TestAbstractConnector.java | 418 ++++++++++++++++++++- .../connector/TestConfigurationStep.java | 325 ++++++++++++++++ 7 files changed, 1025 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apache/nifi/components/DescribedValue.java b/src/main/java/org/apache/nifi/components/DescribedValue.java index a8f1304..b9915a3 100644 --- a/src/main/java/org/apache/nifi/components/DescribedValue.java +++ b/src/main/java/org/apache/nifi/components/DescribedValue.java @@ -35,4 +35,21 @@ public interface DescribedValue { * @return the property description as a string */ String getDescription(); + + DescribedValue NULL = new DescribedValue() { + @Override + public String getValue() { + return null; + } + + @Override + public String getDisplayName() { + return "NULL"; + } + + @Override + public String getDescription() { + return "A null value"; + } + }; } diff --git a/src/main/java/org/apache/nifi/components/connector/AbstractConnector.java b/src/main/java/org/apache/nifi/components/connector/AbstractConnector.java index 6db3acc..49d8059 100644 --- a/src/main/java/org/apache/nifi/components/connector/AbstractConnector.java +++ b/src/main/java/org/apache/nifi/components/connector/AbstractConnector.java @@ -273,7 +273,7 @@ public abstract class AbstractConnector implements Connector { public List<ConfigVerificationResult> verify(final FlowContext flowContext) { final List<ConfigVerificationResult> results = new ArrayList<>(); - final List<ConfigurationStep> configSteps = getConfigurationSteps(flowContext); + final List<ConfigurationStep> configSteps = getConfigurationSteps(); for (final ConfigurationStep configStep : configSteps) { final List<ConfigVerificationResult> stepResults = verifyConfigurationStep(configStep.getName(), Map.of(), flowContext); results.addAll(stepResults); @@ -286,9 +286,14 @@ public abstract class AbstractConnector implements Connector { public List<ValidationResult> validate(final FlowContext flowContext, final ConnectorValidationContext validationContext) { final ConnectorConfigurationContext configContext = flowContext.getConfigurationContext(); final List<ValidationResult> results = new ArrayList<>(); - final List<ConfigurationStep> configurationSteps = getConfigurationSteps(flowContext); + final List<ConfigurationStep> configurationSteps = getConfigurationSteps(); for (final ConfigurationStep configurationStep : configurationSteps) { + if (!isStepDependencySatisfied(configurationStep, configurationSteps, configContext)) { + getLogger().debug("Skipping validation for Configuration Step [{}] because its dependencies are not satisfied", configurationStep.getName()); + continue; + } + results.addAll(validateConfigurationStep(configurationStep, configContext, validationContext)); } @@ -545,6 +550,72 @@ public abstract class AbstractConnector implements Connector { } } + private boolean isStepDependencySatisfied(final ConfigurationStep step, final List<ConfigurationStep> allSteps, + final ConnectorConfigurationContext configContext) { + + final Set<ConfigurationStepDependency> dependencies = step.getDependencies(); + if (dependencies.isEmpty()) { + return true; + } + + for (final ConfigurationStepDependency dependency : dependencies) { + final String dependentStepName = dependency.getStepName(); + final String dependentPropertyName = dependency.getPropertyName(); + + final ConfigurationStep dependentStep = findStepByName(allSteps, dependentStepName); + if (dependentStep == null) { + getLogger().debug("Dependency of step {} is not satisfied because it depends on step {} which could not be found", step.getName(), dependentStepName); + return false; + } + + final ConnectorPropertyDescriptor dependentProperty = findPropertyInStep(dependentStep, dependentPropertyName); + if (dependentProperty == null) { + getLogger().debug("Dependency of step {} is not satisfied because it depends on property {} in step {} which could not be found", + step.getName(), dependentPropertyName, dependentStepName); + return false; + } + + final ConnectorPropertyValue propertyValue = configContext.getProperty(dependentStepName, dependentPropertyName); + final String value = propertyValue == null ? dependentProperty.getDefaultValue() : propertyValue.getValue(); + + final Set<String> dependentValues = dependency.getDependentValues(); + if (dependentValues == null) { + // Dependency is satisfied as long as the property has any value configured. + if (value == null) { + return false; + } + + continue; + } + + if (!dependentValues.contains(value)) { + return false; + } + } + + return true; + } + + private ConfigurationStep findStepByName(final List<ConfigurationStep> steps, final String stepName) { + for (final ConfigurationStep step : steps) { + if (step.getName().equals(stepName)) { + return step; + } + } + return null; + } + + private ConnectorPropertyDescriptor findPropertyInStep(final ConfigurationStep step, final String propertyName) { + for (final ConnectorPropertyGroup group : step.getPropertyGroups()) { + for (final ConnectorPropertyDescriptor descriptor : group.getProperties()) { + if (descriptor.getName().equals(propertyName)) { + return descriptor; + } + } + } + return null; + } + @Override public final void onConfigurationStepConfigured(final String stepName, final FlowContext workingContext) throws FlowUpdateException { onStepConfigured(stepName, workingContext); diff --git a/src/main/java/org/apache/nifi/components/connector/ConfigurationStep.java b/src/main/java/org/apache/nifi/components/connector/ConfigurationStep.java index 6393484..d840b12 100644 --- a/src/main/java/org/apache/nifi/components/connector/ConfigurationStep.java +++ b/src/main/java/org/apache/nifi/components/connector/ConfigurationStep.java @@ -17,21 +17,28 @@ package org.apache.nifi.components.connector; +import org.apache.nifi.components.AllowableValue; +import org.apache.nifi.components.DescribedValue; + import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; public final class ConfigurationStep { private final String name; private final String description; private final List<ConnectorPropertyGroup> propertyGroups; + private final Set<ConfigurationStepDependency> dependencies; private ConfigurationStep(final Builder builder) { this.name = builder.name; this.description = builder.description; this.propertyGroups = Collections.unmodifiableList(builder.propertyGroups); + this.dependencies = Collections.unmodifiableSet(builder.dependencies); } public String getName() { @@ -46,17 +53,25 @@ public final class ConfigurationStep { return propertyGroups; } + /** + * @return the set of dependencies that this step has on other steps' properties + */ + public Set<ConfigurationStepDependency> getDependencies() { + return dependencies; + } + public static final class Builder { private String name; private String description; private List<ConnectorPropertyGroup> propertyGroups = Collections.emptyList(); + private final Set<ConfigurationStepDependency> dependencies = new HashSet<>(); - public Builder name(String name) { + public Builder name(final String name) { this.name = name; return this; } - public Builder description(String description) { + public Builder description(final String description) { this.description = description; return this; } @@ -66,6 +81,75 @@ public final class ConfigurationStep { return this; } + /** + * Sets a dependency on another ConfigurationStep's property having some (any) value configured. + * + * @param step the ConfigurationStep that this step depends on + * @param property the property within the specified step that must have a value + * @return this Builder for method chaining + */ + public Builder dependsOn(final ConfigurationStep step, final ConnectorPropertyDescriptor property) { + dependencies.add(new ConfigurationStepDependency(step.getName(), property.getName())); + return this; + } + + /** + * Sets a dependency on another ConfigurationStep's property having one of the specified values. + * + * @param step the ConfigurationStep that this step depends on + * @param property the property within the specified step that must have one of the specified values + * @param dependentValues the list of values that satisfy this dependency + * @return this Builder for method chaining + */ + public Builder dependsOn(final ConfigurationStep step, final ConnectorPropertyDescriptor property, final List<DescribedValue> dependentValues) { + if (dependentValues == null || dependentValues.isEmpty()) { + dependencies.add(new ConfigurationStepDependency(step.getName(), property.getName())); + } else { + final Set<String> dependentValueSet = dependentValues.stream() + .map(DescribedValue::getValue) + .collect(Collectors.toSet()); + + dependencies.add(new ConfigurationStepDependency(step.getName(), property.getName(), dependentValueSet)); + } + + return this; + } + + /** + * Sets a dependency on another ConfigurationStep's property having one of the specified values. + * + * @param step the ConfigurationStep that this step depends on + * @param property the property within the specified step that must have one of the specified values + * @param firstDependentValue the first value that satisfies this dependency + * @param additionalDependentValues additional values that satisfy this dependency + * @return this Builder for method chaining + */ + public Builder dependsOn(final ConfigurationStep step, final ConnectorPropertyDescriptor property, + final DescribedValue firstDependentValue, final DescribedValue... additionalDependentValues) { + + final List<DescribedValue> dependentValues = new ArrayList<>(); + dependentValues.add(firstDependentValue); + dependentValues.addAll(Arrays.asList(additionalDependentValues)); + return dependsOn(step, property, dependentValues); + } + + /** + * Sets a dependency on another ConfigurationStep's property having one of the specified string values. + * + * @param step the ConfigurationStep that this step depends on + * @param property the property within the specified step that must have one of the specified values + * @param dependentValues the string values that satisfy this dependency + * @return this Builder for method chaining + */ + public Builder dependsOn(final ConfigurationStep step, final ConnectorPropertyDescriptor property, final String... dependentValues) { + final List<DescribedValue> describedValues = Arrays.stream(dependentValues) + .map(AllowableValue::new) + .map(DescribedValue.class::cast) + .toList(); + + return dependsOn(step, property, describedValues); + } + public ConfigurationStep build() { if (name == null) { throw new IllegalStateException("Configuration Step's name must be provided"); diff --git a/src/main/java/org/apache/nifi/components/connector/ConfigurationStepDependency.java b/src/main/java/org/apache/nifi/components/connector/ConfigurationStepDependency.java new file mode 100644 index 0000000..f4e0ff9 --- /dev/null +++ b/src/main/java/org/apache/nifi/components/connector/ConfigurationStepDependency.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.components.connector; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Represents a dependency that a ConfigurationStep has on another ConfigurationStep's property. + * A ConfigurationStep can depend on a property of another step having some value (when dependentValues is null + * and requiresAbsence is false), having one of a specific set of values (when dependentValues is non-null), + * or requiring the property to be absent/null (when requiresAbsence is true). + */ +public final class ConfigurationStepDependency { + private final String stepName; + private final String propertyName; + private final Set<String> dependentValues; + + /** + * Creates a dependency on the specified step and property having one of the specified values. + * + * @param stepName the name of the step that this dependency is on + * @param propertyName the name of the property that this dependency is on + * @param dependentValues the set of values that the property must have; if the property has any + * of these values, the dependency is satisfied + */ + public ConfigurationStepDependency(final String stepName, final String propertyName, final Set<String> dependentValues) { + this.stepName = Objects.requireNonNull(stepName, "Step name is required"); + this.propertyName = Objects.requireNonNull(propertyName, "Property name is required"); + this.dependentValues = dependentValues == null ? null : new HashSet<>(dependentValues); + } + + /** + * Creates a dependency on the specified step and property having some (any) value. + * + * @param stepName the name of the step that this dependency is on + * @param propertyName the name of the property that this dependency is on + */ + public ConfigurationStepDependency(final String stepName, final String propertyName) { + this.stepName = Objects.requireNonNull(stepName, "Step name is required"); + this.propertyName = Objects.requireNonNull(propertyName, "Property name is required"); + this.dependentValues = null; + } + + /** + * @return the name of the ConfigurationStep that this dependency references + */ + public String getStepName() { + return stepName; + } + + /** + * @return the name of the property within the referenced step that this dependency is on + */ + public String getPropertyName() { + return propertyName; + } + + /** + * @return the set of values that the referenced property must have for the dependency to be satisfied, + * or null if the dependency is satisfied by the property having any value (or being absent if requiresAbsence is true) + */ + public Set<String> getDependentValues() { + return dependentValues; + } + + + @Override + public boolean equals(final Object o) { + if (o == null || getClass() != o.getClass()) { + return false; + } + final ConfigurationStepDependency that = (ConfigurationStepDependency) o; + return Objects.equals(stepName, that.stepName) + && Objects.equals(propertyName, that.propertyName) + && Objects.equals(dependentValues, that.dependentValues); + } + + @Override + public int hashCode() { + return Objects.hash(stepName, propertyName, dependentValues); + } + + @Override + public String toString() { + return "ConfigurationStepDependency[stepName=" + stepName + ", propertyName=" + propertyName + + ", dependentValues=" + dependentValues + "]"; + } +} + diff --git a/src/main/java/org/apache/nifi/components/connector/Connector.java b/src/main/java/org/apache/nifi/components/connector/Connector.java index 5de886a..ce88079 100644 --- a/src/main/java/org/apache/nifi/components/connector/Connector.java +++ b/src/main/java/org/apache/nifi/components/connector/Connector.java @@ -150,10 +150,9 @@ public interface Connector { * represents a logical grouping of properties that should be configured together. The order of the steps * in the list represents the order in which the steps should be configured. * - * @param flowContext the flow context that houses the configuration being used to drive the available configuration steps * @return the list of configuration steps */ - List<ConfigurationStep> getConfigurationSteps(FlowContext flowContext); + List<ConfigurationStep> getConfigurationSteps(); /** * Called whenever a specific configuration step has been configured. This allows the Connector to perform any necessary diff --git a/src/test/java/org/apache/nifi/components/connector/TestAbstractConnector.java b/src/test/java/org/apache/nifi/components/connector/TestAbstractConnector.java index c4d96bf..bb77976 100644 --- a/src/test/java/org/apache/nifi/components/connector/TestAbstractConnector.java +++ b/src/test/java/org/apache/nifi/components/connector/TestAbstractConnector.java @@ -25,6 +25,7 @@ import org.apache.nifi.components.Validator; import org.apache.nifi.components.connector.components.FlowContext; import org.apache.nifi.components.connector.components.ProcessGroupFacade; import org.apache.nifi.flow.VersionedExternalFlow; +import org.apache.nifi.logging.ComponentLog; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -73,6 +74,11 @@ public class TestAbstractConnector { when(rootGroupFacade.getControllerServices()).thenReturn(Collections.emptySet()); when(rootGroupFacade.getConnections()).thenReturn(Collections.emptySet()); when(flowContext.getRootGroup()).thenReturn(rootGroupFacade); + + final ConnectorInitializationContext initContext = mock(ConnectorInitializationContext.class); + final ComponentLog logger = mock(ComponentLog.class); + when(initContext.getLogger()).thenReturn(logger); + connector.initialize(initContext); } @Test @@ -430,6 +436,412 @@ public class TestAbstractConnector { assertTrue(connector.isCustomValidateCalled()); } + @Test + void testValidateStepWithUnsatisfiedDependencyIsSkipped() { + final ConnectorPropertyDescriptor enabledProperty = new ConnectorPropertyDescriptor.Builder() + .name("Enabled") + .type(PropertyType.STRING) + .required(false) + .build(); + + final ConnectorPropertyGroup step1Group = ConnectorPropertyGroup.builder() + .name("Settings") + .addProperty(enabledProperty) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Step 1") + .propertyGroups(List.of(step1Group)) + .build(); + + final ConnectorPropertyDescriptor requiredInStep2 = new ConnectorPropertyDescriptor.Builder() + .name("Required When Enabled") + .required(true) + .build(); + + final ConnectorPropertyGroup step2Group = ConnectorPropertyGroup.builder() + .name("Advanced Settings") + .addProperty(requiredInStep2) + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Step 2") + .propertyGroups(List.of(step2Group)) + .dependsOn(step1, enabledProperty, "true") + .build(); + + connector.setConfigurationSteps(List.of(step1, step2)); + + final ConnectorPropertyValue enabledValue = mock(ConnectorPropertyValue.class); + when(enabledValue.getValue()).thenReturn("false"); + when(configurationContext.getProperty("Step 1", "Enabled")).thenReturn(enabledValue); + when(configurationContext.getProperty("Step 2", "Required When Enabled")).thenReturn(null); + + final List<ValidationResult> results = connector.validate(flowContext, validationContext); + + assertTrue(results.isEmpty(), "Step 2 should be skipped because its dependency is not satisfied"); + assertTrue(connector.isCustomValidateCalled()); + } + + @Test + void testValidateStepWithSatisfiedDependencyIsValidated() { + final ConnectorPropertyDescriptor enabledProperty = new ConnectorPropertyDescriptor.Builder() + .name("Enabled") + .type(PropertyType.STRING) + .required(false) + .build(); + + final ConnectorPropertyGroup step1Group = ConnectorPropertyGroup.builder() + .name("Settings") + .addProperty(enabledProperty) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Step 1") + .propertyGroups(List.of(step1Group)) + .build(); + + final ConnectorPropertyDescriptor requiredInStep2 = new ConnectorPropertyDescriptor.Builder() + .name("Required When Enabled") + .required(true) + .build(); + + final ConnectorPropertyGroup step2Group = ConnectorPropertyGroup.builder() + .name("Advanced Settings") + .addProperty(requiredInStep2) + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Step 2") + .propertyGroups(List.of(step2Group)) + .dependsOn(step1, enabledProperty, "true") + .build(); + + connector.setConfigurationSteps(List.of(step1, step2)); + + final ConnectorPropertyValue enabledValue = mock(ConnectorPropertyValue.class); + when(enabledValue.getValue()).thenReturn("true"); + when(configurationContext.getProperty("Step 1", "Enabled")).thenReturn(enabledValue); + when(configurationContext.getProperty("Step 2", "Required When Enabled")).thenReturn(null); + + final List<ValidationResult> results = connector.validate(flowContext, validationContext); + + assertEquals(1, results.size()); + final ValidationResult result = results.getFirst(); + assertFalse(result.isValid()); + assertEquals("Required When Enabled", result.getSubject()); + assertEquals("Required When Enabled is required", result.getExplanation()); + assertFalse(connector.isCustomValidateCalled()); + } + + @Test + void testValidateStepWithDependencyOnAnyValueSatisfied() { + final ConnectorPropertyDescriptor connectionType = new ConnectorPropertyDescriptor.Builder() + .name("Connection Type") + .type(PropertyType.STRING) + .required(false) + .build(); + + final ConnectorPropertyGroup step1Group = ConnectorPropertyGroup.builder() + .name("Connection") + .addProperty(connectionType) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Connection Step") + .propertyGroups(List.of(step1Group)) + .build(); + + final ConnectorPropertyDescriptor requiredProperty = new ConnectorPropertyDescriptor.Builder() + .name("Additional Config") + .required(true) + .build(); + + final ConnectorPropertyGroup step2Group = ConnectorPropertyGroup.builder() + .name("Config") + .addProperty(requiredProperty) + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Additional Step") + .propertyGroups(List.of(step2Group)) + .dependsOn(step1, connectionType) + .build(); + + connector.setConfigurationSteps(List.of(step1, step2)); + + final ConnectorPropertyValue connValue = mock(ConnectorPropertyValue.class); + when(connValue.getValue()).thenReturn("any-value"); + when(configurationContext.getProperty("Connection Step", "Connection Type")).thenReturn(connValue); + when(configurationContext.getProperty("Additional Step", "Additional Config")).thenReturn(null); + + final List<ValidationResult> results = connector.validate(flowContext, validationContext); + + assertEquals(1, results.size()); + assertFalse(results.getFirst().isValid()); + assertEquals("Additional Config", results.getFirst().getSubject()); + } + + @Test + void testValidateStepWithDependencyOnAnyValueNotSatisfiedWhenNull() { + final ConnectorPropertyDescriptor connectionType = new ConnectorPropertyDescriptor.Builder() + .name("Connection Type") + .type(PropertyType.STRING) + .required(false) + .build(); + + final ConnectorPropertyGroup step1Group = ConnectorPropertyGroup.builder() + .name("Connection") + .addProperty(connectionType) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Connection Step") + .propertyGroups(List.of(step1Group)) + .build(); + + final ConnectorPropertyDescriptor requiredProperty = new ConnectorPropertyDescriptor.Builder() + .name("Additional Config") + .required(true) + .build(); + + final ConnectorPropertyGroup step2Group = ConnectorPropertyGroup.builder() + .name("Config") + .addProperty(requiredProperty) + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Additional Step") + .propertyGroups(List.of(step2Group)) + .dependsOn(step1, connectionType) + .build(); + + connector.setConfigurationSteps(List.of(step1, step2)); + + when(configurationContext.getProperty("Connection Step", "Connection Type")).thenReturn(null); + when(configurationContext.getProperty("Additional Step", "Additional Config")).thenReturn(null); + + final List<ValidationResult> results = connector.validate(flowContext, validationContext); + + assertTrue(results.isEmpty(), "Step with dependency should be skipped when dependent property has no value but got validation results: " + results); + assertTrue(connector.isCustomValidateCalled()); + } + + @Test + void testValidateStepWithMultipleDependencies() { + final ConnectorPropertyDescriptor protocol = new ConnectorPropertyDescriptor.Builder() + .name("Protocol") + .type(PropertyType.STRING) + .required(false) + .build(); + + final ConnectorPropertyGroup step1Group = ConnectorPropertyGroup.builder() + .name("Protocol Settings") + .addProperty(protocol) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Protocol Step") + .propertyGroups(List.of(step1Group)) + .build(); + + final ConnectorPropertyDescriptor authEnabled = new ConnectorPropertyDescriptor.Builder() + .name("Auth Enabled") + .type(PropertyType.STRING) + .required(false) + .build(); + + final ConnectorPropertyGroup step2Group = ConnectorPropertyGroup.builder() + .name("Auth Settings") + .addProperty(authEnabled) + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Auth Step") + .propertyGroups(List.of(step2Group)) + .build(); + + final ConnectorPropertyDescriptor credentialsProperty = new ConnectorPropertyDescriptor.Builder() + .name("Credentials") + .required(true) + .build(); + + final ConnectorPropertyGroup step3Group = ConnectorPropertyGroup.builder() + .name("Credentials") + .addProperty(credentialsProperty) + .build(); + + final ConfigurationStep step3 = new ConfigurationStep.Builder() + .name("Credentials Step") + .propertyGroups(List.of(step3Group)) + .dependsOn(step1, protocol, "HTTPS") + .dependsOn(step2, authEnabled, "true") + .build(); + + connector.setConfigurationSteps(List.of(step1, step2, step3)); + + final ConnectorPropertyValue protocolValue = mock(ConnectorPropertyValue.class); + when(protocolValue.getValue()).thenReturn("HTTPS"); + when(configurationContext.getProperty("Protocol Step", "Protocol")).thenReturn(protocolValue); + + final ConnectorPropertyValue authValue = mock(ConnectorPropertyValue.class); + when(authValue.getValue()).thenReturn("false"); + when(configurationContext.getProperty("Auth Step", "Auth Enabled")).thenReturn(authValue); + + final List<ValidationResult> results = connector.validate(flowContext, validationContext); + + assertTrue(results.isEmpty(), "Step 3 should be skipped because auth dependency is not satisfied"); + assertTrue(connector.isCustomValidateCalled()); + + connector.resetCustomValidateCalled(); + when(authValue.getValue()).thenReturn("true"); + + final List<ValidationResult> resultsWithAuthEnabled = connector.validate(flowContext, validationContext); + + assertEquals(1, resultsWithAuthEnabled.size()); + assertFalse(resultsWithAuthEnabled.getFirst().isValid()); + assertEquals("Credentials", resultsWithAuthEnabled.getFirst().getSubject()); + } + + @Test + void testValidateStepWithAbsenceDependencySatisfiedWhenPropertyIsNull() { + final ConnectorPropertyDescriptor customConfig = new ConnectorPropertyDescriptor.Builder() + .name("Custom Config") + .type(PropertyType.STRING) + .required(false) + .build(); + + final ConnectorPropertyGroup step1Group = ConnectorPropertyGroup.builder() + .name("Custom Settings") + .addProperty(customConfig) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Custom Step") + .propertyGroups(List.of(step1Group)) + .build(); + + final ConnectorPropertyDescriptor defaultRequired = new ConnectorPropertyDescriptor.Builder() + .name("Default Required") + .required(true) + .build(); + + final ConnectorPropertyGroup step2Group = ConnectorPropertyGroup.builder() + .name("Default Settings") + .addProperty(defaultRequired) + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Default Step") + .propertyGroups(List.of(step2Group)) + .dependsOn(step1, customConfig, DescribedValue.NULL) + .build(); + + connector.setConfigurationSteps(List.of(step1, step2)); + + when(configurationContext.getProperty("Custom Step", "Custom Config")).thenReturn(null); + when(configurationContext.getProperty("Default Step", "Default Required")).thenReturn(null); + + final List<ValidationResult> results = connector.validate(flowContext, validationContext); + + assertEquals(1, results.size()); + assertFalse(results.getFirst().isValid()); + assertEquals("Default Required", results.getFirst().getSubject()); + assertFalse(connector.isCustomValidateCalled()); + } + + @Test + void testValidateStepWithAbsenceDependencyNotSatisfiedWhenPropertyHasValue() { + final ConnectorPropertyDescriptor customConfig = new ConnectorPropertyDescriptor.Builder() + .name("Custom Config") + .type(PropertyType.STRING) + .required(false) + .build(); + + final ConnectorPropertyGroup step1Group = ConnectorPropertyGroup.builder() + .name("Custom Settings") + .addProperty(customConfig) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Custom Step") + .propertyGroups(List.of(step1Group)) + .build(); + + final ConnectorPropertyDescriptor defaultRequired = new ConnectorPropertyDescriptor.Builder() + .name("Default Required") + .required(true) + .build(); + + final ConnectorPropertyGroup step2Group = ConnectorPropertyGroup.builder() + .name("Default Settings") + .addProperty(defaultRequired) + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Default Step") + .propertyGroups(List.of(step2Group)) + .dependsOn(step1, customConfig, DescribedValue.NULL) + .build(); + + connector.setConfigurationSteps(List.of(step1, step2)); + + final ConnectorPropertyValue customValue = mock(ConnectorPropertyValue.class); + when(customValue.getValue()).thenReturn("some-custom-value"); + when(configurationContext.getProperty("Custom Step", "Custom Config")).thenReturn(customValue); + when(configurationContext.getProperty("Default Step", "Default Required")).thenReturn(null); + + final List<ValidationResult> results = connector.validate(flowContext, validationContext); + + assertTrue(results.isEmpty(), "Default Step should be skipped because Custom Config has a value"); + assertTrue(connector.isCustomValidateCalled()); + } + + @Test + void testValidateStepWithAbsenceDependencyUsesDefaultValue() { + final ConnectorPropertyDescriptor propertyWithDefault = new ConnectorPropertyDescriptor.Builder() + .name("Property With Default") + .type(PropertyType.STRING) + .defaultValue("default-value") + .required(false) + .build(); + + final ConnectorPropertyGroup step1Group = ConnectorPropertyGroup.builder() + .name("Settings") + .addProperty(propertyWithDefault) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Step 1") + .propertyGroups(List.of(step1Group)) + .build(); + + final ConnectorPropertyDescriptor requiredProperty = new ConnectorPropertyDescriptor.Builder() + .name("Required Property") + .required(true) + .build(); + + final ConnectorPropertyGroup step2Group = ConnectorPropertyGroup.builder() + .name("Dependent Settings") + .addProperty(requiredProperty) + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Step 2") + .propertyGroups(List.of(step2Group)) + .dependsOn(step1, propertyWithDefault, DescribedValue.NULL) + .build(); + + connector.setConfigurationSteps(List.of(step1, step2)); + + when(configurationContext.getProperty("Step 1", "Property With Default")).thenReturn(null); + + final List<ValidationResult> results = connector.validate(flowContext, validationContext); + + assertTrue(results.isEmpty(), "Step 2 should be skipped because Property With Default has a default value"); + assertTrue(connector.isCustomValidateCalled()); + } + private static class TestableAbstractConnector extends AbstractConnector { private List<ConfigurationStep> configurationSteps = Collections.emptyList(); private Collection<ValidationResult> customValidationResults = Collections.emptyList(); @@ -447,13 +859,17 @@ public class TestAbstractConnector { return customValidateCalled; } + public void resetCustomValidateCalled() { + this.customValidateCalled = false; + } + @Override public VersionedExternalFlow getInitialFlow() { return null; } @Override - public List<ConfigurationStep> getConfigurationSteps(final FlowContext workingContext) { + public List<ConfigurationStep> getConfigurationSteps() { return configurationSteps; } diff --git a/src/test/java/org/apache/nifi/components/connector/TestConfigurationStep.java b/src/test/java/org/apache/nifi/components/connector/TestConfigurationStep.java new file mode 100644 index 0000000..c32ce43 --- /dev/null +++ b/src/test/java/org/apache/nifi/components/connector/TestConfigurationStep.java @@ -0,0 +1,325 @@ +/* + * 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.components.connector; + +import org.apache.nifi.components.AllowableValue; +import org.apache.nifi.components.DescribedValue; +import org.junit.jupiter.api.Test; + +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; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestConfigurationStep { + + @Test + void testDependsOnWithStepAndProperty() { + final ConnectorPropertyDescriptor connectionTypeProperty = new ConnectorPropertyDescriptor.Builder() + .name("Connection Type") + .type(PropertyType.STRING) + .build(); + + final ConfigurationStep connectionStep = new ConfigurationStep.Builder() + .name("Connection") + .description("Connection configuration") + .build(); + + final ConfigurationStep authenticationStep = new ConfigurationStep.Builder() + .name("Authentication") + .description("Authentication configuration") + .dependsOn(connectionStep, connectionTypeProperty) + .build(); + + final Set<ConfigurationStepDependency> dependencies = authenticationStep.getDependencies(); + assertEquals(1, dependencies.size()); + + final ConfigurationStepDependency dependency = dependencies.iterator().next(); + assertEquals("Connection", dependency.getStepName()); + assertEquals("Connection Type", dependency.getPropertyName()); + assertNull(dependency.getDependentValues()); + } + + @Test + void testDependsOnWithSpecificValues() { + final ConnectorPropertyDescriptor connectionTypeProperty = new ConnectorPropertyDescriptor.Builder() + .name("Connection Type") + .type(PropertyType.STRING) + .allowableValues("HTTP", "HTTPS", "FTP") + .build(); + + final ConfigurationStep connectionStep = new ConfigurationStep.Builder() + .name("Connection") + .description("Connection configuration") + .build(); + + final ConfigurationStep tlsStep = new ConfigurationStep.Builder() + .name("TLS Configuration") + .description("TLS settings") + .dependsOn(connectionStep, connectionTypeProperty, new AllowableValue("HTTPS")) + .build(); + + final Set<ConfigurationStepDependency> dependencies = tlsStep.getDependencies(); + assertEquals(1, dependencies.size()); + + final ConfigurationStepDependency dependency = dependencies.iterator().next(); + assertEquals("Connection", dependency.getStepName()); + assertEquals("Connection Type", dependency.getPropertyName()); + assertNotNull(dependency.getDependentValues()); + assertEquals(Set.of("HTTPS"), dependency.getDependentValues()); + } + + @Test + void testDependsOnWithMultipleDescribedValues() { + final ConnectorPropertyDescriptor authTypeProperty = new ConnectorPropertyDescriptor.Builder() + .name("Auth Type") + .type(PropertyType.STRING) + .allowableValues("NONE", "BASIC", "OAUTH", "API_KEY") + .build(); + + final ConfigurationStep authStep = new ConfigurationStep.Builder() + .name("Authentication") + .build(); + + final ConfigurationStep credentialsStep = new ConfigurationStep.Builder() + .name("Credentials") + .description("Credential configuration") + .dependsOn(authStep, authTypeProperty, new AllowableValue("BASIC"), new AllowableValue("OAUTH"), new AllowableValue("API_KEY")) + .build(); + + final Set<ConfigurationStepDependency> dependencies = credentialsStep.getDependencies(); + assertEquals(1, dependencies.size()); + + final ConfigurationStepDependency dependency = dependencies.iterator().next(); + assertEquals("Authentication", dependency.getStepName()); + assertEquals("Auth Type", dependency.getPropertyName()); + assertNotNull(dependency.getDependentValues()); + assertEquals(Set.of("BASIC", "OAUTH", "API_KEY"), dependency.getDependentValues()); + } + + @Test + void testDependsOnWithStringValues() { + final ConnectorPropertyDescriptor protocolProperty = new ConnectorPropertyDescriptor.Builder() + .name("Protocol") + .type(PropertyType.STRING) + .build(); + + final ConfigurationStep connectionStep = new ConfigurationStep.Builder() + .name("Connection") + .build(); + + final ConfigurationStep securityStep = new ConfigurationStep.Builder() + .name("Security") + .dependsOn(connectionStep, protocolProperty, "HTTPS", "SFTP") + .build(); + + final Set<ConfigurationStepDependency> dependencies = securityStep.getDependencies(); + assertEquals(1, dependencies.size()); + + final ConfigurationStepDependency dependency = dependencies.iterator().next(); + assertEquals("Connection", dependency.getStepName()); + assertEquals("Protocol", dependency.getPropertyName()); + assertNotNull(dependency.getDependentValues()); + assertEquals(Set.of("HTTPS", "SFTP"), dependency.getDependentValues()); + } + + @Test + void testDependsOnWithDescribedValueList() { + final ConnectorPropertyDescriptor modeProperty = new ConnectorPropertyDescriptor.Builder() + .name("Mode") + .type(PropertyType.STRING) + .build(); + + final ConfigurationStep setupStep = new ConfigurationStep.Builder() + .name("Setup") + .build(); + + final ConfigurationStep advancedStep = new ConfigurationStep.Builder() + .name("Advanced") + .dependsOn(setupStep, modeProperty, List.of(new AllowableValue("advanced"), new AllowableValue("expert"))) + .build(); + + final Set<ConfigurationStepDependency> dependencies = advancedStep.getDependencies(); + assertEquals(1, dependencies.size()); + + final ConfigurationStepDependency dependency = dependencies.iterator().next(); + assertEquals("Setup", dependency.getStepName()); + assertEquals("Mode", dependency.getPropertyName()); + assertEquals(Set.of("advanced", "expert"), dependency.getDependentValues()); + } + + @Test + void testMultipleDependencies() { + final ConnectorPropertyDescriptor enabledProperty = new ConnectorPropertyDescriptor.Builder() + .name("Enabled") + .type(PropertyType.BOOLEAN) + .build(); + + final ConnectorPropertyDescriptor modeProperty = new ConnectorPropertyDescriptor.Builder() + .name("Mode") + .type(PropertyType.STRING) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Step 1") + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Step 2") + .build(); + + final ConfigurationStep step3 = new ConfigurationStep.Builder() + .name("Step 3") + .dependsOn(step1, enabledProperty, "true") + .dependsOn(step2, modeProperty, "advanced") + .build(); + + final Set<ConfigurationStepDependency> dependencies = step3.getDependencies(); + assertEquals(2, dependencies.size()); + + boolean foundStep1Dependency = false; + boolean foundStep2Dependency = false; + for (final ConfigurationStepDependency dependency : dependencies) { + if ("Step 1".equals(dependency.getStepName())) { + assertEquals("Enabled", dependency.getPropertyName()); + assertEquals(Set.of("true"), dependency.getDependentValues()); + foundStep1Dependency = true; + } else if ("Step 2".equals(dependency.getStepName())) { + assertEquals("Mode", dependency.getPropertyName()); + assertEquals(Set.of("advanced"), dependency.getDependentValues()); + foundStep2Dependency = true; + } + } + + assertTrue(foundStep1Dependency, "Expected dependency on Step 1 not found"); + assertTrue(foundStep2Dependency, "Expected dependency on Step 2 not found"); + } + + @Test + void testNoDependencies() { + final ConfigurationStep step = new ConfigurationStep.Builder() + .name("Standalone Step") + .description("A step with no dependencies") + .build(); + + assertNotNull(step.getDependencies()); + assertTrue(step.getDependencies().isEmpty()); + } + + @Test + void testDependsOnWithEmptyValueListTreatedAsAnyValue() { + final ConnectorPropertyDescriptor property = new ConnectorPropertyDescriptor.Builder() + .name("Some Property") + .type(PropertyType.STRING) + .build(); + + final ConfigurationStep step1 = new ConfigurationStep.Builder() + .name("Step 1") + .build(); + + final ConfigurationStep step2 = new ConfigurationStep.Builder() + .name("Step 2") + .dependsOn(step1, property, List.of()) + .build(); + + final Set<ConfigurationStepDependency> dependencies = step2.getDependencies(); + assertEquals(1, dependencies.size()); + + final ConfigurationStepDependency dependency = dependencies.iterator().next(); + assertNull(dependency.getDependentValues()); + } + + @Test + void testDependsOnAbsence() { + final ConnectorPropertyDescriptor optionalProperty = new ConnectorPropertyDescriptor.Builder() + .name("Optional Feature") + .type(PropertyType.STRING) + .build(); + + final ConfigurationStep featureStep = new ConfigurationStep.Builder() + .name("Feature Configuration") + .build(); + + final ConfigurationStep alternativeStep = new ConfigurationStep.Builder() + .name("Alternative Configuration") + .description("Only shown when optional feature is not configured") + .dependsOn(featureStep, optionalProperty, DescribedValue.NULL) + .build(); + + final Set<ConfigurationStepDependency> dependencies = alternativeStep.getDependencies(); + assertEquals(1, dependencies.size()); + + final ConfigurationStepDependency dependency = dependencies.iterator().next(); + assertEquals("Feature Configuration", dependency.getStepName()); + assertEquals("Optional Feature", dependency.getPropertyName()); + assertNotNull(dependency.getDependentValues()); + assertTrue(dependency.getDependentValues().contains(null)); + } + + @Test + void testDependsOnAbsenceWithRegularDependency() { + final ConnectorPropertyDescriptor modeProperty = new ConnectorPropertyDescriptor.Builder() + .name("Mode") + .type(PropertyType.STRING) + .build(); + + final ConnectorPropertyDescriptor customProperty = new ConnectorPropertyDescriptor.Builder() + .name("Custom Setting") + .type(PropertyType.STRING) + .build(); + + final ConfigurationStep modeStep = new ConfigurationStep.Builder() + .name("Mode Selection") + .build(); + + final ConfigurationStep customStep = new ConfigurationStep.Builder() + .name("Custom Configuration") + .build(); + + final ConfigurationStep defaultStep = new ConfigurationStep.Builder() + .name("Default Configuration") + .dependsOn(modeStep, modeProperty, "default") + .dependsOn(customStep, customProperty, DescribedValue.NULL) + .build(); + + final Set<ConfigurationStepDependency> dependencies = defaultStep.getDependencies(); + assertEquals(2, dependencies.size()); + + boolean foundModeDependency = false; + boolean foundAbsenceDependency = false; + for (final ConfigurationStepDependency dependency : dependencies) { + if ("Mode Selection".equals(dependency.getStepName())) { + assertEquals("Mode", dependency.getPropertyName()); + assertEquals(Set.of("default"), dependency.getDependentValues()); + foundModeDependency = true; + } else if ("Custom Configuration".equals(dependency.getStepName())) { + assertEquals("Custom Setting", dependency.getPropertyName()); + assertNotNull(dependency.getDependentValues()); + assertTrue(dependency.getDependentValues().contains(null)); + foundAbsenceDependency = true; + } + } + + assertTrue(foundModeDependency, "Expected mode dependency not found"); + assertTrue(foundAbsenceDependency, "Expected absence dependency not found"); + } +} +
