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");
+    }
+}
+

Reply via email to