You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by bb...@apache.org on 2018/01/18 17:44:32 UTC

nifi-registry git commit: NIFIREG-77 - Add a 'diff' endpoint to the API/Client for comparing 2 versions of a flow.

Repository: nifi-registry
Updated Branches:
  refs/heads/master 4cfa8c958 -> 915aa3955


NIFIREG-77 - Add a 'diff' endpoint to the API/Client for comparing 2 versions of a flow.

NIFIREG-77 - Add client code for diff operation

This closes #88.

Signed-off-by: Bryan Bende <bb...@apache.org>


Project: http://git-wip-us.apache.org/repos/asf/nifi-registry/repo
Commit: http://git-wip-us.apache.org/repos/asf/nifi-registry/commit/915aa395
Tree: http://git-wip-us.apache.org/repos/asf/nifi-registry/tree/915aa395
Diff: http://git-wip-us.apache.org/repos/asf/nifi-registry/diff/915aa395

Branch: refs/heads/master
Commit: 915aa395576a30d352e76db6d8d0240da63c01b4
Parents: 4cfa8c9
Author: Danny Lane <da...@gmail.com>
Authored: Mon Jan 8 23:51:27 2018 +0000
Committer: Bryan Bende <bb...@apache.org>
Committed: Thu Jan 18 12:44:04 2018 -0500

----------------------------------------------------------------------
 .../apache/nifi/registry/client/FlowClient.java |  13 +++
 .../registry/client/impl/JerseyFlowClient.java  |  22 ++++
 .../nifi/registry/diff/ComponentDifference.java |  77 ++++++++++++++
 .../registry/diff/ComponentDifferenceGroup.java |  96 +++++++++++++++++
 .../registry/diff/VersionedFlowDifference.java  |  79 ++++++++++++++
 nifi-registry-framework/pom.xml                 |   5 +
 .../nifi/registry/service/DataModelMapper.java  |  35 +++++++
 .../nifi/registry/service/RegistryService.java  | 105 +++++++++++++++++++
 .../registry/service/TestRegistryService.java   |  89 +++++++++++++++-
 .../registry/web/api/BucketFlowResource.java    |  27 +++++
 .../web/api/UnsecuredNiFiRegistryClientIT.java  |  27 ++++-
 11 files changed, 572 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/915aa395/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
index 46526f7..150927a 100644
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
+++ b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/FlowClient.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.registry.client;
 
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
 import org.apache.nifi.registry.field.Fields;
 import org.apache.nifi.registry.flow.VersionedFlow;
 
@@ -106,4 +107,16 @@ public interface FlowClient {
      */
     List<VersionedFlow> getByBucket(String bucketId) throws NiFiRegistryException, IOException;
 
+    /**
+     *
+     * @param bucketId a bucket id
+     * @param flowId the flow that is under inspection
+     * @param versionA the first version to use in the comparison
+     * @param versionB the second flow to use in the comparison
+     * @return the list of differences between the 2 flow versions grouped by component
+     * @throws NiFiRegistryException if an error is encountered other than IOException
+     * @throws IOException if an I/O error is encountered
+     */
+    VersionedFlowDifference diff(final String bucketId, final String flowId,
+                                 final Integer versionA, final Integer versionB) throws NiFiRegistryException, IOException;
 }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/915aa395/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
----------------------------------------------------------------------
diff --git a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
index 95e1fb4..08e8cbb 100644
--- a/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
+++ b/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyFlowClient.java
@@ -19,6 +19,7 @@ package org.apache.nifi.registry.client.impl;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.client.FlowClient;
 import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
 import org.apache.nifi.registry.field.Fields;
 import org.apache.nifi.registry.flow.VersionedFlow;
 
@@ -184,5 +185,26 @@ public class JerseyFlowClient extends AbstractJerseyClient  implements FlowClien
         });
     }
 
+    @Override
+    public VersionedFlowDifference diff(final String bucketId, final String flowId,
+                                        final Integer versionA, final Integer versionB) throws NiFiRegistryException, IOException {
+        if (StringUtils.isBlank(bucketId)) {
+            throw new IllegalArgumentException("Bucket Identifier cannot be blank");
+        }
+
+        if (StringUtils.isBlank(flowId)) {
+            throw new IllegalArgumentException("Flow Identifier cannot be blank");
+        }
 
+        return executeAction("Error retrieving flow", () -> {
+            final WebTarget target = bucketFlowsTarget
+                    .path("/{flowId}/diff/{versionA}/{versionB}")
+                    .resolveTemplate("bucketId", bucketId)
+                    .resolveTemplate("flowId", flowId)
+                    .resolveTemplate("versionA", versionA)
+                    .resolveTemplate("versionB", versionB);
+
+            return  getRequestBuilder(target).get(VersionedFlowDifference.class);
+        });
+    }
 }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/915aa395/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifference.java
----------------------------------------------------------------------
diff --git a/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifference.java b/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifference.java
new file mode 100644
index 0000000..d4fb5d0
--- /dev/null
+++ b/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifference.java
@@ -0,0 +1,77 @@
+/*
+ * 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.registry.diff;
+
+import io.swagger.annotations.ApiModelProperty;
+
+/**
+ * Represents a specific, individual difference that has changed between 2 versions.
+ * The change data and textual descriptions of the change are included for client consumption.
+ */
+public class ComponentDifference {
+    private String valueA;
+    private String valueB;
+    private String changeDescription;
+    private String differenceType;
+    private String differenceTypeDescription;
+
+    @ApiModelProperty("The earlier value from the difference.")
+    public String getValueA() {
+        return valueA;
+    }
+
+    public void setValueA(String valueA) {
+        this.valueA = valueA;
+    }
+
+    @ApiModelProperty("The newer value from the difference.")
+    public String getValueB() {
+        return valueB;
+    }
+
+    public void setValueB(String valueB) {
+        this.valueB = valueB;
+    }
+
+    @ApiModelProperty("The description of the change.")
+    public String getChangeDescription() {
+        return changeDescription;
+    }
+
+    public void setChangeDescription(String changeDescription) {
+        this.changeDescription = changeDescription;
+    }
+
+    @ApiModelProperty("The key to the difference.")
+    public String getDifferenceType() {
+        return differenceType;
+    }
+
+    public void setDifferenceType(String differenceType) {
+        this.differenceType = differenceType;
+    }
+
+    @ApiModelProperty("The description of the change type.")
+    public String getDifferenceTypeDescription() {
+        return differenceTypeDescription;
+    }
+
+    public void setDifferenceTypeDescription(String differenceTypeDescription) {
+        this.differenceTypeDescription = differenceTypeDescription;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/915aa395/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifferenceGroup.java
----------------------------------------------------------------------
diff --git a/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifferenceGroup.java b/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifferenceGroup.java
new file mode 100644
index 0000000..a7b1d58
--- /dev/null
+++ b/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/ComponentDifferenceGroup.java
@@ -0,0 +1,96 @@
+/*
+ * 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.registry.diff;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Represents a group of differences related to a specific component in a flow.
+ */
+public class ComponentDifferenceGroup {
+    private String componentId;
+    private String componentName;
+    private String componentType;
+    private String processGroupId;
+    private Set<ComponentDifference> differences = new HashSet<>();
+
+    @ApiModelProperty("The id of the component whose changes are grouped together.")
+    public String getComponentId() {
+        return componentId;
+    }
+
+    public void setComponentId(String componentId) {
+        this.componentId = componentId;
+    }
+
+    @ApiModelProperty("The name of the component whose changes are grouped together.")
+    public String getComponentName() {
+        return componentName;
+    }
+
+    public void setComponentName(String componentName) {
+        this.componentName = componentName;
+    }
+
+    @ApiModelProperty("The type of component these changes relate to.")
+    public String getComponentType() {
+        return componentType;
+    }
+
+    public void setComponentType(String componentType) {
+        this.componentType = componentType;
+    }
+
+    @ApiModelProperty("The process group id for this component.")
+    public String getProcessGroupId() {
+        return processGroupId;
+    }
+
+    public void setProcessGroupId(String processGroupId) {
+        this.processGroupId = processGroupId;
+    }
+
+    @ApiModelProperty("The list of changes related to this component between the 2 versions.")
+    public Set<ComponentDifference> getDifferences() {
+        return differences;
+    }
+
+    public void setDifferences(Set<ComponentDifference> differences) {
+        this.differences = differences;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        ComponentDifferenceGroup that = (ComponentDifferenceGroup) o;
+        return Objects.equals(componentId, that.componentId)
+                && Objects.equals(componentName, that.componentName)
+                && Objects.equals(componentType, that.componentType)
+                && Objects.equals(processGroupId, that.processGroupId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(componentId, componentName, componentType, processGroupId);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/915aa395/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/VersionedFlowDifference.java
----------------------------------------------------------------------
diff --git a/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/VersionedFlowDifference.java b/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/VersionedFlowDifference.java
new file mode 100644
index 0000000..ccc6a05
--- /dev/null
+++ b/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/diff/VersionedFlowDifference.java
@@ -0,0 +1,79 @@
+/*
+ * 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.registry.diff;
+
+import io.swagger.annotations.ApiModelProperty;
+
+import java.util.Set;
+
+/**
+ * Represents the result of a diff between 2 versions of the same flow.
+ * A subset of the model classes in registry.flow.diff for exposing on the API
+ * The differences are grouped by component
+ */
+public class VersionedFlowDifference {
+    private String bucketId;
+    private String flowId;
+    private int versionA;
+    private int versionB;
+    private Set<ComponentDifferenceGroup> componentDifferenceGroups;
+
+    public Set<ComponentDifferenceGroup> getComponentDifferenceGroups() {
+        return componentDifferenceGroups;
+    }
+
+    public void setComponentDifferenceGroups(Set<ComponentDifferenceGroup> componentDifferenceGroups) {
+        this.componentDifferenceGroups = componentDifferenceGroups;
+    }
+
+    @ApiModelProperty("The id of the bucket that the flow is stored in.")
+    public String getBucketId() {
+        return bucketId;
+    }
+
+    public void setBucketId(String bucketId) {
+        this.bucketId = bucketId;
+    }
+
+    @ApiModelProperty("The id of the flow that is being examined.")
+    public String getFlowId() {
+        return flowId;
+    }
+
+    public void setFlowId(String flowId) {
+        this.flowId = flowId;
+    }
+
+    @ApiModelProperty("The earlier version from the diff operation.")
+    public int getVersionA() {
+        return versionA;
+    }
+
+    public void setVersionA(int versionA) {
+        this.versionA = versionA;
+    }
+
+    @ApiModelProperty("The latter version from the diff operation.")
+    public int getVersionB() {
+        return versionB;
+    }
+
+    public void setVersionB(int versionB) {
+        this.versionB = versionB;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/915aa395/nifi-registry-framework/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-framework/pom.xml b/nifi-registry-framework/pom.xml
index 423fbd5..16fde75 100644
--- a/nifi-registry-framework/pom.xml
+++ b/nifi-registry-framework/pom.xml
@@ -313,6 +313,11 @@
             <artifactId>apacheds-all</artifactId>
             <version>2.0.0-M24</version>
             <scope>test</scope>
+	</dependency>
+	<dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-flow-diff</artifactId>
+            <version>0.1.1-SNAPSHOT</version>
         </dependency>
     </dependencies>
 </project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/915aa395/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
----------------------------------------------------------------------
diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
index 1dbf49a..3436662 100644
--- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
+++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
@@ -22,8 +22,12 @@ import org.apache.nifi.registry.db.entity.BucketItemEntityType;
 import org.apache.nifi.registry.db.entity.FlowEntity;
 import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
 import org.apache.nifi.registry.db.entity.KeyEntity;
+import org.apache.nifi.registry.diff.ComponentDifference;
+import org.apache.nifi.registry.diff.ComponentDifferenceGroup;
+import org.apache.nifi.registry.flow.VersionedComponent;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
+import org.apache.nifi.registry.flow.diff.FlowDifference;
 import org.apache.nifi.registry.security.key.Key;
 
 import java.util.Date;
@@ -112,6 +116,35 @@ public class DataModelMapper {
         return metadata;
     }
 
+    public static ComponentDifference map(final FlowDifference flowDifference){
+        ComponentDifference diff = new ComponentDifference();
+        diff.setChangeDescription(flowDifference.getDescription());
+        diff.setDifferenceType(flowDifference.getDifferenceType().toString());
+        diff.setDifferenceTypeDescription(flowDifference.getDifferenceType().getDescription());
+        diff.setValueA(getValueDescription(flowDifference.getValueA()));
+        diff.setValueB(getValueDescription(flowDifference.getValueB()));
+        return diff;
+    }
+
+    public static ComponentDifferenceGroup map(VersionedComponent versionedComponent){
+        ComponentDifferenceGroup grouping = new ComponentDifferenceGroup();
+        grouping.setComponentId(versionedComponent.getIdentifier());
+        grouping.setComponentName(versionedComponent.getName());
+        grouping.setProcessGroupId(versionedComponent.getGroupIdentifier());
+        grouping.setComponentType(versionedComponent.getComponentType().getTypeName());
+        return grouping;
+    }
+
+    private static String getValueDescription(Object valueA){
+        if(valueA instanceof VersionedComponent){
+            return ((VersionedComponent) valueA).getIdentifier();
+        }
+        if(valueA!= null){
+            return valueA.toString();
+        }
+        return null;
+    }
+
     // --- Map keys
 
     public static Key map(final KeyEntity keyEntity) {
@@ -130,4 +163,6 @@ public class DataModelMapper {
         return keyEntity;
     }
 
+    // map
+
 }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/915aa395/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
index e805520..fad517f 100644
--- a/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
+++ b/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
@@ -16,6 +16,7 @@
  */
 package org.apache.nifi.registry.service;
 
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.Validate;
 import org.apache.nifi.registry.bucket.Bucket;
@@ -24,13 +25,23 @@ import org.apache.nifi.registry.db.entity.BucketEntity;
 import org.apache.nifi.registry.db.entity.BucketItemEntity;
 import org.apache.nifi.registry.db.entity.FlowEntity;
 import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+import org.apache.nifi.registry.diff.ComponentDifferenceGroup;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
 import org.apache.nifi.registry.exception.ResourceNotFoundException;
 import org.apache.nifi.registry.flow.FlowPersistenceProvider;
 import org.apache.nifi.registry.flow.FlowSnapshotContext;
+import org.apache.nifi.registry.flow.VersionedComponent;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.diff.ComparableDataFlow;
+import org.apache.nifi.registry.flow.diff.ConciseEvolvingDifferenceDescriptor;
+import org.apache.nifi.registry.flow.diff.FlowComparator;
+import org.apache.nifi.registry.flow.diff.FlowComparison;
+import org.apache.nifi.registry.flow.diff.FlowDifference;
+import org.apache.nifi.registry.flow.diff.StandardComparableDataFlow;
+import org.apache.nifi.registry.flow.diff.StandardFlowComparator;
 import org.apache.nifi.registry.provider.flow.StandardFlowSnapshotContext;
 import org.apache.nifi.registry.serialization.Serializer;
 import org.slf4j.Logger;
@@ -47,7 +58,9 @@ import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
@@ -781,6 +794,98 @@ public class RegistryService {
         }
     }
 
+    /**
+     * Returns the differences between two specified versions of a flow.
+     *
+     * @param bucketIdentifier the id of the bucket the flow exists in
+     * @param flowIdentifier the flow to be examined
+     * @param versionA the first version of the comparison
+     * @param versionB the second version of the comparison
+     * @return The differences between two specified versions, grouped by component.
+     */
+    public VersionedFlowDifference getFlowDiff(final String bucketIdentifier, final String flowIdentifier,
+                                               final Integer versionA, final Integer versionB) {
+        if (StringUtils.isBlank(bucketIdentifier)) {
+            throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
+        }
+
+        if (StringUtils.isBlank(flowIdentifier)) {
+            throw new IllegalArgumentException("Flow identifier cannot be null or blank");
+        }
+
+        if (versionA == null || versionB == null) {
+            throw new IllegalArgumentException("Version cannot be null or blank");
+        }
+        // older version is always the lower, regardless of the order supplied
+        final Integer older = Math.min(versionA, versionB);
+        final Integer newer = Math.max(versionA, versionB);
+
+        readLock.lock();
+        try {
+            // Get the content for both versions of the flow
+            final byte[] serializedSnapshotA = flowPersistenceProvider.getFlowContent(bucketIdentifier, flowIdentifier, older);
+            if (serializedSnapshotA == null || serializedSnapshotA.length == 0) {
+                throw new IllegalStateException("No serialized content found for snapshot with flow identifier "
+                        + flowIdentifier + " and version " + older);
+            }
+
+            final byte[] serializedSnapshotB = flowPersistenceProvider.getFlowContent(bucketIdentifier, flowIdentifier, newer);
+            if (serializedSnapshotB == null || serializedSnapshotB.length == 0) {
+                throw new IllegalStateException("No serialized content found for snapshot with flow identifier "
+                        + flowIdentifier + " and version " + newer);
+            }
+
+            // deserialize the contents
+            final InputStream inputA = new ByteArrayInputStream(serializedSnapshotA);
+            final VersionedProcessGroup flowContentsA = processGroupSerializer.deserialize(inputA);
+            final InputStream inputB = new ByteArrayInputStream(serializedSnapshotB);
+            final VersionedProcessGroup flowContentsB = processGroupSerializer.deserialize(inputB);
+
+            final ComparableDataFlow comparableFlowA = new StandardComparableDataFlow(String.format("Version %d", older), flowContentsA);
+            final ComparableDataFlow comparableFlowB = new StandardComparableDataFlow(String.format("Version %d", newer), flowContentsB);
+
+            // Compare the two versions of the flow
+            final FlowComparator flowComparator = new StandardFlowComparator(comparableFlowA, comparableFlowB,
+                    null, new ConciseEvolvingDifferenceDescriptor());
+            final FlowComparison flowComparison = flowComparator.compare();
+
+            VersionedFlowDifference result = new VersionedFlowDifference();
+            result.setBucketId(bucketIdentifier);
+            result.setFlowId(flowIdentifier);
+            result.setVersionA(older);
+            result.setVersionB(newer);
+
+            Set<ComponentDifferenceGroup> differenceGroups = getStringComponentDifferenceGroupMap(flowComparison.getDifferences());
+            result.setComponentDifferenceGroups(differenceGroups);
+
+            return result;
+        } finally {
+            readLock.unlock();
+        }
+    }
+
+    /**
+     * Group the differences in the comparison by component
+     * @param flowDifferences The differences to group together by component
+     * @return A set of componentDifferenceGroups where each entry contains a set of differences specific to that group
+     */
+    private Set<ComponentDifferenceGroup> getStringComponentDifferenceGroupMap(Set<FlowDifference> flowDifferences) {
+        Map<String, ComponentDifferenceGroup> differenceGroups = new HashMap<>();
+        for (FlowDifference diff : flowDifferences) {
+            ComponentDifferenceGroup group;
+            // A component may only exist on only one version for new/removed components
+            VersionedComponent component = ObjectUtils.firstNonNull(diff.getComponentA(), diff.getComponentB());
+            if(differenceGroups.containsKey(component.getIdentifier())){
+                group = differenceGroups.get(component.getIdentifier());
+            }else{
+                group = DataModelMapper.map(component);
+                differenceGroups.put(component.getIdentifier(), group);
+            }
+            group.getDifferences().add(DataModelMapper.map(diff));
+        }
+        return differenceGroups.values().stream().collect(Collectors.toSet());
+    }
+
     // ---------------------- Field methods ---------------------------------------------
 
     public Set<String> getBucketFields() {

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/915aa395/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
----------------------------------------------------------------------
diff --git a/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java b/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
index 26467ca..f593518 100644
--- a/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
+++ b/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
@@ -20,12 +20,16 @@ import org.apache.nifi.registry.bucket.Bucket;
 import org.apache.nifi.registry.db.entity.BucketEntity;
 import org.apache.nifi.registry.db.entity.FlowEntity;
 import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
+import org.apache.nifi.registry.diff.ComponentDifference;
+import org.apache.nifi.registry.diff.ComponentDifferenceGroup;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
 import org.apache.nifi.registry.exception.ResourceNotFoundException;
 import org.apache.nifi.registry.flow.FlowPersistenceProvider;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
+import org.apache.nifi.registry.flow.VersionedProcessor;
 import org.apache.nifi.registry.serialization.Serializer;
 import org.apache.nifi.registry.serialization.VersionedProcessGroupSerializer;
 import org.junit.Before;
@@ -43,16 +47,20 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.SortedSet;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
@@ -1006,6 +1014,85 @@ public class TestRegistryService {
         return existingBucket;
     }
 
+    // -----------------Test Flow Diff Service Method---------------------
+    @Test
+    public void testGetDiffReturnsRemovedComponentChanges() {
+        when(flowPersistenceProvider.getFlowContent(
+                anyString(), anyString(), anyInt()
+        )).thenReturn(new byte[10], new byte[10]);
+
+        final VersionedProcessGroup pgA = createVersionedProcessGroupA();
+        final VersionedProcessGroup pgB = createVersionedProcessGroupB();
+        when(snapshotSerializer.deserialize(any())).thenReturn(pgA, pgB);
+
+        final VersionedFlowDifference diff = registryService.getFlowDiff(
+                "bucketIdentifier", "flowIdentifier", 1, 2);
+
+        assertNotNull(diff);
+        Optional<ComponentDifferenceGroup> removedComponent = diff.getComponentDifferenceGroups().stream()
+                .filter(p->p.getComponentId().equals("ID-pg1")).findFirst();
+
+        assertTrue(removedComponent.isPresent());
+        assertTrue(removedComponent.get().getDifferences().iterator().next().getDifferenceType().equals("COMPONENT_REMOVED"));
+    }
+
+    @Test
+    public void testGetDiffReturnsChangesInChronologicalOrder() {
+        when(flowPersistenceProvider.getFlowContent(
+                anyString(), anyString(), anyInt()
+        )).thenReturn(new byte[10], new byte[10]);
+
+        final VersionedProcessGroup pgA = createVersionedProcessGroupA();
+        final VersionedProcessGroup pgB = createVersionedProcessGroupB();
+        when(snapshotSerializer.deserialize(any())).thenReturn(pgA, pgB);
+
+        // getFlowDiff orders the changes in ascending order of version number regardless of param order
+        final VersionedFlowDifference diff = registryService.getFlowDiff(
+                "bucketIdentifier", "flowIdentifier", 2,1);
+
+        assertNotNull(diff);
+        Optional<ComponentDifferenceGroup> nameChangedComponent = diff.getComponentDifferenceGroups().stream()
+                .filter(p->p.getComponentId().equals("ProcessorFirstV1")).findFirst();
+
+        assertTrue(nameChangedComponent.isPresent());
+
+        ComponentDifference nameChangeDifference = nameChangedComponent.get().getDifferences().stream()
+                .filter(d-> d.getDifferenceType().equals("NAME_CHANGED")).findFirst().get();
+
+        assertEquals("ProcessorFirstV1", nameChangeDifference.getValueA());
+        assertEquals("ProcessorFirstV2", nameChangeDifference.getValueB());
+    }
+
+    private VersionedProcessGroup createVersionedProcessGroupA() {
+        VersionedProcessGroup root = new VersionedProcessGroup();
+        root.setProcessGroups(new HashSet<>(Arrays.asList(createProcessGroup("ID-pg1"), createProcessGroup("ID-pg2"))));
+        // Add processors
+        root.setProcessors(new HashSet<>(Arrays.asList(createVersionedProcessor("ProcessorFirstV1"), createVersionedProcessor("ProcessorSecondV1"))));
+        return root;
+    }
+
+    private VersionedProcessGroup createProcessGroup(String identifier){
+        VersionedProcessGroup processGroup = new VersionedProcessGroup();
+        processGroup.setIdentifier(identifier);
+        return processGroup;
+    }
+    private VersionedProcessGroup createVersionedProcessGroupB() {
+        VersionedProcessGroup updated = createVersionedProcessGroupA();
+        // remove a process group
+        updated.getProcessGroups().removeIf(pg->pg.getIdentifier().equals("ID-pg1"));
+        // change the name of a processor
+        updated.getProcessors().stream().forEach(p->p.setPenaltyDuration(p.getName().equals("ProcessorFirstV1") ? "1" : "2"));
+        updated.getProcessors().stream().forEach(p->p.setName(p.getName().equals("ProcessorFirstV1") ? "ProcessorFirstV2" : p.getName()));
+        return updated;
+    }
+
+    private VersionedProcessor createVersionedProcessor(String name){
+        VersionedProcessor processor = new VersionedProcessor();
+        processor.setName(name);
+        processor.setIdentifier(name);
+        processor.setProperties(new HashMap<>());
+        return processor;
+    }
     // -------------------------------------------------------------------
 
     private Answer<BucketEntity> createBucketAnswer() {

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/915aa395/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
index 8d03a03..2e58723 100644
--- a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
+++ b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
@@ -25,6 +25,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.nifi.registry.bucket.BucketItem;
 import org.apache.nifi.registry.exception.ResourceNotFoundException;
 import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.security.authorization.Authorizer;
@@ -402,6 +403,32 @@ public class BucketFlowResource extends AuthorizableApplicationResource {
         return Response.status(Response.Status.OK).entity(snapshot).build();
     }
 
+    @GET
+    @Path("{flowId}/diff/{versionA: \\d+}/{versionB: \\d+}")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Returns a list of differences between 2 versions of a flow",
+            response = VersionedFlowDifference.class
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409)})
+    public Response getFlowDiff(@PathParam("bucketId")
+                                @ApiParam("The bucket identifier") final String bucketId,
+                                @PathParam("flowId")
+                                @ApiParam("The flow identifier") final String flowId,
+                                @PathParam("versionA")
+                                @ApiParam("The first version number") final Integer versionNumberA,
+                                @PathParam("versionB")
+                                @ApiParam("The second version number") final Integer versionNumberB) {
+        VersionedFlowDifference result = registryService.getFlowDiff(bucketId, flowId, versionNumberA, versionNumberB);
+        return Response.status(Response.Status.OK).entity(result).build();
+    }
+
     private void populateLinksAndPermissions(VersionedFlowSnapshot snapshot) {
         if (snapshot.getSnapshotMetadata() != null) {
             linkService.populateSnapshotLinks(snapshot.getSnapshotMetadata());

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/915aa395/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
index a12056b..c733f26 100644
--- a/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
+++ b/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
@@ -29,12 +29,14 @@ import org.apache.nifi.registry.client.NiFiRegistryClientConfig;
 import org.apache.nifi.registry.client.NiFiRegistryException;
 import org.apache.nifi.registry.client.UserClient;
 import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient;
+import org.apache.nifi.registry.diff.VersionedFlowDifference;
 import org.apache.nifi.registry.field.Fields;
 import org.apache.nifi.registry.flow.VersionedFlow;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
 import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
 import org.apache.nifi.registry.flow.VersionedProcessGroup;
 import org.apache.nifi.registry.flow.VersionedProcessor;
+import org.apache.nifi.registry.flow.VersionedPropertyDescriptor;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -265,6 +267,19 @@ public class UnsecuredNiFiRegistryClientIT extends UnsecuredITBase {
         allItems.stream().forEach(i -> Assert.assertNotNull(i.getBucketName()));
         bucketItems.stream().forEach(i -> LOGGER.info("Items in bucket, item " + i.getIdentifier()));
 
+        // ----------------------- TEST DIFF ---------------------------//
+
+        final VersionedFlowSnapshot snapshot3 = buildSnapshot(snapshotFlow, 3);
+        final VersionedProcessGroup newlyAddedPG = new VersionedProcessGroup();
+        newlyAddedPG.setIdentifier("new-pg");
+        newlyAddedPG.setName("NEW Process Group");
+        snapshot3.getFlowContents().getProcessGroups().add(newlyAddedPG);
+        snapshotClient.create(snapshot3);
+
+        VersionedFlowDifference diff = flowClient.diff(snapshotFlow.getBucketIdentifier(), snapshotFlow.getIdentifier(), 3, 2);
+        Assert.assertNotNull(diff);
+        Assert.assertEquals(1, diff.getComponentDifferenceGroups().size());
+
         // ---------------------- DELETE DATA --------------------------//
 
         final VersionedFlow deletedFlow1 = flowClient.delete(flowsBucket.getIdentifier(), flow1.getIdentifier());
@@ -302,7 +317,7 @@ public class UnsecuredNiFiRegistryClientIT extends UnsecuredITBase {
         return client.create(versionedFlow);
     }
 
-    private static VersionedFlowSnapshot createSnapshot(FlowSnapshotClient client, VersionedFlow flow, int num) throws IOException, NiFiRegistryException {
+    private static VersionedFlowSnapshot buildSnapshot(VersionedFlow flow, int num) {
         final VersionedFlowSnapshotMetadata snapshotMetadata = new VersionedFlowSnapshotMetadata();
         snapshotMetadata.setBucketIdentifier(flow.getBucketIdentifier());
         snapshotMetadata.setFlowIdentifier(flow.getIdentifier());
@@ -322,15 +337,19 @@ public class UnsecuredNiFiRegistryClientIT extends UnsecuredITBase {
         processorProperties.put("Prop 1", "Val 1");
         processorProperties.put("Prop 2", "Val 2");
 
+        final Map<String, VersionedPropertyDescriptor> propertyDescriptors = new HashMap<>();
+
         final VersionedProcessor processor1 = new VersionedProcessor();
         processor1.setIdentifier("p1");
         processor1.setName("Processor 1");
         processor1.setProperties(processorProperties);
+        processor1.setPropertyDescriptors(propertyDescriptors);
 
         final VersionedProcessor processor2 = new VersionedProcessor();
         processor2.setIdentifier("p2");
         processor2.setName("Processor 2");
         processor2.setProperties(processorProperties);
+        processor2.setPropertyDescriptors(propertyDescriptors);
 
         subProcessGroup.getProcessors().add(processor1);
         subProcessGroup.getProcessors().add(processor2);
@@ -338,8 +357,12 @@ public class UnsecuredNiFiRegistryClientIT extends UnsecuredITBase {
         final VersionedFlowSnapshot snapshot = new VersionedFlowSnapshot();
         snapshot.setSnapshotMetadata(snapshotMetadata);
         snapshot.setFlowContents(rootProcessGroup);
+        return snapshot;
+    }
+
+    private static VersionedFlowSnapshot createSnapshot(FlowSnapshotClient client, VersionedFlow flow, int num) throws IOException, NiFiRegistryException {
+        final VersionedFlowSnapshot snapshot = buildSnapshot(flow, num);
 
         return client.create(snapshot);
     }
-
 }