NIFI-309: Added Difference Types PROPERTY_PARAMETERIZED and PROPERTY_PARAMETERIZATION_REMOVED
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConciseEvolvingDifferenceDescriptor.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConciseEvolvingDifferenceDescriptor.java
index 177bad9..8ac2da9 100644
--- a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConciseEvolvingDifferenceDescriptor.java
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/ConciseEvolvingDifferenceDescriptor.java
@@ -54,6 +54,12 @@
             case PROPERTY_REMOVED:
                 description = String.format("Property '%s' was removed", fieldName);
                 break;
+            case PROPERTY_PARAMETERIZED:
+                description = String.format("Property '%s' was parameterized", fieldName);
+                break;
+            case PROPERTY_PARAMETERIZATION_REMOVED:
+                description = String.format("Property '%s' is no longer a parameter reference", fieldName);
+                break;
             case VARIABLE_ADDED:
                 description = String.format("Variable '%s' was added", fieldName);
                 break;
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceType.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceType.java
index ebba0f9..67c26ad 100644
--- a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceType.java
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/DifferenceType.java
@@ -109,6 +109,18 @@
     PROPERTY_REMOVED("Property Removed"),
 
     /**
+     * Property is unset or set to an explicit value in Flow A but set to (exactly) a parameter reference in Flow B. Note that if Flow A
+     * has a property set to "#{param1} abc" and it is changed to "#{param1} abc #{param2}" this would indicate a Difference Type of @{link #PROPERTY_CHANGED}, not
+     * PROPERTY_PARAMETERIZED
+     */
+    PROPERTY_PARAMETERIZED("Property Parameterized"),
+
+    /**
+     * Property is set to (exactly) a parameter reference in Flow A but either unset or set to an explicit value in Flow B.
+     */
+    PROPERTY_PARAMETERIZATION_REMOVED("Property Parameterization Removed"),
+
+    /**
      * The component has a different value for the Annotation Data in each of the flows
      */
     ANNOTATION_DATA_CHANGED("Annotation Data (Advanced UI Configuration) Changed"),
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/EvolvingDifferenceDescriptor.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/EvolvingDifferenceDescriptor.java
index 9a21026..a6c5d74 100644
--- a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/EvolvingDifferenceDescriptor.java
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/EvolvingDifferenceDescriptor.java
@@ -51,6 +51,12 @@
             case PROPERTY_REMOVED:
                 description = String.format("Property '%s' was removed from %s with ID %s", fieldName, componentA.getComponentType().getTypeName(), componentA.getIdentifier());
                 break;
+            case PROPERTY_PARAMETERIZED:
+                description = String.format("Property '%s' was parameterized", fieldName);
+                break;
+            case PROPERTY_PARAMETERIZATION_REMOVED:
+                description = String.format("Property '%s' is no longer a parameter reference", fieldName);
+                break;
             case VARIABLE_ADDED:
                 description = String.format("Variable '%s' was added to Process Group with ID %s", fieldName, componentB.getIdentifier());
                 break;
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java
index cb28b83..e4f5266 100644
--- a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StandardFlowComparator.java
@@ -37,6 +37,7 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.function.Function;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
 public class StandardFlowComparator implements FlowComparator {
@@ -44,6 +45,8 @@
     private static final String DEFAULT_PARTITIONING_ATTRIBUTE = "";
     private static final String DEFAULT_LOAD_BALANCE_COMPRESSION = "DO_NOT_COMPRESS";
 
+    private static final Pattern PARAMETER_REFERENCE_PATTERN = Pattern.compile("#\\{[A-Za-z0-9\\-_. ]+}");
+
     private final ComparableDataFlow flowA;
     private final ComparableDataFlow flowB;
     private final Set<String> externallyAccessibleServiceIds;
@@ -192,9 +195,17 @@
             }
 
             if (valueA == null && valueB != null) {
-                differences.add(difference(DifferenceType.PROPERTY_ADDED, componentA, componentB, key, displayName, valueA, valueB));
+                if (isParameterReference(valueB)) {
+                    differences.add(difference(DifferenceType.PROPERTY_PARAMETERIZED, componentA, componentB, key, displayName, null, null));
+                } else {
+                    differences.add(difference(DifferenceType.PROPERTY_ADDED, componentA, componentB, key, displayName, valueA, valueB));
+                }
             } else if (valueA != null && valueB == null) {
-                differences.add(difference(DifferenceType.PROPERTY_REMOVED, componentA, componentB, key, displayName, valueA, valueB));
+                if (isParameterReference(valueA)) {
+                    differences.add(difference(DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED, componentA, componentB, key, displayName, null, null));
+                } else {
+                    differences.add(difference(DifferenceType.PROPERTY_REMOVED, componentA, componentB, key, displayName, valueA, valueB));
+                }
             } else if (valueA != null && !valueA.equals(valueB)) {
                 // If the property in Flow A references a Controller Service that is not available in the flow
                 // and the property in Flow B references a Controller Service that is available in its environment
@@ -211,7 +222,15 @@
                     }
                 }
 
-                differences.add(difference(DifferenceType.PROPERTY_CHANGED, componentA, componentB, key, displayName, valueA, valueB));
+                final boolean aParameterized = isParameterReference(valueA);
+                final boolean bParameterized = isParameterReference(valueB);
+                if (aParameterized && !bParameterized) {
+                    differences.add(difference(DifferenceType.PROPERTY_PARAMETERIZATION_REMOVED, componentA, componentB, key, displayName, null, null));
+                } else if (!aParameterized && bParameterized) {
+                    differences.add(difference(DifferenceType.PROPERTY_PARAMETERIZED, componentA, componentB, key, displayName, null, null));
+                } else {
+                    differences.add(difference(DifferenceType.PROPERTY_CHANGED, componentA, componentB, key, displayName, valueA, valueB));
+                }
             }
         });
 
@@ -229,12 +248,20 @@
                     displayName = descriptor.getDisplayName() == null ? descriptor.getName() : descriptor.getDisplayName();
                 }
 
-                differences.add(difference(DifferenceType.PROPERTY_ADDED, componentA, componentB, key, displayName, null, valueB));
+                if (isParameterReference(valueB)) {
+                    differences.add(difference(DifferenceType.PROPERTY_PARAMETERIZED, componentA, componentB, key, displayName, null, null));
+                } else {
+                    differences.add(difference(DifferenceType.PROPERTY_ADDED, componentA, componentB, key, displayName, null, valueB));
+                }
             }
         });
     }
 
 
+    private boolean isParameterReference(final String propertyValue) {
+        return PARAMETER_REFERENCE_PATTERN.matcher(propertyValue).matches();
+    }
+
     private void compare(final VersionedFunnel funnelA, final VersionedFunnel funnelB, final Set<FlowDifference> differences) {
         compareComponents(funnelA, funnelB, differences);
     }
diff --git a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StaticDifferenceDescriptor.java b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StaticDifferenceDescriptor.java
index 9dd9c5c..8668804 100644
--- a/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StaticDifferenceDescriptor.java
+++ b/nifi-registry-core/nifi-registry-flow-diff/src/main/java/org/apache/nifi/registry/flow/diff/StaticDifferenceDescriptor.java
@@ -51,6 +51,12 @@
                 description = String.format("Property '%s' exists for %s with ID %s in %s but not in %s",
                     fieldName, componentA.getComponentType().getTypeName(), componentA.getIdentifier(), flowAName, flowBName);
                 break;
+            case PROPERTY_PARAMETERIZED:
+                description = String.format("Property '%s' is a parameter reference in %s but not in %s", fieldName, flowAName, flowBName);
+                break;
+            case PROPERTY_PARAMETERIZATION_REMOVED:
+                description = String.format("Property '%s' is a parameter reference in %s but not in %s", fieldName, flowBName, flowAName);
+                break;
             case SCHEDULED_STATE_CHANGED:
                 if (ScheduledState.DISABLED.equals(valueA)) {
                     description = String.format("%s is disabled in %s but enabled in %s", componentA.getComponentType().getTypeName(), flowAName, flowBName);