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>