From a6e90106d060249639e69e75c1268064964048c8 Mon Sep 17 00:00:00 2001 From: Pierre Villard Date: Sun, 12 Apr 2026 12:09:44 +0200 Subject: [PATCH] NIFI-15829 - Support parameter value references to provided parameters from inherited parameter contexts --- .../nifi/groups/StandardProcessGroup.java | 45 +++- .../parameter/StandardParameterContext.java | 105 ++++++-- .../TestStandardParameterContext.java | 236 +++++++++++++++++- .../nifi/parameter/ParameterContext.java | 18 ++ .../nifi/web/StandardNiFiServiceFacade.java | 30 ++- .../system/parameters/ParameterContextIT.java | 59 +++++ 6 files changed, 474 insertions(+), 19 deletions(-) diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java index 1dda6c268432..2a6ee514aac4 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/groups/StandardProcessGroup.java @@ -3320,13 +3320,54 @@ public void setParameterContext(final ParameterContext parameterContext) { public void onParameterContextUpdated(final Map updatedParameters) { readLock.lock(); try { - getProcessors().forEach(proc -> proc.onParametersModified(updatedParameters)); - getControllerServices(false).forEach(cs -> cs.onParametersModified(updatedParameters)); + final Map effectiveUpdates = augmentWithParameterValueReferences(updatedParameters); + getProcessors().forEach(proc -> proc.onParametersModified(effectiveUpdates)); + getControllerServices(false).forEach(cs -> cs.onParametersModified(effectiveUpdates)); } finally { readLock.unlock(); } } + /** + * Augments the given parameter update map with entries for local parameters whose values are + * one-to-one references to changed parameters. For example, if this group's context defines + * parameter X with value {@code #{db_host}} and db_host is in the update map, then X is added + * to the augmented map with the same old/new values, allowing components referencing X to be + * properly notified of the change. + */ + private Map augmentWithParameterValueReferences(final Map updatedParameters) { + final ParameterContext context = getParameterContext(); + if (context == null) { + return updatedParameters; + } + + Map augmented = null; + for (final Map.Entry entry : context.getParameters().entrySet()) { + final Parameter localParam = entry.getValue(); + final String referencedName = ParameterContext.extractOneToOneParameterReference(localParam.getValue()); + if (referencedName == null) { + continue; + } + + final Optional referencedParam = context.getParameter(referencedName); + if (referencedParam.isEmpty() || !referencedParam.get().isProvided()) { + continue; + } + + final ParameterUpdate referencedUpdate = updatedParameters.get(referencedName); + if (referencedUpdate != null && localParam.getDescriptor().isSensitive() == referencedUpdate.isSensitive()) { + if (augmented == null) { + augmented = new HashMap<>(updatedParameters); + } + augmented.put(localParam.getDescriptor().getName(), + new StandardParameterUpdate(localParam.getDescriptor().getName(), + referencedUpdate.getPreviousValue(), referencedUpdate.getUpdatedValue(), + localParam.getDescriptor().isSensitive())); + } + } + return augmented != null ? augmented : updatedParameters; + } + private Map mapParameterUpdates(final ParameterContext previousParameterContext, final ParameterContext updatedParameterContext) { if (previousParameterContext == null && updatedParameterContext == null) { return Collections.emptyMap(); diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java index d5445d2a8c2e..c464fb39d9d2 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java @@ -346,7 +346,9 @@ public Map getParameters() { public Map getEffectiveParameters() { readLock.lock(); try { - return this.getEffectiveParameters(inheritedParameterContexts); + final Map effective = getMergedEffectiveParameters(inheritedParameterContexts, this.parameters); + resolveParameterValueReferences(effective); + return effective; } finally { readLock.unlock(); } @@ -358,7 +360,8 @@ public Map getEffectiveParameterUpdates(final Map currentEffectiveParameters = getEffectiveParameters(); - final Map effectiveProposedParameters = getEffectiveParameters(inheritedParameterContexts, getProposedParameters(parameterUpdates)); + final Map effectiveProposedParameters = getMergedEffectiveParameters(inheritedParameterContexts, getProposedParameters(parameterUpdates)); + resolveParameterValueReferences(effectiveProposedParameters); return getEffectiveParameterUpdates(currentEffectiveParameters, effectiveProposedParameters); } @@ -370,7 +373,9 @@ public Map getEffectiveParameterUpdates(final Map getEffectiveParameters(final Map proposedParameters) { - return getEffectiveParameters(this.inheritedParameterContexts, proposedParameters); + final Map effective = getMergedEffectiveParameters(this.inheritedParameterContexts, proposedParameters); + resolveParameterValueReferences(effective); + return effective; } /** @@ -380,31 +385,103 @@ private Map getEffectiveParameters(final Map getEffectiveParameters(final List parameterContexts) { - return getEffectiveParameters(parameterContexts, this.parameters); + final Map effective = getMergedEffectiveParameters(parameterContexts, this.parameters); + resolveParameterValueReferences(effective); + return effective; } - private Map getEffectiveParameters(final List parameterContexts, - final Map proposedParameters) { - return getEffectiveParameters(parameterContexts, proposedParameters, new HashMap<>()); + private Map getMergedEffectiveParameters(final List parameterContexts, + final Map proposedParameters) { + return getMergedEffectiveParameters(parameterContexts, proposedParameters, new HashMap<>()); } - private Map getEffectiveParameters(final List parameterContexts, - final Map proposedParameters, - final Map> allOverrides) { + /** + * Merges parameters from inherited contexts with the proposed (local) parameters, applying + * override priority. Does NOT resolve parameter value references -- callers that need resolved + * values must call {@link #resolveParameterValueReferences} on the result. + */ + private Map getMergedEffectiveParameters(final List parameterContexts, + final Map proposedParameters, + final Map> allOverrides) { final Map effectiveParameters = new LinkedHashMap<>(); - // Loop backwards so that the first ParameterContext in the list will override any parameters later in the list for (int i = parameterContexts.size() - 1; i >= 0; i--) { - ParameterContext parameterContext = parameterContexts.get(i); - combineOverrides(allOverrides, overrideParameters(effectiveParameters, parameterContext.getEffectiveParameters(), parameterContext)); + final ParameterContext parameterContext = parameterContexts.get(i); + final Map inheritedParameters = getUnresolvedEffectiveParameters(parameterContext); + combineOverrides(allOverrides, overrideParameters(effectiveParameters, inheritedParameters, parameterContext)); } - // Finally, override all child parameters with our own combineOverrides(allOverrides, overrideParameters(effectiveParameters, proposedParameters, this)); return effectiveParameters; } + /** + * Returns the merged effective parameters from a context without applying parameter value + * reference resolution. For StandardParameterContext instances this avoids double-resolution + * when building a parent context's effective parameter set. + */ + private static Map getUnresolvedEffectiveParameters(final ParameterContext parameterContext) { + if (parameterContext instanceof StandardParameterContext standardContext) { + return standardContext.getMergedEffectiveParametersReadLocked(); + } + return parameterContext.getEffectiveParameters(); + } + + private Map getMergedEffectiveParametersReadLocked() { + readLock.lock(); + try { + return getMergedEffectiveParameters(inheritedParameterContexts, this.parameters); + } finally { + readLock.unlock(); + } + } + + /** + * Resolves one-to-one parameter value references within the effective parameter map. + * If a parameter's entire value is exactly {@code #{referencedName}}, and the referenced parameter + * exists in the effective map, is provided by a parameter provider, and has matching sensitivity, + * the value is replaced with the referenced parameter's value. Only a single level of resolution + * is performed (no chaining): the lookup uses a snapshot of the pre-resolution values so that + * transitive references are not followed. + * + * @param effectiveParameters the effective parameter map to resolve in place + */ + private void resolveParameterValueReferences(final Map effectiveParameters) { + final Map originalParametersByName = new HashMap<>(); + for (final Map.Entry entry : effectiveParameters.entrySet()) { + originalParametersByName.put(entry.getKey().getName(), entry.getValue()); + } + + for (final Map.Entry entry : effectiveParameters.entrySet()) { + final ParameterDescriptor descriptor = entry.getKey(); + final Parameter parameter = entry.getValue(); + final String referencedName = ParameterContext.extractOneToOneParameterReference(parameter.getValue()); + if (referencedName == null) { + continue; + } + + final Parameter referencedParameter = originalParametersByName.get(referencedName); + if (referencedParameter == null) { + continue; + } + + if (!referencedParameter.isProvided()) { + continue; + } + + if (descriptor.isSensitive() != referencedParameter.getDescriptor().isSensitive()) { + continue; + } + + final Parameter resolvedParameter = new Parameter.Builder() + .fromParameter(parameter) + .value(referencedParameter.getValue()) + .build(); + entry.setValue(resolvedParameter); + } + } + private void combineOverrides(final Map> existingOverrides, final Map> newOverrides) { for (final Map.Entry> entry : newOverrides.entrySet()) { final ParameterDescriptor key = entry.getKey(); diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java index 31a679301828..94302ecec000 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java @@ -844,12 +844,25 @@ private static ParameterDescriptor addParameter(final ParameterContext parameter } private static ParameterDescriptor addParameter(final ParameterContext parameterContext, final String name, final String value, final boolean isSensitive) { + return addParameter(parameterContext, name, value, isSensitive, false); + } + + private static ParameterDescriptor addProvidedParameter(final ParameterContext parameterContext, final String name, final String value) { + return addParameter(parameterContext, name, value, false, true); + } + + private static ParameterDescriptor addProvidedParameter(final ParameterContext parameterContext, final String name, final String value, final boolean isSensitive) { + return addParameter(parameterContext, name, value, isSensitive, true); + } + + private static ParameterDescriptor addParameter(final ParameterContext parameterContext, final String name, final String value, + final boolean isSensitive, final boolean isProvided) { final Map parameters = new HashMap<>(); for (final Map.Entry entry : parameterContext.getParameters().entrySet()) { parameters.put(entry.getKey().getName(), entry.getValue()); } final ParameterDescriptor parameterDescriptor = new ParameterDescriptor.Builder().name(name).sensitive(isSensitive).build(); - parameters.put(name, createParameter(parameterDescriptor, value)); + parameters.put(name, createParameter(parameterDescriptor, value, isProvided)); parameterContext.setParameters(parameters); return parameterDescriptor; } @@ -868,6 +881,227 @@ private static ParameterContext createParameterContext(final String id, final Pa return parameterContext; } + @Test + public void testExtractOneToOneParameterReference() { + assertEquals("db_host", ParameterContext.extractOneToOneParameterReference("#{db_host}")); + assertEquals("x", ParameterContext.extractOneToOneParameterReference("#{x}")); + assertEquals("a-b_c.d", ParameterContext.extractOneToOneParameterReference("#{a-b_c.d}")); + + assertNull(ParameterContext.extractOneToOneParameterReference(null)); + assertNull(ParameterContext.extractOneToOneParameterReference("")); + assertNull(ParameterContext.extractOneToOneParameterReference("abc")); + assertNull(ParameterContext.extractOneToOneParameterReference("#{")); + assertNull(ParameterContext.extractOneToOneParameterReference("#{}")); + assertNull(ParameterContext.extractOneToOneParameterReference("prefix#{db_host}")); + assertNull(ParameterContext.extractOneToOneParameterReference("#{db_host}suffix")); + assertNull(ParameterContext.extractOneToOneParameterReference("#{a}#{b}")); + assertNull(ParameterContext.extractOneToOneParameterReference("#{db_host}:3306")); + } + + @Test + public void testParameterValueReferenceResolution() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext s = createParameterContext("s", parameterContextLookup); + addProvidedParameter(s, "db_host", "myserver.example.com"); + addProvidedParameter(s, "db_port", "3306"); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "host", "#{db_host}"); + addParameter(p, "port", "#{db_port}"); + addParameter(p, "plain", "literal_value"); + + p.setInheritedParameterContexts(List.of(s)); + + final Map effective = p.getEffectiveParameters(); + assertEquals("myserver.example.com", effective.get(new ParameterDescriptor.Builder().name("host").build()).getValue()); + assertEquals("3306", effective.get(new ParameterDescriptor.Builder().name("port").build()).getValue()); + assertEquals("literal_value", effective.get(new ParameterDescriptor.Builder().name("plain").build()).getValue()); + assertEquals("myserver.example.com", effective.get(new ParameterDescriptor.Builder().name("db_host").build()).getValue()); + } + + @Test + public void testParameterValueReferenceNotResolvedIfMixed() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext s = createParameterContext("s", parameterContextLookup); + addProvidedParameter(s, "db_host", "myserver.example.com"); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "url", "jdbc://#{db_host}:3306"); + + p.setInheritedParameterContexts(List.of(s)); + + final Map effective = p.getEffectiveParameters(); + assertEquals("jdbc://#{db_host}:3306", effective.get(new ParameterDescriptor.Builder().name("url").build()).getValue()); + } + + @Test + public void testParameterValueReferenceNotResolvedIfMultipleRefs() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext s = createParameterContext("s", parameterContextLookup); + addProvidedParameter(s, "a", "valueA"); + addProvidedParameter(s, "b", "valueB"); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "combined", "#{a}#{b}"); + + p.setInheritedParameterContexts(List.of(s)); + + final Map effective = p.getEffectiveParameters(); + assertEquals("#{a}#{b}", effective.get(new ParameterDescriptor.Builder().name("combined").build()).getValue()); + } + + @Test + public void testParameterValueReferenceNoChaining() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext s = createParameterContext("s", parameterContextLookup); + addProvidedParameter(s, "y", "#{z}"); + addProvidedParameter(s, "z", "final_value"); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "x", "#{y}"); + + p.setInheritedParameterContexts(List.of(s)); + + final Map effective = p.getEffectiveParameters(); + // x references y, whose value is "#{z}" -- no chaining, so x resolves to "#{z}" literally + assertEquals("#{z}", effective.get(new ParameterDescriptor.Builder().name("x").build()).getValue()); + } + + @Test + public void testParameterValueReferenceSensitivityMatchResolves() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext s = createParameterContext("s", parameterContextLookup); + addProvidedParameter(s, "secret_value", "my_secret", true); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "password", "#{secret_value}", true); + + p.setInheritedParameterContexts(List.of(s)); + + final Map effective = p.getEffectiveParameters(); + assertEquals("my_secret", effective.get(new ParameterDescriptor.Builder().name("password").sensitive(true).build()).getValue()); + } + + @Test + public void testParameterValueReferenceSensitivityMismatchNotResolved() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext s = createParameterContext("s", parameterContextLookup); + addProvidedParameter(s, "secret_value", "my_secret", true); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "not_sensitive", "#{secret_value}"); + + p.setInheritedParameterContexts(List.of(s)); + + final Map effective = p.getEffectiveParameters(); + assertEquals("#{secret_value}", effective.get(new ParameterDescriptor.Builder().name("not_sensitive").build()).getValue()); + } + + @Test + public void testParameterValueReferenceSensitivityMismatchSensitiveRefNonSensitive() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext s = createParameterContext("s", parameterContextLookup); + addProvidedParameter(s, "plain_value", "not_a_secret"); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "sensitive_param", "#{plain_value}", true); + + p.setInheritedParameterContexts(List.of(s)); + + final Map effective = p.getEffectiveParameters(); + assertEquals("#{plain_value}", effective.get(new ParameterDescriptor.Builder().name("sensitive_param").sensitive(true).build()).getValue()); + } + + @Test + public void testParameterValueReferenceNonExistent() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "ref", "#{nonexistent}"); + + final Map effective = p.getEffectiveParameters(); + assertEquals("#{nonexistent}", effective.get(new ParameterDescriptor.Builder().name("ref").build()).getValue()); + } + + @Test + public void testParameterValueReferenceSelfReference() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "x", "#{x}"); + + final Map effective = p.getEffectiveParameters(); + // Self-reference: x's value is "#{x}", extractOneToOneParameterReference gives "x", + // the referenced parameter is x itself with value "#{x}". One level of resolution + // replaces x with "#{x}" (the referenced parameter's value), so it stays as "#{x}". + assertEquals("#{x}", effective.get(new ParameterDescriptor.Builder().name("x").build()).getValue()); + } + + @Test + public void testGetParametersReturnsRawValues() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext s = createParameterContext("s", parameterContextLookup); + addProvidedParameter(s, "db_host", "myserver.example.com"); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "host", "#{db_host}"); + + p.setInheritedParameterContexts(List.of(s)); + + // getParameters() returns only local parameters with raw (unresolved) values + final Map raw = p.getParameters(); + assertEquals(1, raw.size()); + assertEquals("#{db_host}", raw.get(new ParameterDescriptor.Builder().name("host").build()).getValue()); + } + + @Test + public void testParameterValueReferenceSameContextProvidedResolution() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addProvidedParameter(p, "source", "resolved_value"); + addParameter(p, "ref", "#{source}"); + + final Map effective = p.getEffectiveParameters(); + assertEquals("resolved_value", effective.get(new ParameterDescriptor.Builder().name("ref").build()).getValue()); + } + + @Test + public void testParameterValueReferenceNotResolvedIfNotProvided() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext s = createParameterContext("s", parameterContextLookup); + addParameter(s, "db_host", "myserver.example.com"); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "host", "#{db_host}"); + + p.setInheritedParameterContexts(List.of(s)); + + final Map effective = p.getEffectiveParameters(); + assertEquals("#{db_host}", effective.get(new ParameterDescriptor.Builder().name("host").build()).getValue()); + } + + @Test + public void testParameterValueReferenceSameContextNonProvidedNotResolved() { + final StandardParameterContextManager parameterContextLookup = new StandardParameterContextManager(); + + final ParameterContext p = createParameterContext("p", parameterContextLookup); + addParameter(p, "source", "some_value"); + addParameter(p, "ref", "#{source}"); + + final Map effective = p.getEffectiveParameters(); + assertEquals("#{source}", effective.get(new ParameterDescriptor.Builder().name("ref").build()).getValue()); + } + private static class HashMapParameterReferenceManager implements ParameterReferenceManager { private final Map processors = new HashMap<>(); private final Map controllerServices = new HashMap<>(); diff --git a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/parameter/ParameterContext.java b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/parameter/ParameterContext.java index 7323db1718ff..005e068a5a8a 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/parameter/ParameterContext.java +++ b/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/parameter/ParameterContext.java @@ -188,4 +188,22 @@ public interface ParameterContext extends ParameterLookup, ComponentAuthorizable * @return True if this inherits from the given ParameterContext */ boolean inheritsFrom(String parameterContextId); + + /** + * Extracts the referenced parameter name from a one-to-one parameter value reference. + * A one-to-one reference is a parameter value whose entire content is exactly #{parameterName} + * with no surrounding text. + * + * @param value the parameter value to check + * @return the referenced parameter name if the value is a one-to-one reference, or null otherwise + */ + static String extractOneToOneParameterReference(final String value) { + if (value == null || value.length() < 4) { + return null; + } + if (value.startsWith("#{") && value.endsWith("}") && value.indexOf('}') == value.length() - 1) { + return value.substring(2, value.length() - 1); + } + return null; + } } diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java index 3257d81385cd..77052751cde9 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java @@ -1679,6 +1679,11 @@ private Set getComponentsAffectedByParameterContextUpda final Set updatedParameterNames = getUpdatedParameterNames(parameterContextDto); + // Extend the updated parameter names with cascading names from parameter value references. + // If a context P inherits from the updated context S, and P has a local parameter X = #{Parameter_In_S}, + // then X is effectively updated when Parameter_In_S changes. + final Set extendedParameterNames = extendWithParameterValueReferences(updatedParameterNames, groupsReferencingParameterContext); + // Clear set of Affected Components for each Parameter. This parameter is read-only and it will be populated below. for (final ParameterEntity parameterEntity : parameterContextDto.getParameters()) { parameterEntity.getParameter().setReferencingComponents(new HashSet<>()); @@ -1699,7 +1704,7 @@ private Set getComponentsAffectedByParameterContextUpda for (final ProcessorNode processor : group.getProcessors()) { if (includeInactive || processor.isRunning()) { final Set referencedParams = processor.getReferencedParameterNames(); - final boolean referencesUpdatedParam = referencedParams.stream().anyMatch(updatedParameterNames::contains); + final boolean referencesUpdatedParam = referencedParams.stream().anyMatch(extendedParameterNames::contains); if (referencesUpdatedParam) { affectedComponents.add(processor); @@ -1721,7 +1726,7 @@ private Set getComponentsAffectedByParameterContextUpda for (final ControllerServiceNode service : group.getControllerServices(false)) { if (includeInactive || service.isActive()) { final Set referencedParams = service.getReferencedParameterNames(); - final Set updatedReferencedParams = referencedParams.stream().filter(updatedParameterNames::contains).collect(Collectors.toSet()); + final Set updatedReferencedParams = referencedParams.stream().filter(extendedParameterNames::contains).collect(Collectors.toSet()); final List affectedParameterDtos = new ArrayList<>(); for (final String referencedParam : referencedParams) { @@ -1855,6 +1860,27 @@ private Set getUpdatedParameterNames(final ParameterContextDTO parameter return updatedParameters; } + private Set extendWithParameterValueReferences(final Set updatedParameterNames, final List groupsReferencingParameterContext) { + final Set extended = new HashSet<>(updatedParameterNames); + for (final ProcessGroup group : groupsReferencingParameterContext) { + final ParameterContext groupContext = group.getParameterContext(); + if (groupContext == null) { + continue; + } + for (final Map.Entry entry : groupContext.getParameters().entrySet()) { + final String referencedName = ParameterContext.extractOneToOneParameterReference(entry.getValue().getValue()); + if (referencedName == null || !updatedParameterNames.contains(referencedName)) { + continue; + } + final Optional referencedParam = groupContext.getParameter(referencedName); + if (referencedParam.isPresent() && referencedParam.get().isProvided()) { + extended.add(entry.getKey().getName()); + } + } + } + return extended; + } + @Override public ProcessGroupEntity updateProcessGroup(final Revision revision, final ProcessGroupDTO processGroupDTO) { final ProcessGroup processGroupNode = processGroupDAO.getProcessGroup(processGroupDTO.getId()); diff --git a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/parameters/ParameterContextIT.java b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/parameters/ParameterContextIT.java index 40a609bd79b5..b960d6b7c278 100644 --- a/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/parameters/ParameterContextIT.java +++ b/nifi-system-tests/nifi-system-test-suite/src/test/java/org/apache/nifi/tests/system/parameters/ParameterContextIT.java @@ -1295,6 +1295,65 @@ protected void assertAssetExists(final AssetEntity asset, final AssetsEntity ass assertNotNull(assetFromListing); } + @Test + public void testParameterValueReferenceUpdatePropagation() throws NiFiClientException, IOException, InterruptedException { + // Create a parameter provider for context S with a parameter db_host + ParameterProviderEntity parameterProvider = createParameterProvider("PropertiesParameterProvider"); + parameterProvider = updateParameterProviderProperties(parameterProvider, Collections.singletonMap("parameters", "db_host=0 sec")); + + final String parameterGroupName = "Parameters"; + final String sContextName = "S_Context"; + final ParameterContextEntity sContextEntity = createParameterContextEntity(sContextName, "Inherited provider context", + Collections.emptySet(), Collections.emptyList(), parameterProvider, parameterGroupName); + final ParameterContextEntity createdS = getNifiClient().getParamContextClient().createParamContext(sContextEntity); + + // Fetch and apply parameters from the provider so db_host becomes a provided parameter + final ParameterGroupConfigurationEntity groupConfiguration = new ParameterGroupConfigurationEntity(); + groupConfiguration.setSynchronized(true); + groupConfiguration.setGroupName(parameterGroupName); + groupConfiguration.setParameterContextName(sContextName); + groupConfiguration.setParameterSensitivities(Collections.singletonMap("db_host", ParameterSensitivity.NON_SENSITIVE)); + fetchAndWaitForAppliedParameters(parameterProvider, Collections.singletonList(groupConfiguration)); + + // Create parent context P with parameter host = #{db_host}, inheriting from S + final Set pParams = new HashSet<>(); + pParams.add(createParameterEntity("host", null, false, "#{db_host}")); + final ParameterContextEntity pContextEntity = createParameterContextEntity("P_Context", "Parent context", + pParams, Collections.singletonList(createdS), null, null); + final ParameterContextEntity createdP = getNifiClient().getParamContextClient().createParamContext(pContextEntity); + + // Bind the root process group to P + setParameterContext("root", createdP); + + // Create a processor that references #{host} + ProcessorEntity processorEntity = createProcessor(TEST_PROCESSORS_PACKAGE + ".Sleep", NIFI_GROUP_ID, TEST_EXTENSIONS_ARTIFACT_ID, getNiFiVersion()); + final String processorId = processorEntity.getId(); + + final ProcessorConfigDTO config = processorEntity.getComponent().getConfig(); + config.setProperties(Collections.singletonMap("Validate Sleep Time", "#{host}")); + config.setAutoTerminatedRelationships(Collections.singleton("success")); + getNifiClient().getProcessorClient().updateProcessor(processorEntity); + + // host resolves to "0 sec" (from the provider), so the processor should be valid + waitForValidProcessor(processorId); + + // Start the processor + getClientUtil().startProcessor(processorEntity); + waitForRunningProcessor(processorId); + + try { + // Update db_host via the provider to a new value + parameterProvider = updateParameterProviderProperties(parameterProvider, Collections.singletonMap("parameters", "db_host=1 sec")); + fetchAndWaitForAppliedParameters(parameterProvider, Collections.singletonList(groupConfiguration)); + + // Processor should be running again after the parameter update completes + waitForRunningProcessor(processorId); + } finally { + getClientUtil().stopProcessor(processorEntity); + getNifiClient().getProcessorClient().deleteProcessor(processorId, processorEntity.getRevision().getClientId(), 3); + } + } + protected void assertAsset(final AssetEntity asset, final String expectedName) { assertNotNull(asset); assertNotNull(asset.getAsset());