NIFIREG-300 Added nifi-registry-revision modules

- Added nifi-registry-revision modules containing NiFi's RevisionManager concept with a JDBC implementation and supporting utility modules
- Fixing Travis CI config to get builds running again

This closes #212.

Signed-off-by: Kevin Doran <kdoran@apache.org>
diff --git a/.travis.yml b/.travis.yml
index ab00146..a85ddbd 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -23,7 +23,7 @@
 os: linux
 
 jdk:
-    - oraclejdk8
+    - openjdk8
 
 # Caches mvn repository in order to speed up builds
 cache:
@@ -35,6 +35,9 @@
     # Remove nifi repo again to save travis from caching it
     - rm -rf $HOME/.m2/repository/org/apache/nifi-registry/
 
+services:
+    - xvfb
+
 addons:
     chrome: stable
 
@@ -47,7 +50,6 @@
 #   1. simulate an `X` server on Travis CI for karma tests that require a GUI
 before_script:
     - export DISPLAY=:99.0
-    - sh -e /etc/init.d/xvfb start
     - sleep 3 # give xvfb some time to start
 
 # skip the installation step entirely
diff --git a/nifi-registry-assembly/NOTICE b/nifi-registry-assembly/NOTICE
index 48fe7f4..9e4dca9 100644
--- a/nifi-registry-assembly/NOTICE
+++ b/nifi-registry-assembly/NOTICE
@@ -16,7 +16,7 @@
   (ASLv2) Jetty
     The following NOTICE information applies:
        Jetty Web Container
-       Copyright 1995-2017 Mort Bay Consulting Pty Ltd.
+       Copyright 1995-2019 Mort Bay Consulting Pty Ltd.
 
   (ASLv2) Apache Commons Codec
     The following NOTICE information applies:
@@ -165,13 +165,13 @@
 
   (ASLv2) Spring Framework
     The following NOTICE information applies:
-      Spring Framework 5.0.2.RELEASE
-      Copyright (c) 2002-2017 Pivotal, Inc.
+      Spring Framework 5.1.8.RELEASE
+      Copyright (c) 2002-2019 Pivotal, Inc.
 
   (ASLv2) Spring Security
     The following NOTICE information applies:
-      Spring Framework 5.0.5.RELEASE
-      Copyright (c) 2002-2017 Pivotal, Inc.
+      Spring Framework 5.1.5.RELEASE
+      Copyright (c) 2002-2019 Pivotal, Inc.
 
       This product includes software developed by Spring Security
       Project (https://www.springframework.org/security).
@@ -276,16 +276,16 @@
     (CDDL 1.1) (GPL2 w/ CPE) javax.inject:1 as OSGi bundle (org.glassfish.hk2.external:javax.inject:jar:2.4.0-b25 - https://hk2.java.net/external/javax.inject)
     (CDDL 1.1) (GPL2 w/ CPE) javax.ws.rs-api (javax.ws.rs:javax.ws.rs-api:jar:2.1 - https://jax-rs-spec.java.net)
     (CDDL 1.1) (GPL2 w/ CPE) javax.el (org.glassfish:javax.el:jar:3.0.1-b08 - https://github.com/javaee/el-spec)
-    (CDDL 1.1) (GPL2 w/ CPE) jersey-bean-validation (org.glassfish.jersey.ext:jersey-bean-validation:jar:2.26 - https://jersey.github.io/)
-    (CDDL 1.1) (GPL2 w/ CPE) jersey-client (org.glassfish.jersey.core:jersey-client:jar:2.26 - https://jersey.github.io/)
-    (CDDL 1.1) (GPL2 w/ CPE) jersey-common (org.glassfish.jersey.core:jersey-common:jar:2.26 - https://jersey.github.io/)
-    (CDDL 1.1) (GPL2 w/ CPE) jersey-container-servlet-core (org.glassfish.jersey.containers:jersey-container-servlet-core:jar:2.26 - https://jersey.github.io/)
-    (CDDL 1.1) (GPL2 w/ CPE) jersey-entity-filtering (org.glassfish.jersey.ext:jersey-entity-filtering:jar:2.26 - https://jersey.github.io/)
-    (CDDL 1.1) (GPL2 w/ CPE) jersey-hk2 (org.glassfish.jersey.inject:jersey-hk2:jar:2.26 - https://jersey.github.io/)
-    (CDDL 1.1) (GPL2 w/ CPE) jersey-media-jaxb (org.glassfish.jersey.media:jersey-media-jaxb:jar:2.26 - https://jersey.github.io/)
-    (CDDL 1.1) (GPL2 w/ CPE) jersey-media-json-jackson (org.glassfish.jersey.media:jersey-media-json-jackson:jar:2.26 - https://jersey.github.io/)
-    (CDDL 1.1) (GPL2 w/ CPE) jersey-server (org.glassfish.jersey.core:jersey-server:jar:2.26 - https://jersey.github.io/)
-    (CDDL 1.1) (GPL2 w/ CPE) jersey-spring4 (org.glassfish.jersey.ext:jersey-spring4:jar:2.26 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-bean-validation (org.glassfish.jersey.ext:jersey-bean-validation:jar:2.27 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-client (org.glassfish.jersey.core:jersey-client:jar:2.27 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-common (org.glassfish.jersey.core:jersey-common:jar:2.27 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-container-servlet-core (org.glassfish.jersey.containers:jersey-container-servlet-core:jar:2.27 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-entity-filtering (org.glassfish.jersey.ext:jersey-entity-filtering:jar:2.27 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-hk2 (org.glassfish.jersey.inject:jersey-hk2:jar:2.27 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-media-jaxb (org.glassfish.jersey.media:jersey-media-jaxb:jar:2.27 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-media-json-jackson (org.glassfish.jersey.media:jersey-media-json-jackson:jar:2.27 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-server (org.glassfish.jersey.core:jersey-server:jar:2.27 - https://jersey.github.io/)
+    (CDDL 1.1) (GPL2 w/ CPE) jersey-spring4 (org.glassfish.jersey.ext:jersey-spring4:jar:2.27 - https://jersey.github.io/)
     (CDDL 1.1) (GPL2 w/ CPE) OSGi resource locator bundle (org.glassfish.hk2:osgi-resource-locator:jar:1.0.1 - https://glassfish.org/osgi-resource-locator)
 
 
@@ -304,7 +304,7 @@
 
 The following binary components are provided under the Eclipse Public License 1.0.  See project link for details.
 
-    (EPL 1.0)(MPL 2.0) H2 Database (com.h2database:h2:jar:1.3.176 - https://www.h2database.com/html/license.html)
+    (EPL 1.0)(MPL 2.0) H2 Database (com.h2database:h2:jar:h2-1.4.199 - https://www.h2database.com/html/license.html)
     (EPL 1.0)(LGPL 2.1) Logback Classic (ch.qos.logback:logback-classic:jar:1.2.3 - https://logback.qos.ch/)
     (EPL 1.0)(LGPL 2.1) Logback Core (ch.qos.logback:logback-core:jar:1.2.3 - https://logback.qos.ch/)
     (EPL 1.0) AspectJ Weaver (org.aspectj:aspectjweaver:jar:1.8.13 - https://www.eclipse.org/aspectj/)
diff --git a/nifi-registry-core/nifi-registry-framework/pom.xml b/nifi-registry-core/nifi-registry-framework/pom.xml
index 8fec098..8962ef7 100644
--- a/nifi-registry-core/nifi-registry-framework/pom.xml
+++ b/nifi-registry-core/nifi-registry-framework/pom.xml
@@ -294,7 +294,7 @@
         <dependency>
             <groupId>com.h2database</groupId>
             <artifactId>h2</artifactId>
-            <version>1.4.196</version>
+            <version>${h2.version}</version>
         </dependency>
         <dependency>
             <groupId>org.eclipse.jgit</groupId>
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/pom.xml b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/pom.xml
new file mode 100644
index 0000000..b6ef934
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/pom.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-revision</artifactId>
+        <version>0.5.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-revision-api</artifactId>
+    <packaging>jar</packaging>
+
+
+</project>
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/DeleteRevisionTask.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/DeleteRevisionTask.java
new file mode 100644
index 0000000..a31c5b5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/DeleteRevisionTask.java
@@ -0,0 +1,29 @@
+/*
+ * 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.revision.api;
+
+/**
+ * A task that is responsible for deleting some entities.
+ *
+ * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as
+ * the NiFi PMC and committers deem necessary. It is not considered a public extension point.
+ */
+public interface DeleteRevisionTask<T> {
+
+    T performTask();
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/EntityModification.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/EntityModification.java
new file mode 100644
index 0000000..a693582
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/EntityModification.java
@@ -0,0 +1,64 @@
+/*
+ * 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.revision.api;
+
+/**
+ * A holder for a Revision and the identity of the user that made the last modification.
+ *
+ * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as
+ * the NiFi PMC and committers deem necessary. It is not considered a public extension point.
+ */
+public class EntityModification {
+
+    private final Revision revision;
+    private final String lastModifier;
+
+    /**
+     * Creates a new EntityModification.
+     *
+     * @param revision revision
+     * @param lastModifier modifier
+     */
+    public EntityModification(final Revision revision, final String lastModifier) {
+        this.revision = revision;
+        this.lastModifier = lastModifier;
+    }
+
+    /**
+     * Get the revision.
+     *
+     * @return the revision
+     */
+    public Revision getRevision() {
+        return revision;
+    }
+
+    /**
+     * Get the last modifier.
+     *
+     * @return the modifier
+     */
+    public String getLastModifier() {
+        return lastModifier;
+    }
+
+    @Override
+    public String toString() {
+        return "Last Modified by '" + lastModifier + "' with Revision " + revision;
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/ExpiredRevisionClaimException.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/ExpiredRevisionClaimException.java
new file mode 100644
index 0000000..2dbb4f2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/ExpiredRevisionClaimException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.revision.api;
+
+/**
+ * An exception to be thrown when an expired RevisionClaim is encountered.
+ *
+ * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as
+ * the NiFi PMC and committers deem necessary. It is not considered a public extension point.
+ */
+public class ExpiredRevisionClaimException extends InvalidRevisionException {
+    private static final long serialVersionUID = 5648579322377770273L;
+
+    public ExpiredRevisionClaimException(final String message) {
+        super(message);
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/InvalidRevisionException.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/InvalidRevisionException.java
new file mode 100644
index 0000000..84f67bb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/InvalidRevisionException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.revision.api;
+
+/**
+ * Exception indicating that the client has included an old revision in their request.
+ *
+ * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as
+ * the NiFi PMC and committers deem necessary. It is not considered a public extension point.
+ */
+public class InvalidRevisionException extends RuntimeException {
+
+    public InvalidRevisionException(String message) {
+        super(message);
+    }
+
+    public InvalidRevisionException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/Revision.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/Revision.java
new file mode 100644
index 0000000..1404c06
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/Revision.java
@@ -0,0 +1,117 @@
+/*
+ * 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.revision.api;
+
+/**
+ * A revision for an entity which is made up of the entity id, a version, and an optional client id.
+ *
+ * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as
+ * the NiFi PMC and committers deem necessary. It is not considered a public extension point.
+ */
+public class Revision {
+
+    private final Long version;
+    private final String clientId;
+    private final String entityId;
+
+    /**
+     * @param version the version number for the revision
+     * @param clientId the id of the client creating the revision, or null if one is not provided
+     * @param entityId the id of the component the revision belongs to
+     */
+    public Revision(final Long version, final String clientId, final String entityId) {
+        if (version == null) {
+            throw new IllegalArgumentException("The revision must be specified.");
+        }
+        if (entityId == null) {
+            throw new IllegalArgumentException("The entityId must be specified.");
+        }
+
+        this.version = version;
+        this.clientId = clientId;
+        this.entityId = entityId;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public Long getVersion() {
+        return version;
+    }
+
+    public String getEntityId() {
+        return entityId;
+    }
+
+    /**
+     * Returns a new Revision that has the same Client ID and Component ID as this one, but with a larger version.
+     *
+     * @return the updated Revision
+     */
+    public Revision incrementRevision(final String clientId) {
+        return new Revision(version + 1, clientId, entityId);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == null) {
+            return false;
+        }
+        if (obj == this) {
+            return true;
+        }
+
+        if ((obj instanceof Revision) == false) {
+            return false;
+        }
+
+        final Revision thatRevision = (Revision) obj;
+        // ensure that component ID's are the same (including null)
+        if (thatRevision.getEntityId() == null && getEntityId() != null) {
+            return false;
+        }
+        if (thatRevision.getEntityId() != null && getEntityId() == null) {
+            return false;
+        }
+        if (thatRevision.getEntityId() != null && !thatRevision.getEntityId().equals(getEntityId())) {
+            return false;
+        }
+
+        if (this.version != null && this.version.equals(thatRevision.version)) {
+            return true;
+        } else {
+            return clientId != null && !clientId.trim().isEmpty() && clientId.equals(thatRevision.getClientId());
+        }
+
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 5;
+        hash = 59 * hash + (this.entityId != null ? this.entityId.hashCode() : 0);
+        hash = 59 * hash + (this.version != null ? this.version.hashCode() : 0);
+        hash = 59 * hash + (this.clientId != null ? this.clientId.hashCode() : 0);
+        return hash;
+    }
+
+    @Override
+    public String toString() {
+        return "[" + version + ", " + clientId + ", " + entityId + ']';
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionClaim.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionClaim.java
new file mode 100644
index 0000000..8e0a04e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionClaim.java
@@ -0,0 +1,31 @@
+/*
+ * 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.revision.api;
+
+import java.util.Set;
+
+/**
+ * A set of Revisions submitted by a client.
+ *
+ * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as
+ * the NiFi PMC and committers deem necessary. It is not considered a public extension point.
+ */
+public interface RevisionClaim {
+
+    Set<Revision> getRevisions();
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionManager.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionManager.java
new file mode 100644
index 0000000..0f20c35
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionManager.java
@@ -0,0 +1,92 @@
+/*
+ * 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.revision.api;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * A Revision Manager provides the ability to prevent clients of the Web API from stepping on one another.
+ * This is done by providing revisions for entities individually.
+ * </p>
+ *
+ * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as
+ * the NiFi PMC and committers deem necessary. It is not considered a public extension point.
+ */
+public interface RevisionManager {
+
+    /**
+     * Returns the current Revision for the entity with the given ID. If no Revision yet exists for the
+     * entity with the given ID, one will be created with a Version of 0 and no Client ID.
+     *
+     * @param entityId the ID of the entity
+     * @return the current Revision for the entity with the given ID
+     */
+    Revision getRevision(String entityId);
+
+    /**
+     * Performs the given task without allowing the given Revision Claim to expire. Once this method
+     * returns or an Exception is thrown (with the Exception of ExpiredRevisionClaimException),
+     * the Revision may have been updated for each entity that the RevisionClaim holds a Claim for.
+     * If an ExpiredRevisionClaimException is thrown, the Revisions claimed by RevisionClaim
+     * will not be updated.
+     *
+     * @param claim the Revision Claim that is responsible for holding a Claim on the Revisions for each entity that is
+     *            to be updated
+     * @param task the task that is responsible for updating the entities whose Revisions are claimed by the given
+     *            RevisionClaim. The returned Revision set should include a Revision for each Revision that is the
+     *            supplied Revision Claim. If there exists any Revision in the provided RevisionClaim that is not part
+     *            of the RevisionClaim returned by the task, then the Revision is assumed to have not been modified.
+     *
+     * @return a RevisionUpdate object that represents the new version of the entity that was updated
+     *
+     * @throws ExpiredRevisionClaimException if the Revision Claim has expired
+     */
+    <T> RevisionUpdate<T> updateRevision(RevisionClaim claim, UpdateRevisionTask<T> task);
+
+    /**
+     * Performs the given task that is expected to remove a entity from the flow. As a result,
+     * the Revision for the entity referenced by the RevisionClaim will be removed.
+     *
+     * @param claim the Revision Claim that is responsible for holding a Claim on the Revisions for each entity that is
+     *            to be removed
+     * @param task the task that is responsible for deleting the entities whose Revisions are claimed by the given RevisionClaim
+     * @return the value returned from the DeleteRevisionTask
+     *
+     * @throws ExpiredRevisionClaimException if the Revision Claim has expired
+     */
+    <T> T deleteRevision(RevisionClaim claim, DeleteRevisionTask<T> task) throws ExpiredRevisionClaimException;
+
+    /**
+     * Clears any revisions that are currently held and resets the Revision Manager so that the revisions
+     * present are those provided in the given collection
+     */
+    void reset(Collection<Revision> revisions);
+
+    /**
+     * @return a List of all Revisions managed by this Revision Manager
+     */
+    List<Revision> getAllRevisions();
+
+    /**
+     * @return a Map of all Revisions where the key is the entity id
+     */
+    Map<String,Revision> getRevisionMap();
+
+}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionUpdate.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionUpdate.java
new file mode 100644
index 0000000..8785110
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/RevisionUpdate.java
@@ -0,0 +1,43 @@
+/*
+ * 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.revision.api;
+
+import java.util.Set;
+
+/**
+ * A packaging of an entity and the corresponding Revision for that component.
+ *
+ * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as
+ * the NiFi PMC and committers deem necessary. It is not considered a public extension point.
+ */
+public interface RevisionUpdate<T> {
+
+    /**
+     * @return the entity that was updated
+     */
+    T getEntity();
+
+    /**
+     * @return the last modification that was made for this component
+     */
+    EntityModification getLastModification();
+
+    /**
+     * @return a Set of all Revisions that were updated
+     */
+    Set<Revision> getUpdatedRevisions();
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/UpdateRevisionTask.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/UpdateRevisionTask.java
new file mode 100644
index 0000000..3db8f9f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-api/src/main/java/org/apache/nifi/registry/revision/api/UpdateRevisionTask.java
@@ -0,0 +1,34 @@
+/*
+ * 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.revision.api;
+
+/**
+ * <p>
+ * A task that is responsible for updating some entities.
+ * </p>
+ *
+ * NOTE: This API is considered a framework level API for the NiFi ecosystem and may evolve as
+ * the NiFi PMC and committers deem necessary. It is not considered a public extension point.
+ */
+public interface UpdateRevisionTask<T> {
+    /**
+     * Updates one or more entities and returns updated Revisions for those entities.
+     *
+     * @return the updated revisions for the entities
+     */
+    RevisionUpdate<T> update();
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/pom.xml b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/pom.xml
new file mode 100644
index 0000000..b9a6b01
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/pom.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-revision</artifactId>
+        <version>0.5.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-revision-common</artifactId>
+    <packaging>jar</packaging>
+
+    <!-- NOTE: This module should be mindful of it's dependencies and should generally only depend on the revision API -->
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-revision-api</artifactId>
+            <version>0.5.0-SNAPSHOT</version>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/naive/NaiveRevisionManager.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/naive/NaiveRevisionManager.java
new file mode 100644
index 0000000..0d161cd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/naive/NaiveRevisionManager.java
@@ -0,0 +1,139 @@
+/*
+ * 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.revision.naive;
+
+import org.apache.nifi.registry.revision.api.DeleteRevisionTask;
+import org.apache.nifi.registry.revision.api.ExpiredRevisionClaimException;
+import org.apache.nifi.registry.revision.api.InvalidRevisionException;
+import org.apache.nifi.registry.revision.api.Revision;
+import org.apache.nifi.registry.revision.api.RevisionClaim;
+import org.apache.nifi.registry.revision.api.RevisionManager;
+import org.apache.nifi.registry.revision.api.RevisionUpdate;
+import org.apache.nifi.registry.revision.api.UpdateRevisionTask;
+import org.apache.nifi.registry.revision.standard.RevisionComparator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * <p>
+ * This class implements a naive approach for Revision Management.
+ * Each call into the Revision Manager will block until any previously held
+ * lock is expired or unlocked. This provides a very simple solution but can
+ * likely be improved by allowing, for instance, multiple threads to obtain
+ * temporary locks simultaneously, etc.
+ * </p>
+ */
+public class NaiveRevisionManager implements RevisionManager {
+    private static final Logger logger = LoggerFactory.getLogger(NaiveRevisionManager.class);
+
+    private final ConcurrentMap<String, Revision> revisionMap = new ConcurrentHashMap<>();
+
+
+    @Override
+    public void reset(final Collection<Revision> revisions) {
+        synchronized (this) { // avoid allowing two threads to reset versions concurrently
+            revisionMap.clear();
+
+            for (final Revision revision : revisions) {
+                revisionMap.put(revision.getEntityId(), revision);
+            }
+        }
+    }
+
+    @Override
+    public List<Revision> getAllRevisions() {
+        return new ArrayList<>(revisionMap.values());
+    }
+
+    @Override
+    public Map<String, Revision> getRevisionMap() {
+        return new HashMap<>(revisionMap);
+    }
+
+    @Override
+    public Revision getRevision(final String componentId) {
+        return revisionMap.computeIfAbsent(componentId, id -> new Revision(0L, null, componentId));
+    }
+
+    @Override
+    public <T> T deleteRevision(final RevisionClaim claim, final DeleteRevisionTask<T> task) throws ExpiredRevisionClaimException {
+        logger.debug("Attempting to delete revision using {}", claim);
+        final List<Revision> revisionList = new ArrayList<>(claim.getRevisions());
+        revisionList.sort(new RevisionComparator());
+
+        // Verify the provided revisions.
+        String failedId = null;
+        for (final Revision revision : revisionList) {
+            final Revision curRevision = getRevision(revision.getEntityId());
+            if (!curRevision.equals(revision)) {
+                throw new ExpiredRevisionClaimException("Invalid Revision was given for entity with ID '" + failedId + "'");
+            }
+        }
+
+        // Perform the action provided
+        final T taskResult = task.performTask();
+
+        for (final Revision revision : revisionList) {
+            revisionMap.remove(revision.getEntityId());
+        }
+
+        return taskResult;
+    }
+
+    @Override
+    public <T> RevisionUpdate<T> updateRevision(final RevisionClaim originalClaim, final UpdateRevisionTask<T> task) throws ExpiredRevisionClaimException {
+        logger.debug("Attempting to update revision using {}", originalClaim);
+
+        final List<Revision> revisionList = new ArrayList<>(originalClaim.getRevisions());
+        revisionList.sort(new RevisionComparator());
+
+        for (final Revision revision : revisionList) {
+            final Revision currentRevision = getRevision(revision.getEntityId());
+            final boolean verified = revision.equals(currentRevision);
+
+            if (!verified) {
+                // Throw an Exception indicating that we failed to obtain the locks
+                throw new InvalidRevisionException("Invalid Revision was given for entity with ID '" + revision.getEntityId() + "'");
+            }
+        }
+
+        // We successfully verified all revisions.
+        logger.debug("Successfully verified Revision Claim for all revisions");
+
+        // Perform the update
+        final RevisionUpdate<T> updatedComponent = task.update();
+
+        // If the update succeeded then put the updated revisions into the revisionMap
+        // If an exception is thrown during the update we don't want to update revision so it is ok to bounce out of this method
+        if (updatedComponent != null) {
+            for (final Revision updatedRevision : updatedComponent.getUpdatedRevisions()) {
+                revisionMap.put(updatedRevision.getEntityId(), updatedRevision);
+            }
+        }
+
+        return updatedComponent;
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/RevisionComparator.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/RevisionComparator.java
new file mode 100644
index 0000000..0bf317a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/RevisionComparator.java
@@ -0,0 +1,42 @@
+/*
+ * 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.revision.standard;
+
+import org.apache.nifi.registry.revision.api.Revision;
+
+import java.util.Comparator;
+import java.util.Objects;
+
+public class RevisionComparator implements Comparator<Revision> {
+
+    @Override
+    public int compare(final Revision o1, final Revision o2) {
+        final int entityComparison = o1.getEntityId().compareTo(o2.getEntityId());
+        if (entityComparison != 0) {
+            return entityComparison;
+        }
+
+        final Comparator<String> nullSafeStringComparator = Comparator.nullsFirst(String::compareTo);
+        final int clientComparison = Objects.compare(o1.getClientId(), o2.getClientId(), nullSafeStringComparator);
+        if (clientComparison != 0) {
+            return clientComparison;
+        }
+
+        return o1.getVersion().compareTo(o2.getVersion());
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionClaim.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionClaim.java
new file mode 100644
index 0000000..2903ea2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionClaim.java
@@ -0,0 +1,49 @@
+/*
+ * 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.revision.standard;
+
+import org.apache.nifi.registry.revision.api.Revision;
+import org.apache.nifi.registry.revision.api.RevisionClaim;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public class StandardRevisionClaim implements RevisionClaim {
+    private final Set<Revision> revisions;
+
+    public StandardRevisionClaim(final Revision... revisions) {
+        this.revisions = new HashSet<>(revisions.length);
+        for (final Revision revision : revisions) {
+            this.revisions.add(revision);
+        }
+    }
+
+    public StandardRevisionClaim(final Collection<Revision> revisions) {
+        this.revisions = new HashSet<>(revisions);
+    }
+
+    @Override
+    public Set<Revision> getRevisions() {
+        return revisions;
+    }
+
+    @Override
+    public String toString() {
+        return revisions.toString();
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionUpdate.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionUpdate.java
new file mode 100644
index 0000000..3a6012e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/standard/StandardRevisionUpdate.java
@@ -0,0 +1,66 @@
+/*
+ * 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.revision.standard;
+
+import org.apache.nifi.registry.revision.api.EntityModification;
+import org.apache.nifi.registry.revision.api.Revision;
+import org.apache.nifi.registry.revision.api.RevisionUpdate;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class StandardRevisionUpdate<T> implements RevisionUpdate<T> {
+    private final T entity;
+    private final EntityModification lastModification;
+    private final Set<Revision> updatedRevisions;
+
+    public StandardRevisionUpdate(final T entity, final EntityModification lastModification) {
+        this(entity, lastModification, null);
+    }
+
+    public StandardRevisionUpdate(final T entity, final EntityModification lastModification, final Set<Revision> updatedRevisions) {
+        this.entity = entity;
+        this.lastModification = lastModification;
+        this.updatedRevisions = updatedRevisions == null ? new HashSet<>() : new HashSet<>(updatedRevisions);
+        if (lastModification != null) {
+            this.updatedRevisions.add(lastModification.getRevision());
+        }
+    }
+
+
+    @Override
+    public T getEntity() {
+        return entity;
+    }
+
+    @Override
+    public EntityModification getLastModification() {
+        return lastModification;
+    }
+
+    @Override
+    public Set<Revision> getUpdatedRevisions() {
+        return Collections.unmodifiableSet(updatedRevisions);
+    }
+
+    @Override
+    public String toString() {
+        return "[Entity=" + entity + ", Last Modification=" + lastModification + "]";
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/ClientIdParameter.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/ClientIdParameter.java
new file mode 100644
index 0000000..1e4d09c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/ClientIdParameter.java
@@ -0,0 +1,43 @@
+/*
+ * 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.revision.web;
+
+import java.util.UUID;
+
+/**
+ * Class for parsing handling client ids. If the client id is not specified, one will be generated.
+ */
+public class ClientIdParameter {
+
+    private final String clientId;
+
+    public ClientIdParameter(String clientId) {
+        if (clientId == null || clientId.trim().isEmpty()) {
+            this.clientId = UUID.randomUUID().toString();
+        } else {
+            this.clientId = clientId;
+        }
+    }
+
+    public ClientIdParameter() {
+        this.clientId = UUID.randomUUID().toString();
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/LongParameter.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/LongParameter.java
new file mode 100644
index 0000000..c568d76
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-common/src/main/java/org/apache/nifi/registry/revision/web/LongParameter.java
@@ -0,0 +1,39 @@
+/*
+ * 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.revision.web;
+
+/**
+ * Class for parsing long parameters and providing a user friendly error message.
+ */
+public class LongParameter {
+
+    private static final String INVALID_LONG_MESSAGE = "Unable to parse '%s' as a long value.";
+
+    private Long longValue;
+
+    public LongParameter(String rawLongValue) {
+        try {
+            longValue = Long.parseLong(rawLongValue);
+        } catch (NumberFormatException nfe) {
+            throw new IllegalArgumentException(String.format(INVALID_LONG_MESSAGE, rawLongValue));
+        }
+    }
+
+    public Long getLong() {
+        return longValue;
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/pom.xml b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/pom.xml
new file mode 100644
index 0000000..ba2c1c1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/pom.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-revision</artifactId>
+        <version>0.5.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-revision-entity-model</artifactId>
+    <packaging>jar</packaging>
+
+    <!-- Should be no other external dependencies besides swagger -->
+    <dependencies>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntity.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntity.java
new file mode 100644
index 0000000..260cbb1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntity.java
@@ -0,0 +1,48 @@
+/*
+ * 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.revision.entity;
+
+/**
+ * An entity that supports revision tracking.
+ */
+public interface RevisableEntity {
+
+    /**
+     * @return the identifier for the entity
+     */
+    String getIdentifier();
+
+    /**
+     * Sets the identifier for the entity.
+     *
+     * @param identifier the identifier
+     */
+    void setIdentifier(String identifier);
+
+    /**
+     * @return the revision information for the entity
+     */
+    RevisionInfo getRevision();
+
+    /**
+     * Sets the revision info for the entity.
+     *
+     * @param revision the revision info
+     */
+    void setRevision(RevisionInfo revision);
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisionInfo.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisionInfo.java
new file mode 100644
index 0000000..dec19fe
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-model/src/main/java/org/apache/nifi/registry/revision/entity/RevisionInfo.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.revision.entity;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+@ApiModel(description = "The revision information for an entity managed through the REST API.")
+public class RevisionInfo {
+
+    private String clientId;
+    private Long version;
+    private String lastModifier;
+
+    public RevisionInfo() {
+    }
+
+    public RevisionInfo(String clientId, Long version) {
+        this(clientId, version, null);
+    }
+
+    public RevisionInfo(String clientId, Long version, String lastModifier) {
+        this.clientId = clientId;
+        this.version = version;
+        this.lastModifier = lastModifier;
+    }
+
+    @ApiModelProperty(
+            value = "A client identifier used to make a request. By including a client identifier, the API can allow multiple requests " +
+                    "without needing the current revision. Due to the asynchronous nature of requests/responses this was implemented to " +
+                    "allow the client to make numerous requests without having to wait for the previous response to come back."
+    )
+    public String getClientId() {
+        return clientId;
+    }
+
+    public void setClientId(final String clientId) {
+        this.clientId = clientId;
+    }
+
+    @ApiModelProperty(
+            value = "NiFi Registry employs an optimistic locking strategy where the client must include a revision in their request " +
+                    "when performing an update. In a response to a mutable flow request, this field represents the updated base version."
+    )
+    public Long getVersion() {
+        return version;
+    }
+
+    public void setVersion(final Long version) {
+        this.version = version;
+    }
+
+    @ApiModelProperty(
+            value = "The user that last modified the entity.",
+            readOnly = true
+    )
+    public String getLastModifier() {
+        return lastModifier;
+    }
+
+    public void setLastModifier(final String lastModifier) {
+        this.lastModifier = lastModifier;
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/pom.xml b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/pom.xml
new file mode 100644
index 0000000..2a8cba0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/pom.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-revision</artifactId>
+        <version>0.5.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-revision-entity-service</artifactId>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-revision-entity-model</artifactId>
+            <version>0.5.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-revision-common</artifactId>
+            <version>0.5.0-SNAPSHOT</version>
+        </dependency>
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>${org.slf4j.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntityService.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntityService.java
new file mode 100644
index 0000000..c5d66f5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/RevisableEntityService.java
@@ -0,0 +1,78 @@
+/*
+ * 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.revision.entity;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * A service to perform CRUD operations on a RevisableEntity.
+ */
+public interface RevisableEntityService {
+
+    /**
+     * Creates an entity using the RevisionManager.
+     *
+     * @param requestEntity the entity to create
+     * @param creatorIdentity the identity of the user performing the create operation
+     * @param createEntity a function that creates the entity and returns the created reference
+     * @param <T> the type of RevisableEntity
+     * @return the created entity
+     */
+    <T extends RevisableEntity> T create(T requestEntity, String creatorIdentity, Supplier<T> createEntity);
+
+    /**
+     * Retrieves a RevisableEntity and populates the RevisionInfo.
+     *
+     * @param getEntity a function that retrieves an entity
+     * @param <T> the type of RevisableEntity
+     * @return the retrieved entity
+     */
+    <T extends RevisableEntity> T get(Supplier<T> getEntity);
+
+    /**
+     * Retrieves a List of RevisableEntity instances and populates the RevisionInfo.
+     *
+     * @param getEntities a function that retrieves a list of entities
+     * @param <T> the type of RevisableEntity
+     * @return the list of retrieved entity
+     */
+    <T extends RevisableEntity> List<T> getEntities(Supplier<List<T>> getEntities);
+
+    /**
+     * Updates a RevisableEntity using the RevisionManager.
+     *
+     * @param requestEntity the entity to update
+     * @param updaterIdentity the identity of the user performing the update operation
+     * @param updateEntity a function that updates the entity and returns the updated reference
+     * @param <T> the type of RevisableEntity
+     * @return the updated entity
+     */
+    <T extends RevisableEntity> T update(T requestEntity, String updaterIdentity, Supplier<T> updateEntity);
+
+    /**
+     * Deletes a RevisableEntity using the RevisionManager.
+     *
+     * @param entityIdentifier the identifier of the entity to delete
+     * @param revisionInfo the RevisionInfo for the entity to delete
+     * @param deleteEntity a function that deletes the entity and returns the deleted reference
+     * @param <T> the type of RevisableEntity
+     * @return the deleted entity
+     */
+    <T extends RevisableEntity> T delete(String entityIdentifier, RevisionInfo revisionInfo, Supplier<T> deleteEntity);
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/StandardRevisableEntityService.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/StandardRevisableEntityService.java
new file mode 100644
index 0000000..cd55ea3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/main/java/org/apache/nifi/registry/revision/entity/StandardRevisableEntityService.java
@@ -0,0 +1,168 @@
+/*
+ * 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.revision.entity;
+
+import org.apache.nifi.registry.revision.api.EntityModification;
+import org.apache.nifi.registry.revision.api.Revision;
+import org.apache.nifi.registry.revision.api.RevisionClaim;
+import org.apache.nifi.registry.revision.api.RevisionManager;
+import org.apache.nifi.registry.revision.api.RevisionUpdate;
+import org.apache.nifi.registry.revision.standard.StandardRevisionClaim;
+import org.apache.nifi.registry.revision.standard.StandardRevisionUpdate;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * Standard implementation of RevisableEntityService.
+ */
+public class StandardRevisableEntityService implements RevisableEntityService {
+
+    private final RevisionManager revisionManager;
+
+    public StandardRevisableEntityService(final RevisionManager revisionManager) {
+        this.revisionManager = revisionManager;
+    }
+
+    @Override
+    public <T extends RevisableEntity> T create(final T requestEntity, final String creatorIdentity, final Supplier<T> createEntity) {
+        if (requestEntity == null) {
+            throw new IllegalArgumentException("Request entity is required");
+        }
+
+        if (requestEntity.getRevision() == null || requestEntity.getRevision().getVersion() == null) {
+            throw new IllegalArgumentException("Revision info is required");
+        }
+
+        if (requestEntity.getRevision().getVersion() != 0) {
+            throw new IllegalArgumentException("A revision version of 0 must be specified when creating a new entity");
+        }
+
+        if (creatorIdentity == null || creatorIdentity.trim().isEmpty()) {
+            throw new IllegalArgumentException("Creator identity is required");
+        }
+
+        return createOrUpdate(requestEntity, creatorIdentity, createEntity);
+    }
+
+    @Override
+    public <T extends RevisableEntity> T get(final Supplier<T> getEntity) {
+        final T entity = getEntity.get();
+        if (entity != null) {
+            populateRevision(entity);
+        }
+        return entity;
+    }
+
+    @Override
+    public <T extends RevisableEntity> List<T> getEntities(final Supplier<List<T>> getEntities) {
+        final List<T> entities = getEntities.get();
+        populateRevisions(entities);
+        return entities;
+    }
+
+    @Override
+    public <T extends RevisableEntity> T update(final T requestEntity, final String updaterIdentity, final Supplier<T> updateEntity) {
+        if (requestEntity == null) {
+            throw new IllegalArgumentException("Request entity is required");
+        }
+
+        if (requestEntity.getRevision() == null || requestEntity.getRevision().getVersion() == null) {
+            throw new IllegalArgumentException("Revision info is required");
+        }
+
+        if (updaterIdentity == null || updaterIdentity.trim().isEmpty()) {
+            throw new IllegalArgumentException("Updater identity is required");
+        }
+
+        return createOrUpdate(requestEntity, updaterIdentity, updateEntity);
+    }
+
+    @Override
+    public <T extends RevisableEntity> T delete(final String entityIdentifier, final RevisionInfo revisionInfo, final Supplier<T> deleteEntity) {
+        if (entityIdentifier == null || entityIdentifier.trim().isEmpty()) {
+            throw new IllegalArgumentException("Entity identifier is required");
+        }
+
+        if (revisionInfo == null || revisionInfo.getVersion() == null) {
+            throw new IllegalArgumentException("Revision info is required");
+        }
+
+        final Revision revision = createRevision(entityIdentifier, revisionInfo);
+        final RevisionClaim claim = new StandardRevisionClaim(revision);
+        return revisionManager.deleteRevision(claim, () -> deleteEntity.get());
+    }
+
+    private <T extends RevisableEntity> T createOrUpdate(final T requestEntity, final String userIdentity, final Supplier<T> updateOrCreateEntity) {
+        final Revision revision = createRevision(requestEntity.getIdentifier(), requestEntity.getRevision());
+        final RevisionClaim claim = new StandardRevisionClaim(revision);
+
+        final RevisionUpdate<T> revisionUpdate = revisionManager.updateRevision(claim, () -> {
+            final T updatedEntity = updateOrCreateEntity.get();
+
+            final Revision updatedRevision = revision.incrementRevision(revision.getClientId());
+            final EntityModification entityModification = new EntityModification(updatedRevision, userIdentity);
+
+            final RevisionInfo updatedRevisionInfo = createRevisionInfo(updatedRevision, entityModification);
+            updatedEntity.setRevision(updatedRevisionInfo);
+
+            return new StandardRevisionUpdate<>(updatedEntity, entityModification);
+        });
+
+        return revisionUpdate.getEntity();
+    }
+
+    private <T extends RevisableEntity> void populateRevisions(final Collection<T> revisableEntities) {
+        if (revisableEntities == null) {
+            return;
+        }
+
+        revisableEntities.forEach(e -> {
+            populateRevision(e);
+        });
+    }
+
+    private void populateRevision(final RevisableEntity e) {
+        if (e == null) {
+            return;
+        }
+
+        final Revision entityRevision = revisionManager.getRevision(e.getIdentifier());
+        final RevisionInfo revisionInfo = createRevisionInfo(entityRevision);
+        e.setRevision(revisionInfo);
+    }
+
+    private Revision createRevision(final String entityId, final RevisionInfo revisionInfo) {
+        return new Revision(revisionInfo.getVersion(), revisionInfo.getClientId(), entityId);
+    }
+
+    private RevisionInfo createRevisionInfo(final Revision revision) {
+        return createRevisionInfo(revision, null);
+    }
+
+    private RevisionInfo createRevisionInfo(final Revision revision, final EntityModification entityModification) {
+        final RevisionInfo revisionInfo = new RevisionInfo();
+        revisionInfo.setVersion(revision.getVersion());
+        revisionInfo.setClientId(revision.getClientId());
+        if (entityModification != null) {
+            revisionInfo.setLastModifier(entityModification.getLastModifier());
+        }
+        return revisionInfo;
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/test/java/org/apache/nifi/registry/revision/entity/TestStandardRevisableEntityService.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/test/java/org/apache/nifi/registry/revision/entity/TestStandardRevisableEntityService.java
new file mode 100644
index 0000000..47b3d9c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-entity-service/src/test/java/org/apache/nifi/registry/revision/entity/TestStandardRevisableEntityService.java
@@ -0,0 +1,220 @@
+/*
+ * 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.revision.entity;
+
+import org.apache.nifi.registry.revision.api.InvalidRevisionException;
+import org.apache.nifi.registry.revision.api.RevisionManager;
+import org.apache.nifi.registry.revision.naive.NaiveRevisionManager;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+public class TestStandardRevisableEntityService {
+
+    private RevisionManager revisionManager;
+    private RevisableEntityService entityService;
+
+    @Before
+    public void setup() {
+        revisionManager = new NaiveRevisionManager();
+        entityService = new StandardRevisableEntityService(revisionManager);
+    }
+
+    @Test
+    public void testCreate() {
+        final String clientId = "client1";
+        final RevisionInfo requestRevision = new RevisionInfo(clientId, 0L);
+
+        final String userIdentity = "user1";
+
+        final String entityId = "1";
+        final RevisableEntity requestEntity = new TestEntity(entityId, requestRevision);
+
+        final RevisableEntity createdEntity = entityService.create(
+                requestEntity, userIdentity, () -> new TestEntity(entityId, null));
+        assertNotNull(createdEntity);
+        assertEquals(requestEntity.getIdentifier(), createdEntity.getIdentifier());
+
+        final RevisionInfo createdRevision = createdEntity.getRevision();
+        assertNotNull(createdRevision);
+        assertEquals(requestRevision.getVersion().longValue() + 1, createdRevision.getVersion().longValue());
+        assertEquals(clientId, createdRevision.getClientId());
+        assertEquals(userIdentity, createdRevision.getLastModifier());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testCreateWhenMissingRevision() {
+        final RevisableEntity requestEntity = new TestEntity("1", null);
+        entityService.create(requestEntity, "user1", () -> requestEntity);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testCreateWhenNonZeroRevision() {
+        final RevisionInfo requestRevision = new RevisionInfo(null, 99L);
+        final RevisableEntity requestEntity = new TestEntity("1", requestRevision);
+        entityService.create(requestEntity, "user1", () -> requestEntity);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testCreateWhenTaskThrowsException() {
+        final RevisionInfo requestRevision = new RevisionInfo("client1", 0L);
+        final RevisableEntity requestEntity = new TestEntity("1", requestRevision);
+        entityService.create(requestEntity, "user1", () -> {
+            throw new IllegalArgumentException("");
+        });
+    }
+
+    @Test
+    public void testGetEntityWhenExists() {
+        final RevisableEntity entity = entityService.get(() -> new TestEntity("1", null));
+        assertNotNull(entity);
+
+        final RevisionInfo revision = entity.getRevision();
+        assertNotNull(revision);
+        assertEquals(0, revision.getVersion().longValue());
+    }
+
+    @Test
+    public void testGetEntityWhenDoesNotExist() {
+        final RevisableEntity entity = entityService.get(() -> null);
+        assertNull(entity);
+    }
+
+    @Test
+    public void testGetEntities() {
+        final TestEntity entity1 = new TestEntity("1", null);
+        final TestEntity entity2 = new TestEntity("2", null);
+        final List<TestEntity> entities = Arrays.asList(entity1, entity2);
+
+        final List<TestEntity> resultEntities = entityService.getEntities(() -> entities);
+        assertNotNull(resultEntities);
+        resultEntities.forEach(e -> {
+            assertNotNull(e.getRevision());
+            assertEquals(0, e.getRevision().getVersion().longValue());
+        });
+    }
+
+    @Test
+    public void testUpdate() {
+        final RevisionInfo revisionInfo = new RevisionInfo(null, 0L);
+        final TestEntity requestEntity = new TestEntity("1", revisionInfo);
+
+        final RevisableEntity createdEntity = entityService.create(
+                requestEntity, "user1", () -> requestEntity);
+        assertNotNull(createdEntity);
+        assertEquals(requestEntity.getIdentifier(), createdEntity.getIdentifier());
+        assertNotNull(createdEntity.getRevision());
+        assertEquals(1, createdEntity.getRevision().getVersion().longValue());
+
+        final RevisableEntity updatedEntity = entityService.update(
+                createdEntity, "user2", () -> createdEntity);
+        assertNotNull(updatedEntity.getRevision());
+        assertEquals(2, updatedEntity.getRevision().getVersion().longValue());
+        assertEquals("user2", updatedEntity.getRevision().getLastModifier());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testUpdateWhenMissingRevision() {
+        final RevisionInfo revisionInfo = new RevisionInfo(null, 0L);
+        final TestEntity requestEntity = new TestEntity("1", revisionInfo);
+
+        final RevisableEntity createdEntity = entityService.create(
+                requestEntity, "user1", () -> requestEntity);
+        assertNotNull(createdEntity);
+        assertEquals(requestEntity.getIdentifier(), createdEntity.getIdentifier());
+        assertNotNull(createdEntity.getRevision());
+        assertEquals(1, createdEntity.getRevision().getVersion().longValue());
+
+        createdEntity.setRevision(null);
+        entityService.update(createdEntity, "user2", () -> createdEntity);
+    }
+
+    @Test
+    public void testDelete() {
+        final RevisionInfo revisionInfo = new RevisionInfo(null, 0L);
+        final TestEntity requestEntity = new TestEntity("1", revisionInfo);
+
+        final RevisableEntity createdEntity = entityService.create(
+                requestEntity, "user1", () -> requestEntity);
+        assertNotNull(createdEntity);
+
+        final RevisableEntity deletedEntity = entityService.delete(
+                createdEntity.getIdentifier(), createdEntity.getRevision(), () -> createdEntity);
+        assertNotNull(deletedEntity);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testDeleteWhenMissingRevision() {
+        final RevisionInfo revisionInfo = new RevisionInfo(null, 0L);
+        final TestEntity requestEntity = new TestEntity("1", revisionInfo);
+
+        final RevisableEntity createdEntity = entityService.create(
+                requestEntity, "user1", () -> requestEntity);
+        assertNotNull(createdEntity);
+        assertNotNull(createdEntity.getRevision());
+
+        createdEntity.setRevision(null);
+        entityService.delete(createdEntity.getIdentifier(), createdEntity.getRevision(), () -> createdEntity);
+    }
+
+    @Test(expected = InvalidRevisionException.class)
+    public void testDeleteWhenDoesNotExist() {
+        final RevisionInfo revisionInfo = new RevisionInfo(null, 1L);
+        final RevisableEntity deletedEntity = entityService.delete("1", revisionInfo, () -> null);
+        assertNull(deletedEntity);
+    }
+
+    /**
+     * A RevisableEntity for testing.
+     */
+    static class TestEntity implements RevisableEntity {
+
+        private String identifier;
+        private RevisionInfo revisionInfo;
+
+        public TestEntity(String identifier, RevisionInfo revisionInfo) {
+            this.identifier = identifier;
+            this.revisionInfo = revisionInfo;
+        }
+
+        @Override
+        public String getIdentifier() {
+            return identifier;
+        }
+
+        @Override
+        public void setIdentifier(String identifier) {
+            this.identifier = identifier;
+        }
+
+        @Override
+        public RevisionInfo getRevision() {
+            return revisionInfo;
+        }
+
+        @Override
+        public void setRevision(RevisionInfo revision) {
+            this.revisionInfo = revision;
+        }
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/pom.xml b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/pom.xml
new file mode 100644
index 0000000..17a0643
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/pom.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-revision</artifactId>
+        <version>0.5.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-revision-spring-jdbc</artifactId>
+    <packaging>jar</packaging>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-revision-common</artifactId>
+            <version>0.5.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-jdbc</artifactId>
+            <version>5.1.9.RELEASE</version>
+        </dependency>
+        <!-- Test Deps -->
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-test</artifactId>
+            <version>0.5.0-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <version>${h2.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.flywaydb</groupId>
+            <artifactId>flyway-core</artifactId>
+            <version>${flyway.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/JdbcRevisionManager.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/JdbcRevisionManager.java
new file mode 100644
index 0000000..74b2393
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/JdbcRevisionManager.java
@@ -0,0 +1,227 @@
+/*
+ * 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.revision.jdbc;
+
+import org.apache.nifi.registry.revision.api.DeleteRevisionTask;
+import org.apache.nifi.registry.revision.api.ExpiredRevisionClaimException;
+import org.apache.nifi.registry.revision.api.InvalidRevisionException;
+import org.apache.nifi.registry.revision.api.Revision;
+import org.apache.nifi.registry.revision.api.RevisionClaim;
+import org.apache.nifi.registry.revision.api.RevisionManager;
+import org.apache.nifi.registry.revision.api.RevisionUpdate;
+import org.apache.nifi.registry.revision.api.UpdateRevisionTask;
+import org.apache.nifi.registry.revision.standard.RevisionComparator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * A database implementation of {@link RevisionManager} that use's Spring's {@link JdbcTemplate}.
+ *
+ * It is expected that the database has a table named REVISION with the following schema, but it is up to consumers
+ * of this library to manage the creation of this table:
+ *
+ * <pre>
+ * {@code
+ *  CREATE TABLE REVISION (
+ *     ENTITY_ID VARCHAR(50) NOT NULL,
+ *     VERSION BIGINT NOT NULL DEFAULT(0),
+ *     CLIENT_ID VARCHAR(100),
+ *     CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID)
+ *  );
+ * }
+ * </pre>
+ *
+ * This implementation leverages the transactional semantics of a relational database to implement an optimistic-locking strategy.
+ *
+ * In order to function correctly, this must be used with in a transaction with an isolation level of at least READ_COMMITTED.
+ */
+public class JdbcRevisionManager implements RevisionManager {
+
+    private static Logger LOGGER = LoggerFactory.getLogger(JdbcRevisionManager.class);
+
+    private final JdbcTemplate jdbcTemplate;
+
+    public JdbcRevisionManager(final JdbcTemplate jdbcTemplate) {
+        this.jdbcTemplate = Objects.requireNonNull(jdbcTemplate);
+    }
+
+    @Override
+    public Revision getRevision(final String entityId) {
+        final Revision revision = retrieveRevision(entityId);
+        if (revision == null) {
+            return createRevision(entityId);
+        } else {
+            return revision;
+        }
+    }
+
+    private Revision retrieveRevision(final String entityId) {
+        try {
+            final String selectSql = "SELECT * FROM REVISION WHERE ENTITY_ID = ?";
+            return jdbcTemplate.queryForObject(selectSql, new Object[] {entityId}, new RevisionRowMapper());
+        } catch (EmptyResultDataAccessException e) {
+            return null;
+        }
+    }
+
+    private Revision createRevision(final String entityId) {
+        final Revision revision = new Revision(0L, null, entityId);
+        final String insertSql = "INSERT INTO REVISION(ENTITY_ID, VERSION) VALUES (?, ?)";
+        jdbcTemplate.update(insertSql, revision.getEntityId(), revision.getVersion());
+        return revision;
+    }
+
+    @Override
+    public <T> RevisionUpdate<T> updateRevision(final RevisionClaim claim, final UpdateRevisionTask<T> task) {
+        LOGGER.debug("Attempting to update revision using {}", claim);
+
+        final List<Revision> revisionList = new ArrayList<>(claim.getRevisions());
+        revisionList.sort(new RevisionComparator());
+
+        // Update each revision which increments the version and locks the row.
+        // Since we are in transaction these changes won't be committed unless the entire task completes successfully.
+        // It is important this happens first so that the task won't execute unless the revision can be updated.
+        // This prevents any other changes from happening that might not be part of the database transaction.
+        for (final Revision incomingRevision : revisionList) {
+            // calling getRevision here will lazily create an initial revision
+            getRevision(incomingRevision.getEntityId());
+            updateRevision(incomingRevision);
+        }
+
+        // We successfully verified all revisions.
+        LOGGER.debug("Successfully verified Revision Claim for all revisions");
+
+        // Perform the update
+        final RevisionUpdate<T> updatedEntity = task.update();
+        LOGGER.debug("Update task completed");
+
+        return updatedEntity;
+    }
+
+    /*
+     * Issue an update that increments the version, but only if the incoming version OR client id match the existing revision.
+     *
+     * If no rows were updated, then the incoming revision is stale and an exception is thrown.
+     *
+     * If a row was updated, then the incoming revision is good and that row is no locked in the DB, and we can proceed.
+     */
+    private void updateRevision(final Revision incomingRevision) {
+        final String sql =
+                "UPDATE REVISION SET " +
+                        "VERSION = (VERSION + 1), " +
+                        "CLIENT_ID = ? " +
+                "WHERE " +
+                        "ENTITY_ID = ? AND (" +
+                        "VERSION = ? OR CLIENT_ID = ? " +
+                ")";
+
+        final String entityId = incomingRevision.getEntityId();
+        final String clientId = incomingRevision.getClientId();
+        final Long version = incomingRevision.getVersion();
+
+        final int rowsUpdated = jdbcTemplate.update(sql, clientId, entityId, version, clientId);
+        if (rowsUpdated <= 0) {
+            throw new InvalidRevisionException("Invalid Revision was given for entity with ID '" + entityId + "'");
+        }
+    }
+
+    @Override
+    public <T> T deleteRevision(final RevisionClaim claim, final DeleteRevisionTask<T> task)
+            throws ExpiredRevisionClaimException {
+        LOGGER.debug("Attempting to delete revision using {}", claim);
+
+        final List<Revision> revisionList = new ArrayList<>(claim.getRevisions());
+        revisionList.sort(new RevisionComparator());
+
+        // Issue the delete for each revision
+        // Since we are in transaction these changes won't be committed unless the entire task completes successfully.
+        // It is important this happens first so that the task won't execute unless the revision can be deleted.
+        // This prevents any other changes from happening that might not be part of the database transaction.
+        for (final Revision revision : revisionList) {
+            deleteRevision(revision);
+        }
+
+        // Perform the action provided
+        final T taskResult = task.performTask();
+        LOGGER.debug("Delete task completed");
+
+        return taskResult;
+    }
+
+    /*
+     * Issue a delete for a revision of a given entity, but only if the incoming version OR client id match the existing revision.
+     *
+     * If no rows were updated, then the incoming revision is stale and an exception is thrown.
+     *
+     * If a row was deleted, then the incoming revision is good and that row is no locked in the DB, and we can proceed.
+     */
+    private void deleteRevision(final Revision revision) {
+        final String sql =
+                "DELETE FROM REVISION WHERE " +
+                        "ENTITY_ID = ? AND (" +
+                            "VERSION = ? OR CLIENT_ID = ? " +
+                        ")";
+
+        final String entityId = revision.getEntityId();
+        final String clientId = revision.getClientId();
+        final Long version = revision.getVersion();
+
+        final int rowsUpdated = jdbcTemplate.update(sql, entityId, version, clientId);
+        if (rowsUpdated <= 0) {
+            throw new ExpiredRevisionClaimException("Invalid Revision was given for entity with ID '" + entityId + "'");
+        }
+    }
+
+    @Override
+    public void reset(final Collection<Revision> revisions) {
+        // delete all revisions
+        jdbcTemplate.update("DELETE FROM REVISION");
+
+        // insert all the provided revisions
+        final String insertSql = "INSERT INTO REVISION(ENTITY_ID, VERSION, CLIENT_ID) VALUES (?, ?, ?)";
+        for (final Revision revision : revisions) {
+            jdbcTemplate.update(insertSql, revision.getEntityId(), revision.getVersion(), revision.getClientId());
+        }
+    }
+
+    @Override
+    public List<Revision> getAllRevisions() {
+        return jdbcTemplate.query("SELECT * FROM REVISION", new RevisionRowMapper());
+    }
+
+    @Override
+    public Map<String, Revision> getRevisionMap() {
+        final Map<String,Revision> revisionMap = new HashMap<>();
+        final RevisionRowMapper rowMapper = new RevisionRowMapper();
+
+        jdbcTemplate.query("SELECT * FROM REVISION", (rs) -> {
+            final Revision revision = rowMapper.mapRow(rs, 0);
+            revisionMap.put(revision.getEntityId(), revision);
+        });
+
+        return revisionMap;
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/RevisionRowMapper.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/RevisionRowMapper.java
new file mode 100644
index 0000000..431e7c6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/main/java/org/apache/nifi/registry/revision/jdbc/RevisionRowMapper.java
@@ -0,0 +1,35 @@
+/*
+ * 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.revision.jdbc;
+
+import org.apache.nifi.registry.revision.api.Revision;
+import org.springframework.jdbc.core.RowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class RevisionRowMapper implements RowMapper<Revision> {
+
+    @Override
+    public Revision mapRow(final ResultSet rs, final int i) throws SQLException {
+        final String entityId = rs.getString("ENTITY_ID");
+        final Long version = rs.getLong("VERSION");
+        final String clientId = rs.getString("CLIENT_ID");
+        return new Revision(version, clientId, entityId);
+    }
+
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/TestApplication.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/TestApplication.java
new file mode 100644
index 0000000..133cf8f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/TestApplication.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * Loads an application context for tests.
+ *
+ * This class is purposely in the package org.apache.nifi.registry so that it scans downward and finds beans inside
+ * this module, as well as in dependencies that use the same base package. This allows this module to pick up the test
+ * DataSource factories in nifi-registry-test and leverage test-containers for DB testing.
+ */
+@SpringBootApplication
+public class TestApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(TestApplication.class, args);
+    }
+
+}
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/revision/jdbc/TestJdbcRevisionManager.java b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/revision/jdbc/TestJdbcRevisionManager.java
new file mode 100644
index 0000000..3eecc9a
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/java/org/apache/nifi/registry/revision/jdbc/TestJdbcRevisionManager.java
@@ -0,0 +1,418 @@
+/*
+ * 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.revision.jdbc;
+
+import org.apache.nifi.registry.TestApplication;
+import org.apache.nifi.registry.revision.api.DeleteRevisionTask;
+import org.apache.nifi.registry.revision.api.EntityModification;
+import org.apache.nifi.registry.revision.api.InvalidRevisionException;
+import org.apache.nifi.registry.revision.api.Revision;
+import org.apache.nifi.registry.revision.api.RevisionClaim;
+import org.apache.nifi.registry.revision.api.RevisionManager;
+import org.apache.nifi.registry.revision.api.RevisionUpdate;
+import org.apache.nifi.registry.revision.api.UpdateRevisionTask;
+import org.apache.nifi.registry.revision.standard.StandardRevisionClaim;
+import org.apache.nifi.registry.revision.standard.StandardRevisionUpdate;
+import org.flywaydb.core.internal.jdbc.DatabaseType;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.test.context.TestExecutionListeners;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
+import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+@Transactional
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = TestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.NONE)
+@TestExecutionListeners({DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class})
+public class TestJdbcRevisionManager {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(TestJdbcRevisionManager.class);
+
+    private static final String CREATE_TABLE_SQL_DEFAULT =
+            "CREATE TABLE REVISION (\n" +
+                    "    ENTITY_ID VARCHAR(50) NOT NULL,\n" +
+                    "    VERSION BIGINT NOT NULL DEFAULT (0),\n" +
+                    "    CLIENT_ID VARCHAR(100),\n" +
+                    "    CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID)\n" +
+                    ")";
+
+    private static final String CREATE_TABLE_SQL_MYSQL =
+            "CREATE TABLE REVISION (\n" +
+                    "    ENTITY_ID VARCHAR(50) NOT NULL,\n" +
+                    "    VERSION BIGINT NOT NULL DEFAULT 0,\n" +
+                    "    CLIENT_ID VARCHAR(100),\n" +
+                    "    CONSTRAINT PK__REVISION_ENTITY_ID PRIMARY KEY (ENTITY_ID)\n" +
+                    ")";
+
+    @Autowired
+    private JdbcTemplate jdbcTemplate;
+
+    private RevisionManager revisionManager;
+
+    @Before
+    public void setup() throws SQLException {
+        revisionManager = new JdbcRevisionManager(jdbcTemplate);
+
+        // Create the REVISION table if it does not exist
+        final DataSource dataSource = jdbcTemplate.getDataSource();
+        LOGGER.info("#### DataSource class is {}", new Object[]{dataSource.getClass().getCanonicalName()});
+
+        try (final Connection connection = dataSource.getConnection()) {
+            final String createTableSql;
+            final DatabaseType databaseType = DatabaseType.fromJdbcConnection(connection);
+            if (databaseType == DatabaseType.MYSQL) {
+                createTableSql = CREATE_TABLE_SQL_MYSQL;
+            } else {
+                createTableSql = CREATE_TABLE_SQL_DEFAULT;
+            }
+
+            final DatabaseMetaData meta = connection.getMetaData();
+            try (final ResultSet res = meta.getTables(null, null, "REVISION", new String[]{"TABLE"})) {
+                if (!res.next()) {
+                    jdbcTemplate.execute(createTableSql);
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testGetRevisionWhenDoesNotExist() {
+        final String entityId = "entity1";
+        final Revision revision = revisionManager.getRevision(entityId);
+        assertNotNull(revision);
+        assertEquals(entityId, revision.getEntityId());
+        assertEquals(0L, revision.getVersion().longValue());
+        assertNull(revision.getClientId());
+    }
+
+    @Test
+    public void testGetRevisionWhenExists() {
+        final String entityId = "entity1";
+        final Long version = new Long(99);
+        createRevision(entityId, version, null);
+
+        final Revision revision = revisionManager.getRevision(entityId);
+        assertNotNull(revision);
+        assertEquals(entityId, revision.getEntityId());
+        assertEquals(version.longValue(), revision.getVersion().longValue());
+        assertNull(revision.getClientId());
+    }
+
+    @Test
+    public void testUpdateRevisionWithCurrentVersionNoClientId() {
+        // create the revision being sent in by the client
+        final String entityId = "entity-1";
+        final Revision revision = new Revision(99L, null, entityId);
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
+
+        // seed the database with a matching revision
+        createRevision(revision.getEntityId(), revision.getVersion(), null);
+
+        // perform an update task
+        final RevisionUpdate<RevisableEntity> revisionUpdate = revisionManager.updateRevision(
+                revisionClaim, createUpdateTask(entityId));
+        assertNotNull(revisionUpdate);
+
+        // version should go to 100 since it was 99 before
+        verifyRevisionUpdate(entityId, revisionUpdate, new Long(100), null);
+    }
+
+    @Test(expected = InvalidRevisionException.class)
+    public void testUpdateRevisionWithStaleVersionNoClientId() {
+        // create the revision being sent in by the client
+        final String entityId = "entity-1";
+        final Revision revision = new Revision(99L, null, entityId);
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
+
+        // seed the database with a revision that has a newer version
+        createRevision(revision.getEntityId(), revision.getVersion() + 1, null);
+
+        // perform an update task which should throw InvalidRevisionException
+        revisionManager.updateRevision(revisionClaim, createUpdateTask(entityId));
+    }
+
+    @Test
+    public void testUpdateRevisionWithStaleVersionAndSameClientId() {
+        // create the revision being sent in by the client
+        final String entityId = "entity-1";
+        final String clientId = "client-1";
+        final Revision revision = new Revision(99L, clientId, entityId);
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
+
+        // seed the database with a revision that has a newer version
+        createRevision(revision.getEntityId(), revision.getVersion() + 1, clientId);
+
+        // perform an update task
+        final RevisionUpdate<RevisableEntity> revisionUpdate = revisionManager.updateRevision(
+                revisionClaim, createUpdateTask(entityId));
+        assertNotNull(revisionUpdate);
+
+        // client in 99 which was not latest version, but since client id was the same the update was allowed
+        // and the incremented version should be based on the version in the DB which was 100, so it goes to 101
+        verifyRevisionUpdate(entityId, revisionUpdate, new Long(101), clientId);
+    }
+
+    @Test
+    public void testUpdateRevisionWhenDoesNotExist() {
+        // create the revision being sent in by the client
+        final String entityId = "entity-1";
+        final String clientId = "client-new";
+        final Revision revision = new Revision(0L, clientId, entityId);
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
+
+        // perform an update task
+        final RevisionUpdate<RevisableEntity> revisionUpdate = revisionManager.updateRevision(
+                revisionClaim, createUpdateTask(entityId));
+        assertNotNull(revisionUpdate);
+
+        // version should go to 1 and client id should be updated to client-new
+        verifyRevisionUpdate(entityId, revisionUpdate, new Long(1), clientId);
+    }
+
+    @Test
+    public void testUpdateRevisionWithCurrentVersionAndNewClientId() {
+        // create the revision being sent in by the client
+        final String entityId = "entity-1";
+        final String clientId = "client-new";
+        final Revision revision = new Revision(99L, clientId, entityId);
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
+
+        // seed the database with a revision that has same version but a different client id
+        createRevision(revision.getEntityId(), revision.getVersion(), "client-old");
+
+        // perform an update task
+        final RevisionUpdate<RevisableEntity> revisionUpdate = revisionManager.updateRevision(
+                revisionClaim, createUpdateTask(entityId));
+        assertNotNull(revisionUpdate);
+
+        // version should go to 100 and client id should be updated to client-new
+        verifyRevisionUpdate(entityId, revisionUpdate, new Long(100), clientId);
+    }
+
+    @Test
+    public void testDeleteRevisionWithCurrentVersionAndNoClientId() {
+        // create the revision being sent in by the client
+        final String entityId = "entity-1";
+        final Revision revision = new Revision(99L, null, entityId);
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
+
+        // seed the database with a matching revision
+        createRevision(revision.getEntityId(), revision.getVersion(), null);
+
+        // perform an update task
+        final RevisableEntity deletedEntity = revisionManager.deleteRevision(
+                revisionClaim, createDeleteTask(entityId));
+        assertNotNull(deletedEntity);
+        assertEquals(entityId, deletedEntity.getId());
+    }
+
+    @Test(expected = InvalidRevisionException.class)
+    public void testDeleteRevisionWithStaleVersionAndNoClientId() {
+        // create the revision being sent in by the client
+        final String entityId = "entity-1";
+        final Revision revision = new Revision(99L, null, entityId);
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
+
+        // seed the database with a revision that has a newer version
+        createRevision(revision.getEntityId(), revision.getVersion() + 1, null);
+
+        // perform an update task which should throw InvalidRevisionException
+        revisionManager.deleteRevision(revisionClaim, createDeleteTask(entityId));
+    }
+
+    @Test
+    public void testDeleteRevisionWithStaleVersionAndSameClientId() {
+        // create the revision being sent in by the client
+        final String entityId = "entity-1";
+        final String clientId = "client-1";
+        final Revision revision = new Revision(99L, clientId, entityId);
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
+
+        // seed the database with a revision that has a newer version
+        createRevision(revision.getEntityId(), revision.getVersion() + 1, clientId);
+
+        // perform the delete
+        final RevisableEntity deletedEntity = revisionManager.deleteRevision(
+                revisionClaim, createDeleteTask(entityId));
+        assertNotNull(deletedEntity);
+        assertEquals(entityId, deletedEntity.getId());
+    }
+
+    @Test
+    public void testDeleteRevisionWithCurrentVersionAndNewClientId() {
+        // create the revision being sent in by the client
+        final String entityId = "entity-1";
+        final String clientId = "client-new";
+        final Revision revision = new Revision(99L, clientId, entityId);
+        final RevisionClaim revisionClaim = new StandardRevisionClaim(revision);
+
+        // seed the database with a revision that has same version but a different client id
+        createRevision(revision.getEntityId(), revision.getVersion(), "client-old");
+
+        // perform the delete
+        final RevisableEntity deletedEntity = revisionManager.deleteRevision(
+                revisionClaim, createDeleteTask(entityId));
+        assertNotNull(deletedEntity);
+        assertEquals(entityId, deletedEntity.getId());
+    }
+
+    @Test
+    public void testGetAllAndReset() {
+        createRevision("entity1", new Long(1), null);
+        createRevision("entity2", new Long(1), null);
+
+        final List<Revision> allRevisions = revisionManager.getAllRevisions();
+        assertNotNull(allRevisions);
+        assertEquals(2, allRevisions.size());
+
+        final Revision resetRevision1 = new Revision(10L, null, "resetEntity1");
+        final Revision resetRevision2 = new Revision(50L, null, "resetEntity2");
+        final Revision resetRevision3 = new Revision(20L, "client1", "resetEntity3");
+        revisionManager.reset(Arrays.asList(resetRevision1, resetRevision2, resetRevision3));
+
+        final List<Revision> afterResetRevisions = revisionManager.getAllRevisions();
+        assertNotNull(afterResetRevisions);
+        assertEquals(3, afterResetRevisions.size());
+
+        assertTrue(afterResetRevisions.contains(resetRevision1));
+        assertTrue(afterResetRevisions.contains(resetRevision2));
+        assertTrue(afterResetRevisions.contains(resetRevision3));
+    }
+
+    @Test
+    public void testGetRevisionMap() {
+        createRevision("entity1", new Long(1), null);
+        createRevision("entity2", new Long(1), null);
+
+        final Map<String,Revision> revisions = revisionManager.getRevisionMap();
+        assertNotNull(revisions);
+        assertEquals(2, revisions.size());
+
+        final Revision revision1 = revisions.get("entity1");
+        assertNotNull(revision1);
+        assertEquals("entity1", revision1.getEntityId());
+
+        final Revision revision2 = revisions.get("entity2");
+        assertNotNull(revision2);
+        assertEquals("entity2", revision2.getEntityId());
+    }
+
+    private DeleteRevisionTask<RevisableEntity> createDeleteTask(final String entityId) {
+        return () -> {
+            // normally we would retrieve the entity from some kind of service/dao
+            final RevisableEntity entity = new RevisableEntity();
+            entity.setId(entityId);
+            return entity;
+        };
+    }
+
+    private UpdateRevisionTask<RevisableEntity> createUpdateTask(final String entityId) {
+        return () -> {
+            // normally we would retrieve the entity from some kind of service/dao
+            final RevisableEntity entity = new RevisableEntity();
+            entity.setId(entityId);
+
+            // get the latest revision which has already been incremented
+            final Revision updatedRevision = revisionManager.getRevision(entity.getId());
+            entity.setRevision(updatedRevision);
+
+            final EntityModification entityModification = new EntityModification(updatedRevision, "user1");
+            return new StandardRevisionUpdate<>(entity, entityModification);
+        };
+    }
+
+    private void verifyRevisionUpdate(final String entityId, final RevisionUpdate<RevisableEntity> revisionUpdate,
+                                      final Long expectedVersion, final String expectedClientId) {
+        // verify we got back the entity we expected
+        final RevisableEntity updatedEntity = revisionUpdate.getEntity();
+        assertNotNull(updatedEntity);
+        assertEquals(entityId, updatedEntity.getId());
+
+        // verify the revision in the entity is set and is the updated revision (i.e. version of 100, not 99)
+        final Revision updatedRevision = updatedEntity.getRevision();
+        assertNotNull(updatedRevision);
+        assertEquals(entityId, updatedRevision.getEntityId());
+        assertEquals(expectedVersion, updatedRevision.getVersion());
+        assertEquals(expectedClientId, updatedRevision.getClientId());
+
+        // verify the entity modification is correctly populated
+        final EntityModification entityModification = revisionUpdate.getLastModification();
+        assertNotNull(entityModification);
+        Assert.assertEquals("user1", entityModification.getLastModifier());
+        assertEquals(updatedRevision, entityModification.getRevision());
+
+        // verify the updated revisions is correctly populated and matches the updated entity revision
+        final Set<Revision> updatedRevisions = revisionUpdate.getUpdatedRevisions();
+        assertNotNull(updatedRevisions);
+        assertEquals(1, updatedRevisions.size());
+        assertEquals(updatedRevision, updatedRevisions.stream().findFirst().get());
+    }
+
+    private void createRevision(final String entityId, final Long version, final String clientId) {
+        jdbcTemplate.update("INSERT INTO REVISION(ENTITY_ID, VERSION, CLIENT_ID) VALUES(?, ?, ?)", entityId, version, clientId);
+    }
+
+    /**
+     * Test object to represent a model/entity that has a revision field.
+     */
+    private static class RevisableEntity {
+
+        private String id;
+
+        private Revision revision;
+
+        public String getId() {
+            return id;
+        }
+
+        public void setId(String id) {
+            this.id = id;
+        }
+
+        public Revision getRevision() {
+            return revision;
+        }
+
+        public void setRevision(Revision revision) {
+            this.revision = revision;
+        }
+    }
+}
diff --git a/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/resources/application.properties b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/resources/application.properties
new file mode 100644
index 0000000..82b66dc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/nifi-registry-revision-spring-jdbc/src/test/resources/application.properties
@@ -0,0 +1,22 @@
+# 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.
+
+# Properties for Spring Boot tests
+
+# We are only using Flyway in tests to determine the DB type so there are no actual migrations
+spring.flyway.check-location=false
+
+# Controls logging of SQL queries and parameters
+# logging.level.org.springframework.jdbc: TRACE
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-revision/pom.xml b/nifi-registry-core/nifi-registry-revision/pom.xml
new file mode 100644
index 0000000..208f504
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-revision/pom.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.5.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-revision</artifactId>
+    <packaging>pom</packaging>
+
+    <modules>
+        <module>nifi-registry-revision-api</module>
+        <module>nifi-registry-revision-common</module>
+        <module>nifi-registry-revision-spring-jdbc</module>
+	<module>nifi-registry-revision-entity-model</module>
+	<module>nifi-registry-revision-entity-service</module>
+    </modules>
+
+</project>
diff --git a/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry-core/nifi-registry-web-api/pom.xml
index b165478..795e754 100644
--- a/nifi-registry-core/nifi-registry-web-api/pom.xml
+++ b/nifi-registry-core/nifi-registry-web-api/pom.xml
@@ -404,6 +404,16 @@
             <artifactId>spring-boot-starter-jetty</artifactId>
             <version>${spring.boot.version}</version>
             <scope>test</scope>
+	    <exclusions>
+                <exclusion>
+                    <groupId>org.eclipse.jetty.websocket</groupId>
+                    <artifactId>websocket-server</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.eclipse.jetty.websocket</groupId>
+                    <artifactId>javax-websocket-server-impl</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
         <dependency>
             <groupId>com.unboundid</groupId>
diff --git a/nifi-registry-core/pom.xml b/nifi-registry-core/pom.xml
index 830b0b2..23d9c93 100644
--- a/nifi-registry-core/pom.xml
+++ b/nifi-registry-core/pom.xml
@@ -47,6 +47,7 @@
         <module>nifi-registry-docker</module>
         <module>nifi-registry-bundle-utils</module>
         <module>nifi-registry-test</module>
+	<module>nifi-registry-revision</module>
     </modules>
 
     <dependencyManagement>
diff --git a/pom.xml b/pom.xml
index 69d1ed6..08886ff 100644
--- a/pom.xml
+++ b/pom.xml
@@ -94,13 +94,14 @@
         <jetty.version>9.4.19.v20190610</jetty.version>
         <jax.rs.api.version>2.1</jax.rs.api.version>
         <jersey.version>2.27</jersey.version>
-        <jackson.version>2.9.8</jackson.version>
-        <spring.boot.version>2.1.3.RELEASE</spring.boot.version>
-        <spring.security.version>5.1.3.RELEASE</spring.security.version>
-        <flyway.version>5.2.1</flyway.version>
+        <jackson.version>2.9.9</jackson.version>
+        <spring.boot.version>2.1.6.RELEASE</spring.boot.version>
+        <spring.security.version>5.1.5.RELEASE</spring.security.version>
+        <flyway.version>5.2.4</flyway.version>
         <flyway.tests.version>5.1.0</flyway.tests.version>
         <swagger.ui.version>3.12.0</swagger.ui.version>
         <testcontainers.version>1.11.2</testcontainers.version>
+	<h2.version>1.4.199</h2.version>
     </properties>
 
     <repositories>