This is an automated email from the ASF dual-hosted git repository.
mcgilman pushed a commit to branch NIFI-15258
in repository https://gitbox.apache.org/repos/asf/nifi-api.git
The following commit(s) were added to refs/heads/NIFI-15258 by this push:
new ce524ac NIFI-15369: Allow ConfigurationStep to depend on another
(ConfigurationStep,Property) tuple (#39)
ce524ac is described below
commit ce524ac1555231d3b2047d69a2b2cfc56090a13d
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 | 19 +-
.../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, 1026 insertions(+), 8 deletions(-)
diff --git a/src/main/java/org/apache/nifi/components/DescribedValue.java
b/src/main/java/org/apache/nifi/components/DescribedValue.java
index 87ffc2f..b9915a3 100644
--- a/src/main/java/org/apache/nifi/components/DescribedValue.java
+++ b/src/main/java/org/apache/nifi/components/DescribedValue.java
@@ -32,7 +32,24 @@ public interface DescribedValue {
String getDisplayName();
/**
- * @return the proeprty description as a string
+ * @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");
+ }
+}
+