You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by jo...@apache.org on 2021/01/27 04:55:54 UTC

[nifi] branch main updated: NIFI-8173, NIFI-8174: This closes #4784. Updated Parameter Contexts to allow for unsetting a parameter value / setting it to null. Allow Parameters to make use of Expression Language. Updated docs to illustrated how EL is evaluated

This is an automated email from the ASF dual-hosted git repository.

joewitt pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/nifi.git


The following commit(s) were added to refs/heads/main by this push:
     new 4ff9cdd  NIFI-8173, NIFI-8174: This closes #4784. Updated Parameter Contexts to allow for unsetting a parameter value / setting it to null. Allow Parameters to make use of Expression Language. Updated docs to illustrated how EL is evaluated
4ff9cdd is described below

commit 4ff9cddf159e974c7a5a5c9aa9e238c520d37dc3
Author: Mark Payne <ma...@hotmail.com>
AuthorDate: Tue Jan 26 14:54:17 2021 -0500

    NIFI-8173, NIFI-8174: This closes #4784. Updated Parameter Contexts to allow for unsetting a parameter value / setting it to null. Allow Parameters to make use of Expression Language. Updated docs to illustrated how EL is evaluated
    
    Signed-off-by: Joe Witt <jo...@apache.org>
---
 .../nifi/parameter/StandardParameterReference.java |   2 +-
 .../nifi/parameter/StandardParameterTokenList.java |  15 +-
 .../parameter/TestStandardParameterTokenList.java  |   3 +-
 nifi-docs/src/main/asciidoc/user-guide.adoc        |  25 ++-
 .../org/apache/nifi/web/api/dto/ParameterDTO.java  |  11 ++
 .../nifi/parameter/StandardParameterContext.java   |   7 +-
 .../nifi/processor/StandardProcessContext.java     |   2 +-
 .../parameter/TestStandardParameterContext.java    |   7 +-
 .../nifi/controller/AbstractComponentNode.java     |  10 --
 .../serialization/StandardFlowSerializer.java      |  45 ++---
 .../src/main/resources/FlowConfiguration.xsd       |   2 +-
 .../web/dao/impl/StandardParameterContextDAO.java  |  25 ++-
 .../webapp/js/nf/canvas/nf-parameter-contexts.js   |  16 +-
 .../EvaluatePropertiesWithDifferentELScopes.java   |  97 ++++++++++
 .../nifi/processors/tests/system/WriteToFile.java  | 103 +++++++++++
 .../services/org.apache.nifi.processor.Processor   |   2 +
 .../system/parameters/ParameterContextIT.java      | 198 ++++++++++++++++++++-
 17 files changed, 510 insertions(+), 60 deletions(-)

diff --git a/nifi-commons/nifi-parameter/src/main/java/org/apache/nifi/parameter/StandardParameterReference.java b/nifi-commons/nifi-parameter/src/main/java/org/apache/nifi/parameter/StandardParameterReference.java
index 4f270f1..ec596e6 100644
--- a/nifi-commons/nifi-parameter/src/main/java/org/apache/nifi/parameter/StandardParameterReference.java
+++ b/nifi-commons/nifi-parameter/src/main/java/org/apache/nifi/parameter/StandardParameterReference.java
@@ -68,6 +68,6 @@ public class StandardParameterReference implements ParameterReference {
         }
 
         final Optional<Parameter> parameter = parameterLookup.getParameter(parameterName);
-        return parameter.map(Parameter::getValue).orElse(referenceText);
+        return parameter.map(Parameter::getValue).orElse(null);
     }
 }
diff --git a/nifi-commons/nifi-parameter/src/main/java/org/apache/nifi/parameter/StandardParameterTokenList.java b/nifi-commons/nifi-parameter/src/main/java/org/apache/nifi/parameter/StandardParameterTokenList.java
index f9c0a0f..3959895 100644
--- a/nifi-commons/nifi-parameter/src/main/java/org/apache/nifi/parameter/StandardParameterTokenList.java
+++ b/nifi-commons/nifi-parameter/src/main/java/org/apache/nifi/parameter/StandardParameterTokenList.java
@@ -76,12 +76,18 @@ public class StandardParameterTokenList implements ParameterTokenList {
     private String substitute(final Function<ParameterToken, String> transform) {
         final StringBuilder sb = new StringBuilder();
 
+        int nullCount = 0;
         int lastEndOffset = -1;
         for (final ParameterToken token : tokens) {
             final int startOffset = token.getStartOffset();
 
             sb.append(input, lastEndOffset + 1, startOffset);
-            sb.append(transform.apply(token));
+            final String transformed = transform.apply(token);
+            if (transformed == null) {
+                nullCount++;
+            } else {
+                sb.append(transformed);
+            }
 
             lastEndOffset = token.getEndOffset();
         }
@@ -90,7 +96,12 @@ public class StandardParameterTokenList implements ParameterTokenList {
             sb.append(input, lastEndOffset + 1, input.length());
         }
 
-        return sb.toString();
+        final String substituted = sb.toString();
+        if (nullCount == tokens.size() && !tokens.isEmpty() && substituted.isEmpty()) {
+            return null;
+        }
+
+        return substituted;
     }
 
     @Override
diff --git a/nifi-commons/nifi-parameter/src/test/java/org/apache/nifi/parameter/TestStandardParameterTokenList.java b/nifi-commons/nifi-parameter/src/test/java/org/apache/nifi/parameter/TestStandardParameterTokenList.java
index bfac611..7f5afe7 100644
--- a/nifi-commons/nifi-parameter/src/test/java/org/apache/nifi/parameter/TestStandardParameterTokenList.java
+++ b/nifi-commons/nifi-parameter/src/test/java/org/apache/nifi/parameter/TestStandardParameterTokenList.java
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Optional;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 public class TestStandardParameterTokenList {
 
@@ -60,7 +61,7 @@ public class TestStandardParameterTokenList {
         Mockito.when(paramContext.getParameter(Mockito.anyString())).thenReturn(Optional.empty());
         final StandardParameterTokenList references = new StandardParameterTokenList("#{foo}", referenceList);
 
-        assertEquals("#{foo}", references.substitute(paramContext));
+        assertNull(references.substitute(paramContext));
     }
 
     @Test
diff --git a/nifi-docs/src/main/asciidoc/user-guide.adoc b/nifi-docs/src/main/asciidoc/user-guide.adoc
index f5647d4..88d9d7f 100644
--- a/nifi-docs/src/main/asciidoc/user-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/user-guide.adoc
@@ -881,7 +881,8 @@ The Add Parameter window has the following settings:
 
 - *Name* - A name that is used to denote the Parameter. Only alpha-numeric characters (a-z, A-Z, 0-9), hyphens ( - ), underscores ( _ ), periods ( . ), and spaces are allowed.
 
-- *Value* - The value that will be used when the Parameter is referenced. Parameter values do not support Expression Language or embedded parameter references.
+- *Value* - The value that will be used when the Parameter is referenced. If a Parameter makes use of the Expression Language, it is important to note that the Expression Language will be evaluated
+in the context of the component that references the Parameter. Please see the <<parameters-and-el>> section below for more information.
 
 - *Set empty string* - Check to explicitly set the value of the Parameter to an empty string. Unchecked by default. (Note: If checked but a value is set, the checkbox is ignored.)
 
@@ -900,6 +901,28 @@ image:parameters-validate-affected-components.png[Validate Affected Components]
 The Referencing Components section now lists an aggregation of all the components referenced by the set of parameters added/edited/deleted, organized by process group.
 
 
+[[parameters-and-el]]
+==== Parameters and Expression Language
+
+When adding a Parameter that makes use of the Expression Language, it is important to understand the context in which the Expression Language will be evaluated. The expression is always evaluated
+in the context of the Process or Controller Service that references the Parameter. Take, for example, a scenario where Parameter with the name `Time` is added with a value of `${now()}`. The
+Expression Language results in a call to determine the system time when it is evaluated. When added as a Parameter, the system time is not evaluated when the Parameter is added, but rather when a
+Processor or Controller Service evaluates the Expression. That is, if a Processor has a Property whose value is set to `#{Time}` it will function in exactly the same manner as if the Property's
+value were set to `${now()}`. Each time that the property is referenced, it will produce a different timestamp.
+
+Furthermore, some Properties do not allow for Expression Language, while others allow for Expression Language but do not evaluate expressions against FlowFile attributes. To help understand how
+this works, consider a Parameter named `File` whose value is `${filename}`. Then consider three different properties, each with a different Expression Language Scope and a FlowFile whose filename
+is `test.txt`. If each of those Properties is set to `#{File}`, then the follow table illustrates the resultant value.
+
+|===
+| Configured Property Value | Expression Language Scope | Effective Property Value | Notes
+
+| #{File} | FlowFile Attributes | test.txt | The filename is resolved by looking at the `filename` attribute.
+| #{File} | Variable Registry Only | _Empty String_ | FlowFile attributes are not in scope, and we assume there is no Variable in the Variable Registry named "filename"
+| #{File} | None | ${filename} | The literal text "${filename}" will be unevaluated.
+|===
+
+
 [[assigning_parameter_context_to_PG]]
 ==== Assigning a Parameter Context to a Process Group
 For a component to reference a Parameter, its Process Group must first be assigned a Parameter Context. Once assigned, processors and controller services within that Process Group may only reference Parameters within that Parameter Context.
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ParameterDTO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ParameterDTO.java
index 4cbeb6f..7d48bec 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ParameterDTO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/dto/ParameterDTO.java
@@ -28,6 +28,7 @@ public class ParameterDTO {
     private String description;
     private Boolean sensitive;
     private String value;
+    private Boolean valueRemoved;
     private Set<AffectedComponentEntity> referencingComponents;
 
     @ApiModelProperty("The name of the Parameter")
@@ -66,6 +67,16 @@ public class ParameterDTO {
         this.value = value;
     }
 
+    @ApiModelProperty("Whether or not the value of the Parameter was removed. When a request is made to change a parameter, the value may be null. The absence of the value may be used either to " +
+        "indicate that the value is not to be changed, or that the value is to be set to null (i.e., removed). This denotes which of the two scenarios is being encountered.")
+    public Boolean getValueRemoved() {
+        return valueRemoved;
+    }
+
+    public void setValueRemoved(final Boolean valueRemoved) {
+        this.valueRemoved = valueRemoved;
+    }
+
     @ApiModelProperty("The set of all components in the flow that are referencing this Parameter")
     public Set<AffectedComponentEntity> getReferencingComponents() {
         return referencingComponents;
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java
index 6485827..763e0fd 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java
@@ -172,12 +172,7 @@ public class StandardParameterContext implements ParameterContext {
     }
 
     private String getFullyPopulatedValue(final Parameter proposedParameter) {
-        if (proposedParameter.getValue() != null) {
-            return proposedParameter.getValue();
-        }
-
-        final Parameter oldParameter = parameters.get(proposedParameter.getDescriptor());
-        return oldParameter == null ? null : oldParameter.getValue();
+        return proposedParameter.getValue();
     }
 
     private ParameterDescriptor getFullyPopulatedDescriptor(final Parameter proposedParameter) {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/processor/StandardProcessContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/processor/StandardProcessContext.java
index d7030ef..ca66598 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/processor/StandardProcessContext.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/processor/StandardProcessContext.java
@@ -67,7 +67,7 @@ public class StandardProcessContext implements ProcessContext, ControllerService
         properties = Collections.unmodifiableMap(processorNode.getEffectivePropertyValues());
 
         preparedQueries = new HashMap<>();
-        for (final Map.Entry<PropertyDescriptor, String> entry : processorNode.getRawPropertyValues().entrySet()) {
+        for (final Map.Entry<PropertyDescriptor, String> entry : properties.entrySet()) {
             final PropertyDescriptor desc = entry.getKey();
             String value = entry.getValue();
             if (value == null) {
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java
index 2be492e..15b268f 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-components/src/test/java/org/apache/nifi/parameter/TestStandardParameterContext.java
@@ -33,6 +33,7 @@ import java.util.Set;
 import static junit.framework.TestCase.assertTrue;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
 
 public class TestStandardParameterContext {
 
@@ -53,12 +54,12 @@ public class TestStandardParameterContext {
 
         final Parameter abcParam = context.getParameter("abc").get();
         assertEquals(abcDescriptor, abcParam.getDescriptor());
-        Assert.assertNull(abcParam.getDescriptor().getDescription());
+        assertNull(abcParam.getDescriptor().getDescription());
         assertEquals("123", abcParam.getValue());
 
         final Parameter xyzParam = context.getParameter("xyz").get();
         assertEquals(xyzDescriptor, xyzParam.getDescriptor());
-        Assert.assertNull(xyzParam.getDescriptor().getDescription());
+        assertNull(xyzParam.getDescriptor().getDescription());
         assertEquals("242526", xyzParam.getValue());
 
         final Map<String, Parameter> secondParameters = new HashMap<>();
@@ -124,7 +125,7 @@ public class TestStandardParameterContext {
         abcParam = context.getParameter("abc").get();
         assertEquals(abcDescriptor, abcParam.getDescriptor());
         assertEquals("Updated Again", abcParam.getDescriptor().getDescription());
-        assertEquals("321", abcParam.getValue());
+        assertNull(abcParam.getValue());
     }
 
     @Test
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
index 9c44f7a..a40c729 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core-api/src/main/java/org/apache/nifi/controller/AbstractComponentNode.java
@@ -673,16 +673,6 @@ public abstract class AbstractComponentNode implements ComponentNode {
                         .valid(false)
                         .explanation("Property references Parameter '" + paramName + "' but the currently selected Parameter Context does not have a Parameter with that name")
                         .build());
-
-                    continue;
-                }
-
-                if (!validationContext.isParameterSet(paramName)) {
-                    results.add(new ValidationResult.Builder()
-                        .subject(propertyDescriptor.getDisplayName())
-                        .valid(false)
-                        .explanation("Property references Parameter '" + paramName + "' but the currently selected Parameter Context does not have a value set for that Parameter")
-                        .build());
                 }
             }
         }
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
index 950da5c..b382930 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/java/org/apache/nifi/controller/serialization/StandardFlowSerializer.java
@@ -16,22 +16,6 @@
  */
 package org.apache.nifi.controller.serialization;
 
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.TimeUnit;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.transform.OutputKeys;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.TransformerFactoryConfigurationError;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.stream.StreamResult;
 import org.apache.nifi.bundle.BundleCoordinate;
 import org.apache.nifi.components.PropertyDescriptor;
 import org.apache.nifi.connectable.ConnectableType;
@@ -72,6 +56,23 @@ import org.w3c.dom.Document;
 import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
 /**
  * Serializes a Flow Controller as XML to an output stream.
  *
@@ -175,11 +176,13 @@ public class StandardFlowSerializer implements FlowSerializer<Document> {
         addStringElement(parameterElement, "description", descriptor.getDescription());
         addStringElement(parameterElement, "sensitive", String.valueOf(descriptor.isSensitive()));
 
-        if (descriptor.isSensitive()) {
-            final String parameterValue = parameter.getValue();
-            addStringElement(parameterElement, "value", parameterValue == null ? null : ENC_PREFIX + encryptor.encrypt(parameterValue) + ENC_SUFFIX);
-        } else {
-            addStringElement(parameterElement, "value", parameter.getValue());
+        if (parameter.getValue() != null) {
+            if (descriptor.isSensitive()) {
+                final String parameterValue = parameter.getValue();
+                addStringElement(parameterElement, "value", parameterValue == null ? null : ENC_PREFIX + encryptor.encrypt(parameterValue) + ENC_SUFFIX);
+            } else {
+                addStringElement(parameterElement, "value", parameter.getValue());
+            }
         }
     }
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
index 36d7d92..fd3577b 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-framework-core/src/main/resources/FlowConfiguration.xsd
@@ -77,7 +77,7 @@
             <xs:element name="name" type="NonEmptyStringType" />
             <xs:element name="description" type="xs:string" />
             <xs:element name="sensitive" type="xs:boolean" />
-            <xs:element name="value" type="xs:string" />
+            <xs:element name="value" type="xs:string" minOccurs="0" maxOccurs="1" />
         </xs:sequence>
     </xs:complexType>
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardParameterContextDAO.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardParameterContextDAO.java
index bb5ec9a..583806e 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardParameterContextDAO.java
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/dao/impl/StandardParameterContextDAO.java
@@ -40,6 +40,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 
 public class StandardParameterContextDAO implements ParameterContextDAO {
@@ -57,7 +58,7 @@ public class StandardParameterContextDAO implements ParameterContextDAO {
 
     @Override
     public ParameterContext createParameterContext(final ParameterContextDTO parameterContextDto) {
-        final Map<String, Parameter> parameters = getParameters(parameterContextDto);
+        final Map<String, Parameter> parameters = getParameters(parameterContextDto, null);
         final ParameterContext parameterContext = flowManager.createParameterContext(parameterContextDto.getId(), parameterContextDto.getName(), parameters);
         if (parameterContextDto.getDescription() != null) {
             parameterContext.setDescription(parameterContextDto.getDescription());
@@ -65,7 +66,7 @@ public class StandardParameterContextDAO implements ParameterContextDAO {
         return parameterContext;
     }
 
-    private Map<String, Parameter> getParameters(final ParameterContextDTO parameterContextDto) {
+    private Map<String, Parameter> getParameters(final ParameterContextDTO parameterContextDto, final ParameterContext context) {
         final Set<ParameterEntity> parameterEntities = parameterContextDto.getParameters();
         if (parameterEntities == null) {
             return Collections.emptyMap();
@@ -83,7 +84,7 @@ public class StandardParameterContextDAO implements ParameterContextDAO {
             if (deletion) {
                 parameterMap.put(parameterDto.getName().trim(), null);
             } else {
-                final Parameter parameter = createParameter(parameterDto);
+                final Parameter parameter = createParameter(parameterDto, context);
                 parameterMap.put(parameterDto.getName().trim(), parameter);
             }
         }
@@ -91,14 +92,26 @@ public class StandardParameterContextDAO implements ParameterContextDAO {
         return parameterMap;
     }
 
-    private Parameter createParameter(final ParameterDTO dto) {
+    private Parameter createParameter(final ParameterDTO dto, final ParameterContext context) {
         final ParameterDescriptor descriptor = new ParameterDescriptor.Builder()
             .name(dto.getName())
             .description(dto.getDescription())
             .sensitive(Boolean.TRUE.equals(dto.getSensitive()))
             .build();
 
-        return new Parameter(descriptor, dto.getValue());
+        final String value;
+        if (dto.getValue() == null && Boolean.TRUE.equals(dto.getValueRemoved())) {
+            // Value is being explicitly set to null
+            value = null;
+        } else if (dto.getValue() == null && context != null) {
+            // Value was just never supplied. Use the value from the Parameter Context, if there is one.
+            final Optional<Parameter> optionalParameter = context.getParameter(dto.getName());
+            value = optionalParameter.map(Parameter::getValue).orElse(dto.getValue());
+        } else {
+            value = dto.getValue();
+        }
+
+        return new Parameter(descriptor, value);
     }
 
     @Override
@@ -132,7 +145,7 @@ public class StandardParameterContextDAO implements ParameterContextDAO {
         }
 
         if (parameterContextDto.getParameters() != null) {
-            final Map<String, Parameter> parameters = getParameters(parameterContextDto);
+            final Map<String, Parameter> parameters = getParameters(parameterContextDto, context);
             context.setParameters(parameters);
         }
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-parameter-contexts.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-parameter-contexts.js
index c86df4d..7bbe867 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-parameter-contexts.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-parameter-contexts.js
@@ -270,6 +270,13 @@
                 // if modified grab what's changed
                 if (this.hasValueChanged) {
                     parameter['value'] = this.value;
+
+                    if (_.isEmpty(this.value) && !this.isEmptyStringSet) {
+                        // No value is set, and the 'Set empty string' checkbox was not set. The value is expected to be null.
+                        // Because sensitive parameter values are not known, when a sensitive parameter is updated, its value is send back as null.
+                        // To disambiguate between a value explicitly being null, an empty string, and a value not be provided, we need to set the 'valueRemoved' flag.
+                        parameter['valueRemoved'] = true;
+                    }
                 }
 
                 parameter['description'] = this.description;
@@ -854,12 +861,9 @@
         var isSensitive = $('#parameter-dialog').find('input[name="sensitive"]:checked').val() === 'sensitive' ? true : false;
 
         var validateValue = function () {
-            // updates to a parameter cannot have a null value
-            if (!this.isNew) {
-                if (_.isEmpty(this.value) && !this.isEmptyStringSet) {
-                    return false;
-                }
-            }
+            // in previous versions, updates to a parameter were not allowed to have a null value.
+            // This has changed, but the validation function is left intact for now because it is likely that
+            // we may introduce additional validation in the future.
             return true;
         };
 
diff --git a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/EvaluatePropertiesWithDifferentELScopes.java b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/EvaluatePropertiesWithDifferentELScopes.java
new file mode 100644
index 0000000..dfc071b
--- /dev/null
+++ b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/EvaluatePropertiesWithDifferentELScopes.java
@@ -0,0 +1,97 @@
+/*
+ * 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.processors.tests.system;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.processor.util.StandardValidators;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class EvaluatePropertiesWithDifferentELScopes extends AbstractProcessor {
+    static final PropertyDescriptor EVALUATE_FLOWFILE_CONTEXT = new PropertyDescriptor.Builder()
+        .name("FlowFile Context")
+        .description("The value of the property will be evaluated with FlowFile attributes")
+        .required(false)
+        .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+        .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
+        .build();
+    static final PropertyDescriptor EVALUATE_VARIABLE_REGISTRY_CONTEXT = new PropertyDescriptor.Builder()
+        .name("Variable Registry Context")
+        .description("The value of the property will be evaluated with only with the Variable Registry")
+        .required(false)
+        .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+        .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+        .build();
+    static final PropertyDescriptor EVALUATE_NO_EL_CONTEXT = new PropertyDescriptor.Builder()
+        .name("Expression Language Not Evaluated")
+        .description("The value of the property will be evaluated without evaluating Expression Language")
+        .required(false)
+        .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+        .expressionLanguageSupported(ExpressionLanguageScope.NONE)
+        .build();
+
+    public static final Relationship REL_SUCCESS = new Relationship.Builder()
+        .name("success")
+        .build();
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return Collections.singleton(REL_SUCCESS);
+    }
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return Arrays.asList(EVALUATE_FLOWFILE_CONTEXT, EVALUATE_VARIABLE_REGISTRY_CONTEXT, EVALUATE_NO_EL_CONTEXT);
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
+        FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        final Integer flowFileContext = context.getProperty(EVALUATE_FLOWFILE_CONTEXT).evaluateAttributeExpressions(flowFile).asInteger();
+        final Integer variableRegistryContext = context.getProperty(EVALUATE_VARIABLE_REGISTRY_CONTEXT).evaluateAttributeExpressions().asInteger();
+        final String noElContext = context.getProperty(EVALUATE_NO_EL_CONTEXT).getValue();
+
+        if (flowFileContext != null) {
+            session.adjustCounter("flowfile", flowFileContext, false);
+        }
+
+        if (variableRegistryContext != null) {
+            session.adjustCounter("variable.registry", variableRegistryContext, false);
+        }
+
+        if (noElContext != null && noElContext.matches("[0-9]+")) {
+            session.adjustCounter("no.el.evaluation", Integer.parseInt(noElContext), false);
+        }
+
+        session.transfer(flowFile, REL_SUCCESS);
+    }
+}
diff --git a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/WriteToFile.java b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/WriteToFile.java
new file mode 100644
index 0000000..e5086cc
--- /dev/null
+++ b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/java/org/apache/nifi/processors/tests/system/WriteToFile.java
@@ -0,0 +1,103 @@
+/*
+ * 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.processors.tests.system;
+
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.components.PropertyDescriptor.Builder;
+import org.apache.nifi.flowfile.FlowFile;
+import org.apache.nifi.processor.AbstractProcessor;
+import org.apache.nifi.processor.ProcessContext;
+import org.apache.nifi.processor.ProcessSession;
+import org.apache.nifi.processor.Relationship;
+import org.apache.nifi.processor.exception.ProcessException;
+import org.apache.nifi.stream.io.StreamUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.apache.nifi.expression.ExpressionLanguageScope.FLOWFILE_ATTRIBUTES;
+import static org.apache.nifi.processor.util.StandardValidators.NON_EMPTY_VALIDATOR;
+
+public class WriteToFile extends AbstractProcessor {
+
+    static final PropertyDescriptor FILENAME = new Builder()
+        .name("Filename")
+        .displayName("Filename")
+        .description("The file to write the FlowFile contents to")
+        .required(true)
+        .addValidator(NON_EMPTY_VALIDATOR)
+        .expressionLanguageSupported(FLOWFILE_ATTRIBUTES)
+        .defaultValue("target/WriteToFile.out")
+        .build();
+
+    static final Relationship REL_SUCCESS = new Relationship.Builder()
+        .name("success")
+        .build();
+
+    static final Relationship REL_FAILURE = new Relationship.Builder()
+        .name("failure")
+        .build();
+
+    @Override
+    public Set<Relationship> getRelationships() {
+        return new HashSet<>(Arrays.asList(REL_SUCCESS, REL_FAILURE));
+    }
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        return Collections.singletonList(FILENAME);
+    }
+
+    @Override
+    public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException {
+        FlowFile flowFile = session.get();
+        if (flowFile == null) {
+            return;
+        }
+
+        final String filename = context.getProperty(FILENAME).evaluateAttributeExpressions(flowFile).getValue();
+        final File file = new File(filename);
+        final File dir = file.getParentFile();
+        if (!dir.exists() && !dir.mkdirs()) {
+            getLogger().error("Could not write FlowFile to {} because the directory does not exist and could not be created", file.getAbsolutePath());
+            session.transfer(flowFile, REL_FAILURE);
+            return;
+        }
+
+        try (final OutputStream out = new FileOutputStream(file);
+             final InputStream in = session.read(flowFile)) {
+            StreamUtils.copy(in, out);
+        } catch (final Exception e) {
+            getLogger().error("Could not write FlowFile to {}", file.getAbsolutePath(), e);
+            session.transfer(flowFile, REL_FAILURE);
+            return;
+        }
+
+        session.transfer(flowFile, REL_SUCCESS);
+
+        getLogger().info("Wrote one FlowFile of size {} to {}", flowFile.getSize(), file.getAbsolutePath());
+        session.getProvenanceReporter().send(flowFile, file.toURI().toString());
+    }
+}
diff --git a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
index ce4347a..169a9c6 100644
--- a/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
+++ b/nifi-system-tests/nifi-system-test-extensions-bundle/nifi-system-test-extensions/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor
@@ -17,6 +17,7 @@ org.apache.nifi.processors.tests.system.CountEvents
 org.apache.nifi.processors.tests.system.CountFlowFiles
 org.apache.nifi.processors.tests.system.DependOnProperties
 org.apache.nifi.processors.tests.system.Duplicate
+org.apache.nifi.processors.tests.system.EvaluatePropertiesWithDifferentELScopes
 org.apache.nifi.processors.tests.system.FakeProcessor
 org.apache.nifi.processors.tests.system.FakeDynamicPropertiesProcessor
 org.apache.nifi.processors.tests.system.GenerateFlowFile
@@ -26,3 +27,4 @@ org.apache.nifi.processors.tests.system.Sleep
 org.apache.nifi.processors.tests.system.TerminateFlowFile
 org.apache.nifi.processors.tests.system.ThrowProcessException
 org.apache.nifi.processors.tests.system.ValidateFileExists
+org.apache.nifi.processors.tests.system.WriteToFile
\ No newline at end of file
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 2221789..17f07e1 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
@@ -31,9 +31,14 @@ import org.apache.nifi.web.api.entity.ProcessGroupEntity;
 import org.apache.nifi.web.api.entity.ProcessorEntity;
 import org.junit.Test;
 
+import java.io.File;
 import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -104,7 +109,7 @@ public class ParameterContextIT extends NiFiSystemIT {
     }
 
 
-    @Test(timeout = 30000)
+    @Test
     public void testAddingMissingParameterMakesProcessorValid() throws NiFiClientException, IOException, InterruptedException {
         final ProcessorEntity createdProcessorEntity = createProcessor(TEST_PROCESSORS_PACKAGE + ".CountEvents", NIFI_GROUP_ID, TEST_EXTENSIONS_ARTIFACT_ID, getNiFiVersion());
         final String processorId = createdProcessorEntity.getId();
@@ -125,6 +130,197 @@ public class ParameterContextIT extends NiFiSystemIT {
     }
 
     @Test
+    public void testValidationWithRequiredPropertiesAndDefault() throws NiFiClientException, IOException, InterruptedException {
+        final ProcessorEntity generate = getClientUtil().createProcessor("GenerateFlowFile");
+        getClientUtil().updateProcessorProperties(generate, Collections.singletonMap("File Size", "#{foo}"));
+        getClientUtil().setAutoTerminatedRelationships(generate, "success");
+
+        final String processorId = generate.getId();
+
+        waitForInvalidProcessor(processorId);
+
+        final ParameterEntity fooKB = createParameterEntity("foo", null, false, "1 KB");
+        final Set<ParameterEntity> parameters = new HashSet<>();
+        parameters.add(fooKB);
+        final ParameterContextEntity contextEntity = createParameterContextEntity(getTestName(), null, parameters);
+        final ParameterContextEntity createdContextEntity = getNifiClient().getParamContextClient().createParamContext(contextEntity);
+
+        setParameterContext("root", createdContextEntity);
+        waitForValidProcessor(processorId);
+
+        final ParameterEntity fooNull = createParameterEntity("foo", null, false, null);
+        createdContextEntity.getComponent().setParameters(Collections.singleton(fooNull));
+        getNifiClient().getParamContextClient().updateParamContext(createdContextEntity);
+        // Should remain valid because property has a default.
+        waitForValidProcessor(processorId);
+    }
+
+    @Test(timeout=30000)
+    public void testValidationWithRequiredPropertiesAndNoDefault() throws NiFiClientException, IOException, InterruptedException {
+        final ProcessorEntity generate = getClientUtil().createProcessor("DependOnProperties");
+        final Map<String, String> properties = new HashMap<>();
+        properties.put("Always Required", "#{foo}");
+        properties.put("Required If Always Required Is Bar Or Baz", "15");
+        getClientUtil().updateProcessorProperties(generate, properties);
+        final String processorId = generate.getId();
+
+        waitForInvalidProcessor(processorId);
+
+        final ParameterEntity fooBar = createParameterEntity("foo", null, false, "bar");
+        final Set<ParameterEntity> parameters = new HashSet<>();
+        parameters.add(fooBar);
+        final ParameterContextEntity contextEntity = createParameterContextEntity(getTestName(), null, parameters);
+        final ParameterContextEntity createdContextEntity = getNifiClient().getParamContextClient().createParamContext(contextEntity);
+
+        setParameterContext("root", createdContextEntity);
+        waitForValidProcessor(processorId);
+
+        final ParameterEntity fooNull = createParameterEntity("foo", null, false, null);
+        createdContextEntity.getComponent().setParameters(Collections.singleton(fooNull));
+        getNifiClient().getParamContextClient().updateParamContext(createdContextEntity);
+        // Should become invalid because property is required and has no default
+        waitForInvalidProcessor(processorId);
+    }
+
+    @Test
+    public void testParametersReferencingEL() throws NiFiClientException, IOException, InterruptedException {
+        final ProcessorEntity generate = getClientUtil().createProcessor("GenerateFlowFile");
+        getClientUtil().updateProcessorProperties(generate, Collections.singletonMap("a", "1"));
+        getClientUtil().updateProcessorSchedulingPeriod(generate, "10 min");
+
+        final ProcessorEntity evaluate = getClientUtil().createProcessor("EvaluatePropertiesWithDifferentELScopes");
+        final Map<String, String> evaluateProperties = new HashMap<>();
+        evaluateProperties.put("FlowFile Context", "#{A}");
+        evaluateProperties.put("Variable Registry Context", "#{A Replace With 5}");
+        evaluateProperties.put("Expression Language Not Evaluated", "#{Eleven A}");
+        getClientUtil().updateProcessorProperties(evaluate, evaluateProperties);
+
+        getClientUtil().createConnection(generate, evaluate, "success");
+        getClientUtil().setAutoTerminatedRelationships(evaluate, "success");
+
+        final Set<ParameterEntity> parameters = new HashSet<>();
+        parameters.add(createParameterEntity("A", null, false, "${a}"));
+        parameters.add(createParameterEntity("A Replace With 5", null, false, "${a:replaceNull(5)}"));
+        parameters.add(createParameterEntity("Eleven A", null, false, "11${a}"));
+        final ParameterContextEntity contextEntity = createParameterContextEntity(getTestName(), null, parameters);
+        final ParameterContextEntity createdContextEntity = getNifiClient().getParamContextClient().createParamContext(contextEntity);
+
+        setParameterContext("root", createdContextEntity);
+
+        waitForValidProcessor(generate.getId());
+        waitForValidProcessor(evaluate.getId());
+
+        getClientUtil().startProcessGroupComponents("root");
+
+        waitFor(() -> {
+            try {
+                return getClientUtil().getCountersAsMap(evaluate.getId()).get("flowfile") == getNumberOfNodes();
+            } catch (final Exception e) {
+                return false;
+            }
+        });
+
+        final Map<String, Long> counters = getClientUtil().getCountersAsMap(evaluate.getId());
+        assertEquals(getNumberOfNodes(), counters.get("flowfile").longValue());
+        assertEquals(5L * getNumberOfNodes(), counters.get("variable.registry").longValue()); // Since no value present in variable registry, will replace null with 5.
+        assertFalse(counters.containsKey("no.el.evaluation")); // Should not be evaluated
+
+        // Update parameters so that Eleven A has the value 11 without evaluating EL.
+        final Set<ParameterEntity> updatedParameters = new HashSet<>();
+        updatedParameters.add(createParameterEntity("A", null, false, "${a}"));
+        updatedParameters.add(createParameterEntity("A Replace With 5", null, false, "${a:replaceNull(5)}"));
+        updatedParameters.add(createParameterEntity("Eleven A", null, false, "11"));
+        final ParameterContextEntity updatedContextEntity = createParameterContextEntity(getTestName() + "-2", null, updatedParameters);
+        final ParameterContextEntity secondContextEntity = getNifiClient().getParamContextClient().createParamContext(updatedContextEntity);
+
+        // Stop process group so we can change the Parameter Context, then change the context and restart.
+        getClientUtil().stopProcessGroupComponents("root");
+        setParameterContext("root", secondContextEntity);
+        getClientUtil().startProcessGroupComponents("root");
+
+        // Wait for the 'no.el.evaluation' counter to be set
+        waitFor(() -> {
+            try {
+                return getClientUtil().getCountersAsMap(evaluate.getId()).get("no.el.evaluation") == 11 * getNumberOfNodes();
+            } catch (final Exception e) {
+                return false;
+            }
+        });
+    }
+
+    @Test
+    public void testParameterWithOptionalProperty() throws NiFiClientException, IOException, InterruptedException {
+        final ProcessorEntity generate = getClientUtil().createProcessor("GenerateFlowFile");
+        getClientUtil().updateProcessorSchedulingPeriod(generate, "10 min");
+
+        final Map<String, String> generateProperties = new HashMap<>();
+        generateProperties.put("Text", "#{Text}");
+        generateProperties.put("File Size", "1 KB");
+        getClientUtil().updateProcessorProperties(generate, generateProperties);
+
+        final ProcessorEntity writeFile = getClientUtil().createProcessor("WriteToFile");
+        final File file = new File("target/testParameterWithOptionalProperty.txt");
+        getClientUtil().updateProcessorProperties(writeFile, Collections.singletonMap("Filename", file.getAbsolutePath()));
+
+        getClientUtil().createConnection(generate, writeFile, "success");
+        getClientUtil().setAutoTerminatedRelationships(writeFile, new HashSet<>(Arrays.asList("success", "failure")));
+
+        final Set<ParameterEntity> parameters = new HashSet<>();
+        final ParameterContextEntity contextEntity = createParameterContextEntity(getTestName(), null, parameters);
+        final ParameterContextEntity createdContextEntity = getNifiClient().getParamContextClient().createParamContext(contextEntity);
+
+        setParameterContext("root", createdContextEntity);
+
+        // Processor should be invalid because it references a Parameter (Text) that does not exist
+        waitForInvalidProcessor(generate.getId());
+
+        // Update the Parameter Context to add new parameter but with null value.
+        final ParameterEntity nullText = createParameterEntity("Text", "Text", false, null);
+        createdContextEntity.getComponent().getParameters().add(nullText);
+        getNifiClient().getParamContextClient().updateParamContext(createdContextEntity);
+
+        waitForValidProcessor(generate.getId());
+
+        getClientUtil().startProcessGroupComponents("root");
+
+        // Ensure that the file is written with a file size of 1 KB
+        waitFor(() -> {
+            try {
+                return file.exists() && file.length() == 1024;
+            } catch (final Exception e) {
+                return false;
+            }
+        });
+
+        // Update Parameter to have a specific value
+        createdContextEntity.getComponent().getParameters().remove(nullText);
+        final String customText = "Some Custom Text";
+        createdContextEntity.getComponent().getParameters().add(createParameterEntity("Text", "Text", false, customText));
+
+        getNifiClient().getParamContextClient().updateParamContext(createdContextEntity);
+
+        // Wait for file to be written out
+        waitFor(() -> {
+            try {
+                final boolean correctSize = file.exists() && file.length() == customText.length();
+                if (!correctSize) {
+                    return false;
+                }
+
+                final List<String> lines = Files.readAllLines(file.toPath());
+                if (lines.size() != 1) {
+                    return false;
+                }
+
+                return customText.equals(lines.get(0));
+            } catch (final Exception e) {
+                return false;
+            }
+        });
+
+    }
+
+    @Test
     public void testProcessorStartedAfterLongValidationPeriod() throws NiFiClientException, IOException, InterruptedException {
         final ParameterContextEntity createdContextEntity = createParameterContext("sleep", "6 secs");