This is an automated email from the ASF dual-hosted git repository. kdoran pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/nifi-registry.git
The following commit(s) were added to refs/heads/master by this push: new 396e068 NIFIREG-300 Added nifi-registry-revision modules 396e068 is described below commit 396e068f277964e37bf0d2442fcfa679bc4e2683 Author: Bryan Bende <bbe...@apache.org> AuthorDate: Fri Aug 9 09:48:09 2019 -0400 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 <kdo...@apache.org> --- .travis.yml | 6 +- nifi-registry-assembly/NOTICE | 32 +- nifi-registry-core/nifi-registry-framework/pom.xml | 2 +- .../nifi-registry-revision-api/pom.xml | 27 ++ .../registry/revision/api/DeleteRevisionTask.java | 29 ++ .../registry/revision/api/EntityModification.java | 64 ++++ .../api/ExpiredRevisionClaimException.java | 31 ++ .../revision/api/InvalidRevisionException.java | 34 ++ .../nifi/registry/revision/api/Revision.java | 117 ++++++ .../nifi/registry/revision/api/RevisionClaim.java | 31 ++ .../registry/revision/api/RevisionManager.java | 92 +++++ .../nifi/registry/revision/api/RevisionUpdate.java | 43 +++ .../registry/revision/api/UpdateRevisionTask.java | 34 ++ .../nifi-registry-revision-common/pom.xml | 35 ++ .../revision/naive/NaiveRevisionManager.java | 139 +++++++ .../revision/standard/RevisionComparator.java | 42 +++ .../revision/standard/StandardRevisionClaim.java | 49 +++ .../revision/standard/StandardRevisionUpdate.java | 66 ++++ .../registry/revision/web/ClientIdParameter.java | 43 +++ .../nifi/registry/revision/web/LongParameter.java | 39 ++ .../nifi-registry-revision-entity-model/pom.xml | 34 ++ .../registry/revision/entity/RevisableEntity.java | 48 +++ .../registry/revision/entity/RevisionInfo.java | 79 ++++ .../nifi-registry-revision-entity-service/pom.xml | 46 +++ .../revision/entity/RevisableEntityService.java | 78 ++++ .../entity/StandardRevisableEntityService.java | 168 +++++++++ .../entity/TestStandardRevisableEntityService.java | 220 +++++++++++ .../nifi-registry-revision-spring-jdbc/pom.xml | 62 +++ .../revision/jdbc/JdbcRevisionManager.java | 227 +++++++++++ .../registry/revision/jdbc/RevisionRowMapper.java | 35 ++ .../org/apache/nifi/registry/TestApplication.java | 36 ++ .../revision/jdbc/TestJdbcRevisionManager.java | 418 +++++++++++++++++++++ .../src/test/resources/application.properties | 22 ++ nifi-registry-core/nifi-registry-revision/pom.xml | 34 ++ nifi-registry-core/nifi-registry-web-api/pom.xml | 10 + nifi-registry-core/pom.xml | 1 + pom.xml | 9 +- 37 files changed, 2459 insertions(+), 23 deletions(-) diff --git a/.travis.yml b/.travis.yml index ab00146..a85ddbd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ env: os: linux jdk: - - oraclejdk8 + - openjdk8 # Caches mvn repository in order to speed up builds cache: @@ -35,6 +35,9 @@ before_cache: # 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 @@ before_install: # 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 @@ The following binary components are provided under the Apache Software License v (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 @@ The following binary components are provided under the Apache Software License v (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 @@ The following binary components are provided under the Common Development and Di (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 @@ Eclipse Public License 1.0 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>