NIFIREG-211 Initial work for adding extenion bundles to NiFi Registry
- Setting up DB tables and entities for extensions
- Updated MetadataService and DatabaseMetadataService with new methods for extension entities
- Added ON DELETE CASCADE to existing tables and simplified delete logic for buckets and flows
- Created data model for extension bundles and mapping to/from DB entities
- Created ExtensionBundleExtractor with an implemenetation for NARs
- Setup LinkService and LinkBuilder for extension bundles
- Setup pluggable persistence provider for extension bundles and implemented a local file-system provider.
- Refactored LinkService and add links for all extension related items
- Changed extension service to write out bundles to a temp directory before extracting and passing to persistence provider
- Implemented multi-part form upload for extensions bundles
- Upgraded to spring-boot 2.1.0
- Added SHA-256 checksums for bundle versions
- Initial client methods for uploading and retrieving bundles
- Configuring NiFi Registry Jersey client to use chunked entity processing so we don't load the entire bundle content into memory during an upload
- Added event publishing for extension bundles
- Add an adapter for serializing ExtensionBundleType enum
- Remove capitalize class from droplet grid item
- Add ability for clients to optionally specify the SHA-256 when uploading a bundle
This closes #148.
Signed-off-by: Kevin Doran <kdoran@apache.org>
diff --git a/nifi-registry-assembly/pom.xml b/nifi-registry-assembly/pom.xml
index 91dad51..f1cc1ac 100644
--- a/nifi-registry-assembly/pom.xml
+++ b/nifi-registry-assembly/pom.xml
@@ -160,6 +160,9 @@
<!-- nifi-registry.properties: provider properties -->
<nifi.registry.providers.configuration.file>./conf/providers.xml</nifi.registry.providers.configuration.file>
+ <!-- nifi-registry.properties: extension properties -->
+ <nifi.registry.extensions.working.directory>./work/extensions</nifi.registry.extensions.working.directory>
+
<!-- nifi-registry.properties: legacy database properties, used to migrate data from old DB to the new DB below -->
<nifi.registry.db.directory />
<nifi.registry.db.url.append />
diff --git a/nifi-registry-core/nifi-registry-bundle-utils/pom.xml b/nifi-registry-core/nifi-registry-bundle-utils/pom.xml
new file mode 100644
index 0000000..670331e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bundle-utils/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 http://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.4.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>nifi-registry-bundle-utils</artifactId>
+ <packaging>jar</packaging>
+
+ <dependencies>
+
+ </dependencies>
+
+</project>
diff --git a/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/BundleCoordinate.java b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/BundleCoordinate.java
new file mode 100644
index 0000000..96ce5ea
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/BundleCoordinate.java
@@ -0,0 +1,87 @@
+/*
+ * 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.extension;
+
+/**
+ * The coordinate of an extension bundle (i.e group + artifact + version).
+ */
+public class BundleCoordinate {
+
+ private final String groupId;
+ private final String artifactId;
+ private final String version;
+
+ private final String coordinate;
+
+
+ public BundleCoordinate(final String groupId, final String artifactId, final String version) {
+ this.groupId = groupId;
+ this.artifactId = artifactId;
+ this.version = version;
+
+ if (isBlank(this.groupId) || isBlank(this.artifactId) || isBlank(this.version)) {
+ throw new IllegalStateException("Group, Id, and Version are required for BundleCoordinate");
+ }
+
+ this.coordinate = this.groupId + ":" + this.artifactId + ":" + this.version;
+ }
+
+ private boolean isBlank(String str) {
+ return str == null || str.trim().length() == 0;
+ }
+
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public String getArtifactId() {
+ return artifactId;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public final String getCoordinate() {
+ return coordinate;
+ }
+
+ @Override
+ public String toString() {
+ return coordinate;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+
+ if (!(obj instanceof BundleCoordinate)) {
+ return false;
+ }
+
+ final BundleCoordinate other = (BundleCoordinate) obj;
+ return getCoordinate().equals(other.getCoordinate());
+ }
+
+ @Override
+ public int hashCode() {
+ return 37 * this.coordinate.hashCode();
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/BundleDetails.java b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/BundleDetails.java
new file mode 100644
index 0000000..88ec469
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/BundleDetails.java
@@ -0,0 +1,72 @@
+/*
+ * 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.extension;
+
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class BundleDetails {
+
+ private final BundleCoordinate bundleCoordinate;
+
+ // Can be null when there is no dependent bundle
+ private final Set<BundleCoordinate> dependencyBundleCoordinates;
+
+ private BundleDetails(final Builder builder) {
+ this.bundleCoordinate = builder.bundleCoordinate;
+ this.dependencyBundleCoordinates = Collections.unmodifiableSet(new HashSet<>(builder.dependencyBundleCoordinates));
+ if (this.bundleCoordinate == null) {
+ throw new IllegalStateException("A bundle coordinate is required");
+ }
+ }
+
+ public BundleCoordinate getBundleCoordinate() {
+ return bundleCoordinate;
+ }
+
+ public Set<BundleCoordinate> getDependencyBundleCoordinates() {
+ return dependencyBundleCoordinates;
+ }
+
+ /**
+ * Builder for creating instances of BundleDetails.
+ */
+ public static class Builder {
+
+ private BundleCoordinate bundleCoordinate;
+ private Set<BundleCoordinate> dependencyBundleCoordinates = new HashSet<>();
+
+ public Builder coordinate(final BundleCoordinate bundleCoordinate) {
+ this.bundleCoordinate = bundleCoordinate;
+ return this;
+ }
+
+ public Builder dependencyCoordinate(final BundleCoordinate dependencyCoordinate) {
+ if (dependencyCoordinate != null) {
+ this.dependencyBundleCoordinates.add(dependencyCoordinate);
+ }
+ return this;
+ }
+
+ public BundleDetails build() {
+ return new BundleDetails(this);
+ }
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/BundleExtractor.java
similarity index 62%
copy from nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
copy to nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/BundleExtractor.java
index ec356fd..771c632 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
+++ b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/BundleExtractor.java
@@ -14,17 +14,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.nifi.registry.web.link.builder;
+package org.apache.nifi.registry.extension;
-import javax.ws.rs.core.Link;
+import java.io.IOException;
+import java.io.InputStream;
/**
- * Creates a Link for a given type.
- *
- * @param <T> the type to create a link for
+ * Extracts the bundle metadata from the given InputStream.
*/
-public interface LinkBuilder<T> {
+public interface BundleExtractor {
- Link createLink(T t);
+ /**
+ * @param inputStream the input stream of the binary bundle
+ * @return the bundle metadata extracted from the input stream
+ * @throws IOException if an error occurs reading from the InputStream
+ */
+ BundleDetails extract(InputStream inputStream) throws IOException;
}
diff --git a/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/minificpp/MiNiFiCppBundleExtractor.java b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/minificpp/MiNiFiCppBundleExtractor.java
new file mode 100644
index 0000000..ba0eb68
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/minificpp/MiNiFiCppBundleExtractor.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.extension.minificpp;
+
+import org.apache.nifi.registry.extension.BundleDetails;
+import org.apache.nifi.registry.extension.BundleExtractor;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * ExtensionBundleExtractor for MiNiFi CPP extensions.
+ */
+public class MiNiFiCppBundleExtractor implements BundleExtractor {
+
+ @Override
+ public BundleDetails extract(final InputStream inputStream) throws IOException {
+ // TODO implement
+ throw new UnsupportedOperationException("Minifi CPP extensions are not yet supported");
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/nar/NarBundleExtractor.java b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/nar/NarBundleExtractor.java
new file mode 100644
index 0000000..d9cfe71
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/nar/NarBundleExtractor.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.nifi.registry.extension.nar;
+
+import org.apache.nifi.registry.extension.BundleCoordinate;
+import org.apache.nifi.registry.extension.BundleDetails;
+import org.apache.nifi.registry.extension.BundleExtractor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.jar.Attributes;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+
+/**
+ * Implementation of ExtensionBundleExtractor for NAR bundles.
+ */
+public class NarBundleExtractor implements BundleExtractor {
+
+ @Override
+ public BundleDetails extract(final InputStream inputStream) throws IOException {
+ try (final JarInputStream jarInputStream = new JarInputStream(inputStream)) {
+ final Manifest manifest = jarInputStream.getManifest();
+ if (manifest == null) {
+ throw new IllegalArgumentException("NAR bundles must contain a valid MANIFEST");
+ }
+
+ final Attributes attributes = manifest.getMainAttributes();
+
+ final String groupId = attributes.getValue(NarManifestEntry.NAR_GROUP.getManifestName());
+ final String artifactId = attributes.getValue(NarManifestEntry.NAR_ID.getManifestName());
+ final String version = attributes.getValue(NarManifestEntry.NAR_VERSION.getManifestName());
+
+ final BundleCoordinate bundleCoordinate = new BundleCoordinate(groupId, artifactId, version);
+
+ final String dependencyGroupId = attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_GROUP.getManifestName());
+ final String dependencyArtifactId = attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_ID.getManifestName());
+ final String dependencyVersion = attributes.getValue(NarManifestEntry.NAR_DEPENDENCY_VERSION.getManifestName());
+
+ final BundleCoordinate dependencyCoordinate;
+ if (dependencyArtifactId != null) {
+ dependencyCoordinate = new BundleCoordinate(dependencyGroupId, dependencyArtifactId, dependencyVersion);
+ } else {
+ dependencyCoordinate = null;
+ }
+
+ // TODO figure out what to do with build info
+ final String buildBranch = attributes.getValue(NarManifestEntry.BUILD_BRANCH.getManifestName());
+ final String buildTag = attributes.getValue(NarManifestEntry.BUILD_TAG.getManifestName());
+ final String buildRevision = attributes.getValue(NarManifestEntry.BUILD_REVISION.getManifestName());
+ final String buildTimestamp = attributes.getValue(NarManifestEntry.BUILD_TIMESTAMP.getManifestName());
+ final String buildJdk = attributes.getValue(NarManifestEntry.BUILD_JDK.getManifestName());
+ final String builtBy = attributes.getValue(NarManifestEntry.BUILT_BY.getManifestName());
+
+ final BundleDetails.Builder builder = new BundleDetails.Builder()
+ .coordinate(bundleCoordinate)
+ .dependencyCoordinate(dependencyCoordinate);
+
+ return builder.build();
+ }
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/nar/NarManifestEntry.java b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/nar/NarManifestEntry.java
new file mode 100644
index 0000000..0c75ed3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bundle-utils/src/main/java/org/apache/nifi/registry/extension/nar/NarManifestEntry.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.extension.nar;
+
+/**
+ * Enumeration of entries that will be in a NAR MANIFEST file.
+ */
+public enum NarManifestEntry {
+
+ NAR_GROUP("Nar-Group"),
+ NAR_ID("Nar-Id"),
+ NAR_VERSION("Nar-Version"),
+ NAR_DEPENDENCY_GROUP("Nar-Dependency-Group"),
+ NAR_DEPENDENCY_ID("Nar-Dependency-Id"),
+ NAR_DEPENDENCY_VERSION("Nar-Dependency-Version"),
+ BUILD_TAG("Build-Tag"),
+ BUILD_REVISION("Build-Revision"),
+ BUILD_BRANCH("Build-Branch"),
+ BUILD_TIMESTAMP("Build-Timestamp"),
+ BUILD_JDK("Build-Jdk"),
+ BUILT_BY("Built-By"),
+ ;
+
+ final String manifestName;
+
+ NarManifestEntry(String manifestName) {
+ this.manifestName = manifestName;
+ }
+
+ public String getManifestName() {
+ return manifestName;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/extension/TestNarBundleExtractor.java b/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/extension/TestNarBundleExtractor.java
new file mode 100644
index 0000000..306f1d1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bundle-utils/src/test/java/org/apache/nifi/registry/extension/TestNarBundleExtractor.java
@@ -0,0 +1,93 @@
+/*
+ * 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.extension;
+
+import org.apache.nifi.registry.extension.nar.NarBundleExtractor;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+public class TestNarBundleExtractor {
+
+ private BundleExtractor extractor;
+
+ @Before
+ public void setup() {
+ this.extractor = new NarBundleExtractor();
+ }
+
+ @Test
+ public void testExtractFromGoodNarNoDependencies() throws IOException {
+ try (final InputStream in = new FileInputStream("src/test/resources/nars/nifi-framework-nar.nar")) {
+ final BundleDetails bundleDetails = extractor.extract(in);
+ assertNotNull(bundleDetails);
+ assertNotNull(bundleDetails.getBundleCoordinate());
+ assertNotNull(bundleDetails.getDependencyBundleCoordinates());
+ assertEquals(0, bundleDetails.getDependencyBundleCoordinates().size());
+
+ final BundleCoordinate bundleCoordinate = bundleDetails.getBundleCoordinate();
+ assertEquals("org.apache.nifi", bundleCoordinate.getGroupId());
+ assertEquals("nifi-framework-nar", bundleCoordinate.getArtifactId());
+ assertEquals("1.8.0", bundleCoordinate.getVersion());
+ }
+ }
+
+ @Test
+ public void testExtractFromGoodNarWithDependencies() throws IOException {
+ try (final InputStream in = new FileInputStream("src/test/resources/nars/nifi-foo-nar.nar")) {
+ final BundleDetails bundleDetails = extractor.extract(in);
+ assertNotNull(bundleDetails);
+ assertNotNull(bundleDetails.getBundleCoordinate());
+ assertNotNull(bundleDetails.getDependencyBundleCoordinates());
+ assertEquals(1, bundleDetails.getDependencyBundleCoordinates().size());
+
+ final BundleCoordinate bundleCoordinate = bundleDetails.getBundleCoordinate();
+ assertEquals("org.apache.nifi", bundleCoordinate.getGroupId());
+ assertEquals("nifi-foo-nar", bundleCoordinate.getArtifactId());
+ assertEquals("1.8.0", bundleCoordinate.getVersion());
+
+ final BundleCoordinate dependencyCoordinate = bundleDetails.getDependencyBundleCoordinates().stream().findFirst().get();
+ assertEquals("org.apache.nifi", dependencyCoordinate.getGroupId());
+ assertEquals("nifi-bar-nar", dependencyCoordinate.getArtifactId());
+ assertEquals("2.0.0", dependencyCoordinate.getVersion());
+ }
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testExtractFromNarMissingRequiredManifestEntries() throws IOException {
+ try (final InputStream in = new FileInputStream("src/test/resources/nars/nifi-missing-manifest-entries.nar")) {
+ extractor.extract(in);
+ fail("Should have thrown exception");
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testExtractFromNarMissingManifest() throws IOException {
+ try (final InputStream in = new FileInputStream("src/test/resources/nars/nifi-missing-manifest.nar")) {
+ extractor.extract(in);
+ fail("Should have thrown exception");
+ }
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-foo-nar.nar b/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-foo-nar.nar
new file mode 100644
index 0000000..e33ee88
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-foo-nar.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-framework-nar.nar b/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-framework-nar.nar
new file mode 100644
index 0000000..0d0319b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-framework-nar.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest-entries.nar b/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest-entries.nar
new file mode 100644
index 0000000..22b8d12
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest-entries.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest.nar b/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest.nar
new file mode 100644
index 0000000..bc930c8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-bundle-utils/src/test/resources/nars/nifi-missing-manifest.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-client/pom.xml b/nifi-registry-core/nifi-registry-client/pom.xml
index 5eb894b..6021d11 100644
--- a/nifi-registry-core/nifi-registry-client/pom.xml
+++ b/nifi-registry-core/nifi-registry-client/pom.xml
@@ -52,6 +52,11 @@
<version>${jersey.version}</version>
</dependency>
<dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-multipart</artifactId>
+ <version>${jersey.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${org.slf4j.version}</version>
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleClient.java
new file mode 100644
index 0000000..32eb3c9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleClient.java
@@ -0,0 +1,72 @@
+/*
+ * 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.client;
+
+import org.apache.nifi.registry.extension.ExtensionBundle;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Client for interacting with extension bundles.
+ */
+public interface ExtensionBundleClient {
+
+ /**
+ * Retrieves all extension bundles located in buckets the current user is authorized for.
+ *
+ * @return the list of extension bundles
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ List<ExtensionBundle> getAll() throws IOException, NiFiRegistryException;
+
+ /**
+ * Retrieves the extension bundles located in the given bucket.
+ *
+ * @param bucketId the bucket id
+ * @return the list of bundles in the bucket
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ List<ExtensionBundle> getByBucket(String bucketId) throws IOException, NiFiRegistryException;
+
+ /**
+ * Retrieves the extension bundle with the given id.
+ *
+ * @param bundleId the id of the bundle
+ * @return the bundle with the given id
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ ExtensionBundle get(String bundleId) throws IOException, NiFiRegistryException;
+
+ /**
+ * Deletes the extension bundle with the given id, and all of its versions.
+ *
+ * @param bundleId the bundle id
+ * @return the deleted bundle
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ ExtensionBundle delete(String bundleId) throws IOException, NiFiRegistryException;
+
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleVersionClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleVersionClient.java
new file mode 100644
index 0000000..8bad8c9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionBundleVersionClient.java
@@ -0,0 +1,156 @@
+/*
+ * 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.client;
+
+import org.apache.nifi.registry.extension.ExtensionBundleType;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Client for interacting with extension bundle versions.
+ */
+public interface ExtensionBundleVersionClient {
+
+ /**
+ * Uploads a version of an extension bundle to NiFi Registry where the bundle content comes from an InputStream.
+ *
+ * @param bucketId the bucket where the extension bundle will leave
+ * @param bundleType the type of bundle being uploaded
+ * @param bundleContentStream the input stream with the binary content of the bundle
+ * @return the ExtensionBundleVersion entity
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ ExtensionBundleVersion create(String bucketId, ExtensionBundleType bundleType, InputStream bundleContentStream)
+ throws IOException, NiFiRegistryException;
+
+ /**
+ * Uploads a version of an extension bundle to NiFi Registry where the bundle content comes from an InputStream.
+ *
+ * @param bucketId the bucket where the extension bundle will leave
+ * @param bundleType the type of bundle being uploaded
+ * @param bundleContentStream the input stream with the binary content of the bundle
+ * @param sha256 the optional SHA-256 in hex form
+ * @return the ExtensionBundleVersion entity
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ ExtensionBundleVersion create(String bucketId, ExtensionBundleType bundleType, InputStream bundleContentStream, String sha256)
+ throws IOException, NiFiRegistryException;
+
+ /**
+ * Uploads a version of an extension bundle to NiFi Registry where the bundle content comes from a File.
+ *
+ * @param bucketId the bucket where the extension bundle will leave
+ * @param bundleType the type of bundle being uploaded
+ * @param bundleFile the file with the binary content of the bundle
+ * @return the ExtensionBundleVersion entity
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ ExtensionBundleVersion create(String bucketId, ExtensionBundleType bundleType, File bundleFile)
+ throws IOException, NiFiRegistryException;
+
+ /**
+ * Uploads a version of an extension bundle to NiFi Registry where the bundle content comes from a File.
+ *
+ * @param bucketId the bucket where the extension bundle will leave
+ * @param bundleType the type of bundle being uploaded
+ * @param bundleFile the file with the binary content of the bundle
+ * @param sha256 the optional SHA-256 in hex form
+ * @return the ExtensionBundleVersion entity
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ ExtensionBundleVersion create(String bucketId, ExtensionBundleType bundleType, File bundleFile, String sha256)
+ throws IOException, NiFiRegistryException;
+
+
+ /**
+ * Retrieves the metadata about the versions of the given bundle.
+ *
+ * @param bundleId the bundle id
+ * @return the list of version metadata
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ List<ExtensionBundleVersionMetadata> getBundleVersions(String bundleId) throws IOException, NiFiRegistryException;
+
+ /**
+ * Retrieves bundle version entity for the given bundle id and version string.
+ *
+ * The entity contains all of the information about the version, such as the bucket, bundle, and version metadata.
+ *
+ * The binary content of the bundle can be obtained by calling {@method getBundleVersionContent}.
+ *
+ * @param bundleId the bundle id
+ * @param version the bundle version
+ * @return the ExtensionBundleVersion entity
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ ExtensionBundleVersion getBundleVersion(String bundleId, String version) throws IOException, NiFiRegistryException;
+
+ /**
+ * Obtains an InputStream for the binary content for the version of the given bundle.
+ *
+ * @param bundleId the bundle id
+ * @param version the version
+ * @return the InputStream for the bundle version content
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ InputStream getBundleVersionContent(String bundleId, String version) throws IOException, NiFiRegistryException;
+
+ /**
+ * Writes the binary content for the version of the given the bundle to the specified directory.
+ *
+ * @param bundleId the bundle id
+ * @param version the bundle version
+ * @param directory the directory to write to
+ * @return the File object for the bundle that was written
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ File writeBundleVersionContent(String bundleId, String version, File directory) throws IOException, NiFiRegistryException;
+
+ /**
+ * Deletes the given extension bundle version.
+ *
+ * @param bundleId the bundle id
+ * @param version the bundle version
+ * @return the deleted bundle versions
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ ExtensionBundleVersion delete(String bundleId, String version) throws IOException, NiFiRegistryException;
+
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionRepoClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionRepoClient.java
new file mode 100644
index 0000000..6682921
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/ExtensionRepoClient.java
@@ -0,0 +1,126 @@
+/*
+ * 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.client;
+
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+/**
+ * Client for interacting with the extension repository.
+ */
+public interface ExtensionRepoClient {
+
+ /**
+ * Gets the buckets in the extension repo.
+ *
+ * @return the list of extension repo buckets.
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ List<ExtensionRepoBucket> getBuckets() throws IOException, NiFiRegistryException;
+
+ /**
+ * Gets the extension repo groups in the specified bucket.
+ *
+ * @param bucketName the bucket name
+ * @return the list of groups
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ List<ExtensionRepoGroup> getGroups(String bucketName) throws IOException, NiFiRegistryException;
+
+ /**
+ * Gets the extension repo artifacts in the given bucket and group.
+ *
+ * @param bucketName the bucket name
+ * @param groupId the group id
+ * @return the list of artifacts
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ List<ExtensionRepoArtifact> getArtifacts(String bucketName, String groupId) throws IOException, NiFiRegistryException;
+
+ /**
+ * Gets the extension repo versions for the given bucket, group, artifact.
+ *
+ * @param bucketName the bucket name
+ * @param groupId the group id
+ * @param artifactId the artifact id
+ * @return the list of version summaries
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ List<ExtensionRepoVersionSummary> getVersions(String bucketName, String groupId, String artifactId)
+ throws IOException, NiFiRegistryException;
+
+ /**
+ * Gets the extension repo version for the given bucket, group, artifact, and version.
+ *
+ * @param bucketName the bucket name
+ * @param groupId the group id
+ * @param artifactId the artifact id
+ * @param version the version
+ * @return the extension repo version
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ ExtensionRepoVersion getVersion(String bucketName, String groupId, String artifactId, String version)
+ throws IOException, NiFiRegistryException;
+
+ /**
+ * Gets an InputStream for the binary content of the specified version.
+ *
+ * @param bucketName the bucket name
+ * @param groupId the group id
+ * @param artifactId the artifact id
+ * @param version the version
+ * @return the input stream
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ InputStream getVersionContent(String bucketName, String groupId, String artifactId, String version)
+ throws IOException, NiFiRegistryException;
+
+ /**
+ * Gets the hex representation of the SHA-256 hash of the binary content for the given version.
+ *
+ * @param bucketName the bucket name
+ * @param groupId the group id
+ * @param artifactId the artifact id
+ * @param version the version
+ * @return the SHA-256 hex string
+ *
+ * @throws IOException if an I/O error occurs
+ * @throws NiFiRegistryException if an non I/O error occurs
+ */
+ String getVersionSha256(String bucketName, String groupId, String artifactId, String version)
+ throws IOException, NiFiRegistryException;
+
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
index 07fb817..c5c4df4 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/NiFiRegistryClient.java
@@ -74,6 +74,36 @@
UserClient getUserClient(String ... proxiedEntity);
/**
+ * @return the client for interacting with extension bundles
+ */
+ ExtensionBundleClient getExtensionBundleClient();
+
+ /**
+ * @return the client for interacting with extension bundles on behalf of the given proxied entities
+ */
+ ExtensionBundleClient getExtensionBundleClient(String ... proxiedEntity);
+
+ /**
+ * @return the client for interacting with extension bundle versions
+ */
+ ExtensionBundleVersionClient getExtensionBundleVersionClient();
+
+ /**
+ * @return the client for interacting with extension bundle versions on behalf of the given proxied entities
+ */
+ ExtensionBundleVersionClient getExtensionBundleVersionClient(String ... proxiedEntity);
+
+ /**
+ * @return the client for interacting with the extension repository
+ */
+ ExtensionRepoClient getExtensionRepoClient();
+
+ /**
+ * @return the client for interacting with the extension repository on behalf of the given proxied entities
+ */
+ ExtensionRepoClient getExtensionRepoClient(String ... proxiedEntity);
+
+ /**
* The builder interface that implementations should provide for obtaining the client.
*/
interface Builder {
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java
index 5640d43..3bd7359 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/BucketItemDeserializer.java
@@ -24,6 +24,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.registry.bucket.BucketItem;
import org.apache.nifi.registry.bucket.BucketItemType;
+import org.apache.nifi.registry.extension.ExtensionBundle;
import org.apache.nifi.registry.flow.VersionedFlow;
import java.io.IOException;
@@ -65,8 +66,12 @@
final VersionedFlow versionedFlow = jsonParser.getCodec().treeToValue(node, VersionedFlow.class);
bucketItems.add(versionedFlow);
break;
+ case Extension_Bundle:
+ final ExtensionBundle extensionBundle = jsonParser.getCodec().treeToValue(node, ExtensionBundle.class);
+ bucketItems.add(extensionBundle);
+ break;
default:
- throw new IllegalStateException("Unknown type for BucketItem");
+ throw new IllegalStateException("Unknown type for BucketItem: " + bucketItemType);
}
}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleClient.java
new file mode 100644
index 0000000..b70a653
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleClient.java
@@ -0,0 +1,103 @@
+/*
+ * 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.client.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.client.ExtensionBundleClient;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+
+import javax.ws.rs.client.WebTarget;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Jersey implementation of ExtensionBundleClient.
+ */
+public class JerseyExtensionBundleClient extends AbstractJerseyClient implements ExtensionBundleClient {
+
+ private final WebTarget bucketExtensionBundlesTarget;
+ private final WebTarget extensionBundlesTarget;
+
+ public JerseyExtensionBundleClient(final WebTarget baseTarget) {
+ this(baseTarget, Collections.emptyMap());
+ }
+
+ public JerseyExtensionBundleClient(final WebTarget baseTarget, final Map<String, String> headers) {
+ super(headers);
+ this.bucketExtensionBundlesTarget = baseTarget.path("buckets/{bucketId}/extensions/bundles");
+ this.extensionBundlesTarget = baseTarget.path("extensions/bundles");
+ }
+
+ @Override
+ public List<ExtensionBundle> getAll() throws IOException, NiFiRegistryException {
+ return executeAction("Error getting extension bundles", () -> {
+ WebTarget target = extensionBundlesTarget;
+
+ final ExtensionBundle[] bundles = getRequestBuilder(target).get(ExtensionBundle[].class);
+ return bundles == null ? Collections.emptyList() : Arrays.asList(bundles);
+ });
+ }
+
+ @Override
+ public List<ExtensionBundle> getByBucket(final String bucketId) throws IOException, NiFiRegistryException {
+ if (StringUtils.isBlank(bucketId)) {
+ throw new IllegalArgumentException("Bucket id cannot be null or blank");
+ }
+
+ return executeAction("Error getting extension bundles for bucket", () -> {
+ WebTarget target = bucketExtensionBundlesTarget.resolveTemplate("bucketId", bucketId);
+
+ final ExtensionBundle[] bundles = getRequestBuilder(target).get(ExtensionBundle[].class);
+ return bundles == null ? Collections.emptyList() : Arrays.asList(bundles);
+ });
+ }
+
+ @Override
+ public ExtensionBundle get(final String bundleId) throws IOException, NiFiRegistryException {
+ if (StringUtils.isBlank(bundleId)) {
+ throw new IllegalArgumentException("Bundle id cannot be null or blank");
+ }
+
+ return executeAction("Error getting extension bundle", () -> {
+ WebTarget target = extensionBundlesTarget
+ .path("{bundleId}")
+ .resolveTemplate("bundleId", bundleId);
+
+ return getRequestBuilder(target).get(ExtensionBundle.class);
+ });
+ }
+
+ @Override
+ public ExtensionBundle delete(final String bundleId) throws IOException, NiFiRegistryException {
+ if (StringUtils.isBlank(bundleId)) {
+ throw new IllegalArgumentException("Bundle id cannot be null or blank");
+ }
+
+ return executeAction("Error deleting extension bundle", () -> {
+ WebTarget target = extensionBundlesTarget
+ .path("{bundleId}")
+ .resolveTemplate("bundleId", bundleId);
+
+ return getRequestBuilder(target).delete(ExtensionBundle.class);
+ });
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleVersionClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleVersionClient.java
new file mode 100644
index 0000000..6969317
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionBundleVersionClient.java
@@ -0,0 +1,280 @@
+/*
+ * 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.client.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.client.ExtensionBundleVersionClient;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.extension.ExtensionBundleType;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.glassfish.jersey.media.multipart.FormDataMultiPart;
+import org.glassfish.jersey.media.multipart.file.FileDataBodyPart;
+import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Jersey implementation of ExtensionBundleVersionClient.
+ */
+public class JerseyExtensionBundleVersionClient extends AbstractJerseyClient implements ExtensionBundleVersionClient {
+
+ private final WebTarget bucketExtensionBundlesTarget;
+ private final WebTarget extensionBundlesTarget;
+
+ public JerseyExtensionBundleVersionClient(final WebTarget baseTarget) {
+ this(baseTarget, Collections.emptyMap());
+ }
+
+ public JerseyExtensionBundleVersionClient(final WebTarget baseTarget, final Map<String, String> headers) {
+ super(headers);
+ this.bucketExtensionBundlesTarget = baseTarget.path("buckets/{bucketId}/extensions/bundles");
+ this.extensionBundlesTarget = baseTarget.path("extensions/bundles");
+ }
+
+ @Override
+ public ExtensionBundleVersion create(final String bucketId, final ExtensionBundleType bundleType, final InputStream bundleContentStream)
+ throws IOException, NiFiRegistryException {
+ return create(bucketId, bundleType, bundleContentStream, null);
+ }
+
+ @Override
+ public ExtensionBundleVersion create(final String bucketId, final ExtensionBundleType bundleType, final InputStream bundleContentStream, final String sha256)
+ throws IOException, NiFiRegistryException {
+
+ if (StringUtils.isBlank(bucketId)) {
+ throw new IllegalArgumentException("Bucket id cannot be null or blank");
+ }
+
+ if (bundleType == null) {
+ throw new IllegalArgumentException("Bundle type cannot be null");
+ }
+
+ if (bundleContentStream == null) {
+ throw new IllegalArgumentException("Bundle content cannot be null");
+ }
+
+ return executeAction("Error creating extension bundle version", () -> {
+ final WebTarget target = bucketExtensionBundlesTarget
+ .path("{bundleType}")
+ .resolveTemplate("bucketId", bucketId)
+ .resolveTemplate("bundleType", bundleType.toString());
+
+ final StreamDataBodyPart streamBodyPart = new StreamDataBodyPart("file", bundleContentStream);
+
+ final FormDataMultiPart multipart = new FormDataMultiPart();
+ multipart.bodyPart(streamBodyPart);
+
+ if (!StringUtils.isBlank(sha256)) {
+ multipart.field("sha256", sha256);
+ }
+
+ return getRequestBuilder(target)
+ .post(
+ Entity.entity(multipart, multipart.getMediaType()),
+ ExtensionBundleVersion.class
+ );
+ });
+ }
+
+ @Override
+ public ExtensionBundleVersion create(final String bucketId, final ExtensionBundleType bundleType, final File bundleFile)
+ throws IOException, NiFiRegistryException {
+ return create(bucketId, bundleType, bundleFile, null);
+ }
+
+ @Override
+ public ExtensionBundleVersion create(final String bucketId, final ExtensionBundleType bundleType, final File bundleFile, final String sha256)
+ throws IOException, NiFiRegistryException {
+
+ if (StringUtils.isBlank(bucketId)) {
+ throw new IllegalArgumentException("Bucket id cannot be null or blank");
+ }
+
+ if (bundleType == null) {
+ throw new IllegalArgumentException("Bundle type cannot be null");
+ }
+
+ if (bundleFile == null) {
+ throw new IllegalArgumentException("Bundle file cannot be null");
+ }
+
+ return executeAction("Error creating extension bundle version", () -> {
+ final WebTarget target = bucketExtensionBundlesTarget
+ .path("{bundleType}")
+ .resolveTemplate("bucketId", bucketId)
+ .resolveTemplate("bundleType", bundleType.toString());
+
+ final FileDataBodyPart fileBodyPart = new FileDataBodyPart("file", bundleFile, MediaType.APPLICATION_OCTET_STREAM_TYPE);
+
+ final FormDataMultiPart multipart = new FormDataMultiPart();
+ multipart.bodyPart(fileBodyPart);
+
+ if (!StringUtils.isBlank(sha256)) {
+ multipart.field("sha256", sha256);
+ }
+
+ return getRequestBuilder(target)
+ .post(
+ Entity.entity(multipart, multipart.getMediaType()),
+ ExtensionBundleVersion.class
+ );
+ });
+ }
+
+ @Override
+ public List<ExtensionBundleVersionMetadata> getBundleVersions(final String bundleId)
+ throws IOException, NiFiRegistryException {
+
+ if (StringUtils.isBlank(bundleId)) {
+ throw new IllegalArgumentException("Bundle id cannot be null or blank");
+ }
+
+ return executeAction("Error getting extension bundle versions", () -> {
+ final WebTarget target = extensionBundlesTarget
+ .path("{bundleId}/versions")
+ .resolveTemplate("bundleId", bundleId);
+
+ final ExtensionBundleVersionMetadata[] bundleVersions = getRequestBuilder(target).get(ExtensionBundleVersionMetadata[].class);
+ return bundleVersions == null ? Collections.emptyList() : Arrays.asList(bundleVersions);
+ });
+ }
+
+ @Override
+ public ExtensionBundleVersion getBundleVersion(final String bundleId, final String version)
+ throws IOException, NiFiRegistryException {
+
+ if (StringUtils.isBlank(bundleId)) {
+ throw new IllegalArgumentException("Bundle id cannot be null or blank");
+ }
+
+ if (StringUtils.isBlank(version)) {
+ throw new IllegalArgumentException("Version cannot be null or blank");
+ }
+
+ return executeAction("Error getting extension bundle version", () -> {
+ final WebTarget target = extensionBundlesTarget
+ .path("{bundleId}/versions/{version}")
+ .resolveTemplate("bundleId", bundleId)
+ .resolveTemplate("version", version);
+
+ return getRequestBuilder(target).get(ExtensionBundleVersion.class);
+ });
+ }
+
+ @Override
+ public InputStream getBundleVersionContent(final String bundleId, final String version)
+ throws IOException, NiFiRegistryException {
+
+ if (StringUtils.isBlank(bundleId)) {
+ throw new IllegalArgumentException("Bundle id cannot be null or blank");
+ }
+
+ if (StringUtils.isBlank(version)) {
+ throw new IllegalArgumentException("Version cannot be null or blank");
+ }
+
+ return executeAction("Error getting extension bundle version", () -> {
+ final WebTarget target = extensionBundlesTarget
+ .path("{bundleId}/versions/{version}/content")
+ .resolveTemplate("bundleId", bundleId)
+ .resolveTemplate("version", version);
+
+ return getRequestBuilder(target)
+ .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE)
+ .get()
+ .readEntity(InputStream.class);
+ });
+ }
+
+ @Override
+ public File writeBundleVersionContent(final String bundleId, final String version, final File directory)
+ throws IOException, NiFiRegistryException {
+
+ if (StringUtils.isBlank(bundleId)) {
+ throw new IllegalArgumentException("Bundle id cannot be null or blank");
+ }
+
+ if (StringUtils.isBlank(version)) {
+ throw new IllegalArgumentException("Version cannot be null or blank");
+ }
+
+ if (directory == null || !directory.exists() || !directory.isDirectory()) {
+ throw new IllegalArgumentException("Directory must exist and be a valid directory");
+ }
+
+ return executeAction("Error getting extension bundle version", () -> {
+ final WebTarget target = extensionBundlesTarget
+ .path("{bundleId}/versions/{version}/content")
+ .resolveTemplate("bundleId", bundleId)
+ .resolveTemplate("version", version);
+
+ final Response response = getRequestBuilder(target)
+ .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE)
+ .get();
+
+ final String contentDispositionHeader = response.getHeaderString("Content-Disposition");
+ if (StringUtils.isBlank(contentDispositionHeader)) {
+ throw new IllegalStateException("Content-Disposition header was blank or missing");
+ }
+
+ final int equalsIndex = contentDispositionHeader.lastIndexOf("=");
+ final String filename = contentDispositionHeader.substring(equalsIndex + 1).trim();
+ final File bundleFile = new File(directory, filename);
+
+ try (final InputStream responseInputStream = response.readEntity(InputStream.class)) {
+ Files.copy(responseInputStream, bundleFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ return bundleFile;
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to write bundle content due to: " + e.getMessage(), e);
+ }
+ });
+ }
+
+ @Override
+ public ExtensionBundleVersion delete(final String bundleId, final String version) throws IOException, NiFiRegistryException {
+ if (StringUtils.isBlank(bundleId)) {
+ throw new IllegalArgumentException("Bundle id cannot be null or blank");
+ }
+
+ if (StringUtils.isBlank(version)) {
+ throw new IllegalArgumentException("Version cannot be null or blank");
+ }
+
+ return executeAction("Error deleting extension bundle version", () -> {
+ final WebTarget target = extensionBundlesTarget
+ .path("{bundleId}/versions/{version}")
+ .resolveTemplate("bundleId", bundleId)
+ .resolveTemplate("version", version);
+
+ return getRequestBuilder(target).delete(ExtensionBundleVersion.class);
+ });
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java
new file mode 100644
index 0000000..18a6979
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyExtensionRepoClient.java
@@ -0,0 +1,199 @@
+/*
+ * 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.client.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.client.ExtensionRepoClient;
+import org.apache.nifi.registry.client.NiFiRegistryException;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
+
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class JerseyExtensionRepoClient extends AbstractJerseyClient implements ExtensionRepoClient {
+
+ private WebTarget extensionRepoTarget;
+
+ public JerseyExtensionRepoClient(final WebTarget baseTarget) {
+ this(baseTarget, Collections.emptyMap());
+ }
+
+ public JerseyExtensionRepoClient(final WebTarget baseTarget, final Map<String, String> headers) {
+ super(headers);
+ this.extensionRepoTarget = baseTarget.path("extensions/repo");
+ }
+
+ @Override
+ public List<ExtensionRepoBucket> getBuckets() throws IOException, NiFiRegistryException {
+ return executeAction("Error retrieving buckets for extension repo", () -> {
+ final ExtensionRepoBucket[] repoBuckets = getRequestBuilder(extensionRepoTarget).get(ExtensionRepoBucket[].class);
+ return repoBuckets == null ? Collections.emptyList() : Arrays.asList(repoBuckets);
+ });
+ }
+
+ @Override
+ public List<ExtensionRepoGroup> getGroups(final String bucketName) throws IOException, NiFiRegistryException {
+ if (StringUtils.isBlank(bucketName)) {
+ throw new IllegalArgumentException("Bucket name cannot be null or blank");
+ }
+
+ return executeAction("Error retrieving groups for extension repo", () -> {
+ final WebTarget target = extensionRepoTarget
+ .path("{bucketName}")
+ .resolveTemplate("bucketName", bucketName);
+
+ final ExtensionRepoGroup[] repoGroups = getRequestBuilder(target).get(ExtensionRepoGroup[].class);
+ return repoGroups == null ? Collections.emptyList() : Arrays.asList(repoGroups);
+ });
+ }
+
+ @Override
+ public List<ExtensionRepoArtifact> getArtifacts(final String bucketName, final String groupId)
+ throws IOException, NiFiRegistryException {
+
+ if (StringUtils.isBlank(bucketName)) {
+ throw new IllegalArgumentException("Bucket name cannot be null or blank");
+ }
+
+ if (StringUtils.isBlank(groupId)) {
+ throw new IllegalArgumentException("Group id cannot be null or blank");
+ }
+
+ return executeAction("Error retrieving artifacts for extension repo", () -> {
+ final WebTarget target = extensionRepoTarget
+ .path("{bucketName}/{groupId}")
+ .resolveTemplate("bucketName", bucketName)
+ .resolveTemplate("groupId", groupId);
+
+ final ExtensionRepoArtifact[] repoArtifacts = getRequestBuilder(target).get(ExtensionRepoArtifact[].class);
+ return repoArtifacts == null ? Collections.emptyList() : Arrays.asList(repoArtifacts);
+ });
+ }
+
+ @Override
+ public List<ExtensionRepoVersionSummary> getVersions(final String bucketName, final String groupId, final String artifactId)
+ throws IOException, NiFiRegistryException {
+
+ if (StringUtils.isBlank(bucketName)) {
+ throw new IllegalArgumentException("Bucket name cannot be null or blank");
+ }
+
+ if (StringUtils.isBlank(groupId)) {
+ throw new IllegalArgumentException("Group id cannot be null or blank");
+ }
+
+ if (StringUtils.isBlank(artifactId)) {
+ throw new IllegalArgumentException("Artifact id cannot be null or blank");
+ }
+
+ return executeAction("Error retrieving versions for extension repo", () -> {
+ final WebTarget target = extensionRepoTarget
+ .path("{bucketName}/{groupId}/{artifactId}")
+ .resolveTemplate("bucketName", bucketName)
+ .resolveTemplate("groupId", groupId)
+ .resolveTemplate("artifactId", artifactId);
+
+ final ExtensionRepoVersionSummary[] repoVersions = getRequestBuilder(target).get(ExtensionRepoVersionSummary[].class);
+ return repoVersions == null ? Collections.emptyList() : Arrays.asList(repoVersions);
+ });
+ }
+
+ @Override
+ public ExtensionRepoVersion getVersion(final String bucketName, final String groupId, final String artifactId, final String version)
+ throws IOException, NiFiRegistryException {
+
+ validate(bucketName, groupId, artifactId, version);
+
+ return executeAction("Error retrieving versions for extension repo", () -> {
+ final WebTarget target = extensionRepoTarget
+ .path("{bucketName}/{groupId}/{artifactId}/{version}")
+ .resolveTemplate("bucketName", bucketName)
+ .resolveTemplate("groupId", groupId)
+ .resolveTemplate("artifactId", artifactId)
+ .resolveTemplate("version", version);
+
+ return getRequestBuilder(target).get(ExtensionRepoVersion.class);
+ });
+ }
+
+ @Override
+ public InputStream getVersionContent(final String bucketName, final String groupId, final String artifactId, final String version)
+ throws IOException, NiFiRegistryException {
+
+ validate(bucketName, groupId, artifactId, version);
+
+ return executeAction("Error retrieving version content for extension repo", () -> {
+ final WebTarget target = extensionRepoTarget
+ .path("{bucketName}/{groupId}/{artifactId}/{version}/content")
+ .resolveTemplate("bucketName", bucketName)
+ .resolveTemplate("groupId", groupId)
+ .resolveTemplate("artifactId", artifactId)
+ .resolveTemplate("version", version);
+
+ return getRequestBuilder(target)
+ .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE)
+ .get()
+ .readEntity(InputStream.class);
+ });
+ }
+
+ @Override
+ public String getVersionSha256(final String bucketName, final String groupId, final String artifactId, final String version)
+ throws IOException, NiFiRegistryException {
+
+ validate(bucketName, groupId, artifactId, version);
+
+ return executeAction("Error retrieving version content for extension repo", () -> {
+ final WebTarget target = extensionRepoTarget
+ .path("{bucketName}/{groupId}/{artifactId}/{version}/sha256")
+ .resolveTemplate("bucketName", bucketName)
+ .resolveTemplate("groupId", groupId)
+ .resolveTemplate("artifactId", artifactId)
+ .resolveTemplate("version", version);
+
+ return getRequestBuilder(target).accept(MediaType.TEXT_PLAIN_TYPE).get(String.class);
+ });
+ }
+
+ private void validate(String bucketName, String groupId, String artifactId, String version) {
+ if (StringUtils.isBlank(bucketName)) {
+ throw new IllegalArgumentException("Bucket name cannot be null or blank");
+ }
+
+ if (StringUtils.isBlank(groupId)) {
+ throw new IllegalArgumentException("Group id cannot be null or blank");
+ }
+
+ if (StringUtils.isBlank(artifactId)) {
+ throw new IllegalArgumentException("Artifact id cannot be null or blank");
+ }
+
+ if (StringUtils.isBlank(version)) {
+ throw new IllegalArgumentException("Version cannot be null or blank");
+ }
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
index 329a47a..972211b 100644
--- a/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
+++ b/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
@@ -24,6 +24,9 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.registry.bucket.BucketItem;
import org.apache.nifi.registry.client.BucketClient;
+import org.apache.nifi.registry.client.ExtensionBundleClient;
+import org.apache.nifi.registry.client.ExtensionBundleVersionClient;
+import org.apache.nifi.registry.client.ExtensionRepoClient;
import org.apache.nifi.registry.client.FlowClient;
import org.apache.nifi.registry.client.FlowSnapshotClient;
import org.apache.nifi.registry.client.ItemsClient;
@@ -33,7 +36,9 @@
import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
+import org.glassfish.jersey.client.RequestEntityProcessing;
import org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
@@ -107,9 +112,13 @@
final ClientConfig clientConfig = new ClientConfig();
clientConfig.property(ClientProperties.CONNECT_TIMEOUT, connectTimeout);
clientConfig.property(ClientProperties.READ_TIMEOUT, readTimeout);
+ clientConfig.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.CHUNKED);
clientConfig.register(jacksonJaxbJsonProvider());
clientBuilder.withConfig(clientConfig);
- this.client = clientBuilder.build();
+
+ this.client = clientBuilder
+ .register(MultiPartFeature.class)
+ .build();
this.baseTarget = client.target(baseUrl);
this.bucketClient = new JerseyBucketClient(baseTarget);
@@ -173,6 +182,39 @@
return new JerseyUserClient(baseTarget, headers);
}
+ @Override
+ public ExtensionBundleClient getExtensionBundleClient() {
+ return new JerseyExtensionBundleClient(baseTarget);
+ }
+
+ @Override
+ public ExtensionBundleClient getExtensionBundleClient(String... proxiedEntity) {
+ final Map<String,String> headers = getHeaders(proxiedEntity);
+ return new JerseyExtensionBundleClient(baseTarget, headers);
+ }
+
+ @Override
+ public ExtensionBundleVersionClient getExtensionBundleVersionClient() {
+ return new JerseyExtensionBundleVersionClient(baseTarget);
+ }
+
+ @Override
+ public ExtensionBundleVersionClient getExtensionBundleVersionClient(String... proxiedEntity) {
+ final Map<String,String> headers = getHeaders(proxiedEntity);
+ return new JerseyExtensionBundleVersionClient(baseTarget, headers);
+ }
+
+ @Override
+ public ExtensionRepoClient getExtensionRepoClient() {
+ return new JerseyExtensionRepoClient(baseTarget);
+ }
+
+ @Override
+ public ExtensionRepoClient getExtensionRepoClient(String... proxiedEntity) {
+ final Map<String,String> headers = getHeaders(proxiedEntity);
+ return new JerseyExtensionRepoClient(baseTarget, headers);
+ }
+
private Map<String,String> getHeaders(String[] proxiedEntities) {
final String proxiedEntitiesValue = getProxiedEntitesValue(proxiedEntities);
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java
index e119c02..b746491 100644
--- a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java
@@ -23,5 +23,8 @@
// The case of these enum names matches what we want to return in
// the BucketItem.type field when serialized in an API response.
- Flow;
+
+ Flow,
+
+ Extension_Bundle;
}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundle.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundle.java
new file mode 100644
index 0000000..c9f0e7f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundle.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.extension;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.bucket.BucketItemType;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * Represents an extension bundle identified by a group and artifact id with in a bucket.
+ *
+ * Each bundle may then have one or more versions associated with it by creating an {@link ExtensionBundleVersion}.
+ *
+ * The {@link ExtensionBundleVersion} represents the actually binary bundle which may contain one or more extensions.
+ */
+@ApiModel
+@XmlRootElement
+public class ExtensionBundle extends BucketItem {
+
+ @NotNull
+ private ExtensionBundleType bundleType;
+
+ @NotBlank
+ private String groupId;
+
+ @NotBlank
+ private String artifactId;
+
+ @Min(0)
+ private long versionCount;
+
+ public ExtensionBundle() {
+ super(BucketItemType.Extension_Bundle);
+ }
+
+ @ApiModelProperty(value = "The type of the extension bundle")
+ public ExtensionBundleType getBundleType() {
+ return bundleType;
+ }
+
+ public void setBundleType(ExtensionBundleType bundleType) {
+ this.bundleType = bundleType;
+ }
+
+ @ApiModelProperty(value = "The group id of the extension bundle")
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(String groupId) {
+ this.groupId = groupId;
+ }
+
+ @ApiModelProperty(value = "The artifact id of the extension bundle")
+ public String getArtifactId() {
+ return artifactId;
+ }
+
+ public void setArtifactId(String artifactId) {
+ this.artifactId = artifactId;
+ }
+
+ @ApiModelProperty(value = "The number of versions of this extension bundle.", readOnly = true)
+ public long getVersionCount() {
+ return versionCount;
+ }
+
+ public void setVersionCount(long versionCount) {
+ this.versionCount = versionCount;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleType.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleType.java
new file mode 100644
index 0000000..0eb0447
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleType.java
@@ -0,0 +1,58 @@
+/*
+ * 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.extension;
+
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+/**
+ * The possible types of extension bundles.
+ */
+@XmlJavaTypeAdapter(ExtensionBundleTypeAdapter.class)
+public enum ExtensionBundleType {
+
+ NIFI_NAR("nifi-nar"),
+
+ MINIFI_CPP("minifi-cpp");
+
+ private final String displayName;
+
+ ExtensionBundleType(String displayName) {
+ this.displayName = displayName;
+ }
+
+ // Note: This method must be name fromString for JAX-RS/Jersey to use it on query and path params
+ public static ExtensionBundleType fromString(String value) {
+ if (value == null) {
+ throw new IllegalArgumentException("Value cannot be null");
+ }
+
+ for (final ExtensionBundleType type : values()) {
+ if (type.toString().equals(value)) {
+ return type;
+ }
+ }
+
+ throw new IllegalArgumentException("Unknown ExtensionBundleType: " + value);
+ }
+
+
+ @Override
+ public String toString() {
+ return displayName;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleTypeAdapter.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleTypeAdapter.java
new file mode 100644
index 0000000..1a993cf
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleTypeAdapter.java
@@ -0,0 +1,40 @@
+/*
+ * 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.extension;
+
+import javax.xml.bind.annotation.adapters.XmlAdapter;
+
+public class ExtensionBundleTypeAdapter extends XmlAdapter<String,ExtensionBundleType> {
+
+ @Override
+ public ExtensionBundleType unmarshal(String v) throws Exception {
+ if (v == null) {
+ return null;
+ }
+
+ return ExtensionBundleType.fromString(v);
+ }
+
+ @Override
+ public String marshal(final ExtensionBundleType v) throws Exception {
+ if (v == null) {
+ return null;
+ }
+
+ return v.toString();
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersion.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersion.java
new file mode 100644
index 0000000..a8ef0c3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersion.java
@@ -0,0 +1,98 @@
+/*
+ * 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.extension;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.link.LinkableEntity;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlTransient;
+import java.util.Set;
+
+@ApiModel
+@XmlRootElement
+public class ExtensionBundleVersion extends LinkableEntity {
+
+ @Valid
+ @NotNull
+ private ExtensionBundleVersionMetadata versionMetadata;
+
+ // read-only, only populated from retrieval of an individual bundle version
+ private Set<ExtensionBundleVersionDependency> dependencies;
+
+ // read-only, only populated from retrieval of an individual bundle version
+ private ExtensionBundle extensionBundle;
+
+ // read-only, only populated from retrieval of an individual bundle version
+ private Bucket bucket;
+
+ @ApiModelProperty(value = "The metadata about this version of the extension bundle")
+ public ExtensionBundleVersionMetadata getVersionMetadata() {
+ return versionMetadata;
+ }
+
+ public void setVersionMetadata(ExtensionBundleVersionMetadata versionMetadata) {
+ this.versionMetadata = versionMetadata;
+ }
+
+ @ApiModelProperty(value = "The set of other bundle versions that this version is dependent on", readOnly = true)
+ public Set<ExtensionBundleVersionDependency> getDependencies() {
+ return dependencies;
+ }
+
+ public void setDependencies(Set<ExtensionBundleVersionDependency> dependencies) {
+ this.dependencies = dependencies;
+ }
+
+ @ApiModelProperty(value = "The bundle this version is for", readOnly = true)
+ public ExtensionBundle getExtensionBundle() {
+ return extensionBundle;
+ }
+
+ public void setExtensionBundle(ExtensionBundle extensionBundle) {
+ this.extensionBundle = extensionBundle;
+ }
+
+ @ApiModelProperty(value = "The bucket that the extension bundle belongs to")
+ public Bucket getBucket() {
+ return bucket;
+ }
+
+ public void setBucket(Bucket bucket) {
+ this.bucket = bucket;
+ }
+
+ @XmlTransient
+ public String getFilename() {
+ final String filename = extensionBundle.getArtifactId() + "-" + versionMetadata.getVersion();
+
+ switch (extensionBundle.getBundleType()) {
+ case NIFI_NAR:
+ return filename + ".nar";
+ case MINIFI_CPP:
+ // TODO should CPP get a special extension
+ return filename;
+ default:
+ throw new IllegalStateException("Unknown bundle type: " + extensionBundle.getBundleType());
+ }
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionDependency.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionDependency.java
new file mode 100644
index 0000000..f84649b
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionDependency.java
@@ -0,0 +1,84 @@
+/*
+ * 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.extension;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+
+import javax.validation.constraints.NotBlank;
+import java.util.Objects;
+
+@ApiModel
+public class ExtensionBundleVersionDependency {
+
+ @NotBlank
+ private String groupId;
+
+ @NotBlank
+ private String artifactId;
+
+ @NotBlank
+ private String version;
+
+ @ApiModelProperty(value = "The group id of the bundle dependency")
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(String groupId) {
+ this.groupId = groupId;
+ }
+
+ @ApiModelProperty(value = "The artifact id of the bundle dependency")
+ public String getArtifactId() {
+ return artifactId;
+ }
+
+ public void setArtifactId(String artifactId) {
+ this.artifactId = artifactId;
+ }
+
+ @ApiModelProperty(value = "The version of the bundle dependency")
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(groupId, artifactId, version);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final ExtensionBundleVersionDependency other = (ExtensionBundleVersionDependency) obj;
+
+ return Objects.equals(groupId, other.groupId)
+ && Objects.equals(artifactId, other.artifactId)
+ && Objects.equals(version, other.version);
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionMetadata.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionMetadata.java
new file mode 100644
index 0000000..35756f9
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionMetadata.java
@@ -0,0 +1,163 @@
+/*
+ * 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.extension;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.link.LinkableEntity;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Objects;
+
+@ApiModel
+@XmlRootElement
+public class ExtensionBundleVersionMetadata extends LinkableEntity implements Comparable<ExtensionBundleVersionMetadata> {
+
+ @NotBlank
+ private String id;
+
+ @NotBlank
+ private String extensionBundleId;
+
+ @NotBlank
+ private String bucketId;
+
+ @NotBlank
+ private String version;
+
+ @Min(1)
+ private long timestamp;
+
+ @NotBlank
+ private String author;
+
+ private String description;
+
+ @NotBlank
+ private String sha256;
+
+ @NotNull
+ private Boolean sha256Supplied;
+
+
+ @ApiModelProperty(value = "The id of this version of the extension bundle")
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @ApiModelProperty(value = "The id of the extension bundle this version is for")
+ public String getExtensionBundleId() {
+ return extensionBundleId;
+ }
+
+ public void setExtensionBundleId(String extensionBundleId) {
+ this.extensionBundleId = extensionBundleId;
+ }
+
+ @ApiModelProperty(value = "The id of the bucket the extension bundle belongs to", required = true)
+ public String getBucketId() {
+ return bucketId;
+ }
+
+ public void setBucketId(String bucketId) {
+ this.bucketId = bucketId;
+ }
+
+ @ApiModelProperty(value = "The version of the extension bundle")
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ @ApiModelProperty(value = "The timestamp of the create date of this version")
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ @ApiModelProperty(value = "The identity that created this version")
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+ @ApiModelProperty(value = "The description for this version")
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ @ApiModelProperty(value = "The hex representation of the SHA-256 digest of the binary content for this version")
+ public String getSha256() {
+ return sha256;
+ }
+
+ public void setSha256(String sha256) {
+ this.sha256 = sha256;
+ }
+
+ @ApiModelProperty(value = "Whether or not the client supplied a SHA-256 when uploading the bundle")
+ public Boolean getSha256Supplied() {
+ return sha256Supplied;
+ }
+
+ public void setSha256Supplied(Boolean sha256Supplied) {
+ this.sha256Supplied = sha256Supplied;
+ }
+
+ @Override
+ public int compareTo(final ExtensionBundleVersionMetadata o) {
+ return o == null ? -1 : version.compareTo(o.version);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(this.id);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final ExtensionBundleVersionMetadata other = (ExtensionBundleVersionMetadata) obj;
+ return Objects.equals(this.id, other.id);
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoArtifact.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoArtifact.java
new file mode 100644
index 0000000..6b42678
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoArtifact.java
@@ -0,0 +1,93 @@
+/*
+ * 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.extension.repo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.link.LinkableEntity;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Comparator;
+import java.util.Objects;
+
+@ApiModel
+@XmlRootElement
+public class ExtensionRepoArtifact extends LinkableEntity implements Comparable<ExtensionRepoArtifact> {
+
+ private String bucketName;
+
+ private String groupId;
+
+ private String artifactId;
+
+ @ApiModelProperty(value = "The bucket name")
+ public String getBucketName() {
+ return bucketName;
+ }
+
+ public void setBucketName(String bucketName) {
+ this.bucketName = bucketName;
+ }
+
+ @ApiModelProperty("The group id")
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(String groupId) {
+ this.groupId = groupId;
+ }
+
+ @ApiModelProperty("The artifact id")
+ public String getArtifactId() {
+ return artifactId;
+ }
+
+ public void setArtifactId(String artifactId) {
+ this.artifactId = artifactId;
+ }
+
+ @Override
+ public int compareTo(final ExtensionRepoArtifact o) {
+ return Comparator.comparing(ExtensionRepoArtifact::getArtifactId)
+ .thenComparing(ExtensionRepoArtifact::getGroupId)
+ .thenComparing(ExtensionRepoArtifact::getBucketName)
+ .compare(this, o);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.bucketName, this.groupId, this.artifactId) ;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final ExtensionRepoArtifact other = (ExtensionRepoArtifact) obj;
+
+ return Objects.equals(this.getBucketName(), other.getBucketName())
+ && Objects.equals(this.getGroupId(), other.getGroupId())
+ && Objects.equals(this.getArtifactId(), other.getArtifactId());
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoBucket.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoBucket.java
new file mode 100644
index 0000000..1798df7
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoBucket.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.extension.repo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.link.LinkableEntity;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Comparator;
+import java.util.Objects;
+
+@ApiModel
+@XmlRootElement
+public class ExtensionRepoBucket extends LinkableEntity implements Comparable<ExtensionRepoBucket> {
+
+ private String bucketName;
+
+ @ApiModelProperty(value = "The name of the bucket")
+ public String getBucketName() {
+ return bucketName;
+ }
+
+ public void setBucketName(String bucketName) {
+ this.bucketName = bucketName;
+ }
+
+ @Override
+ public int compareTo(final ExtensionRepoBucket o) {
+ return Comparator.comparing(ExtensionRepoBucket::getBucketName).compare(this, o);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(this.bucketName);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final ExtensionRepoBucket other = (ExtensionRepoBucket) obj;
+ return Objects.equals(this.getBucketName(), other.getBucketName());
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoGroup.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoGroup.java
new file mode 100644
index 0000000..86e25f2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoGroup.java
@@ -0,0 +1,80 @@
+/*
+ * 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.extension.repo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.link.LinkableEntity;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Comparator;
+import java.util.Objects;
+
+@ApiModel
+@XmlRootElement
+public class ExtensionRepoGroup extends LinkableEntity implements Comparable<ExtensionRepoGroup> {
+
+ private String bucketName;
+
+ private String groupId;
+
+ @ApiModelProperty(value = "The bucket name")
+ public String getBucketName() {
+ return bucketName;
+ }
+
+ public void setBucketName(String bucketName) {
+ this.bucketName = bucketName;
+ }
+
+ @ApiModelProperty(value = "The group id")
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(String groupId) {
+ this.groupId = groupId;
+ }
+
+ @Override
+ public int compareTo(final ExtensionRepoGroup o) {
+ return Comparator.comparing(ExtensionRepoGroup::getGroupId)
+ .thenComparing(ExtensionRepoGroup::getBucketName)
+ .compare(this, o);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.bucketName, this.groupId) ;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final ExtensionRepoGroup other = (ExtensionRepoGroup) obj;
+
+ return Objects.equals(this.getBucketName(), other.getBucketName())
+ && Objects.equals(this.getGroupId(), other.getGroupId());
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersion.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersion.java
new file mode 100644
index 0000000..4dff6e6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersion.java
@@ -0,0 +1,68 @@
+/*
+ * 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.extension.repo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.link.LinkAdapter;
+
+import javax.ws.rs.core.Link;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
+
+@ApiModel
+@XmlRootElement
+public class ExtensionRepoVersion {
+
+ private Link downloadLink;
+
+ private Link sha256Link;
+
+ private Boolean sha256Supplied;
+
+ @XmlElement
+ @XmlJavaTypeAdapter(LinkAdapter.class)
+ @ApiModelProperty(value = "The WebLink to download this version of the extension bundle.", readOnly = true)
+ public Link getDownloadLink() {
+ return downloadLink;
+ }
+
+ public void setDownloadLink(Link downloadLink) {
+ this.downloadLink = downloadLink;
+ }
+
+ @XmlElement
+ @XmlJavaTypeAdapter(LinkAdapter.class)
+ @ApiModelProperty(value = "The WebLink to retrieve the SHA-256 digest for this version of the extension bundle.", readOnly = true)
+ public Link getSha256Link() {
+ return sha256Link;
+ }
+
+ public void setSha256Link(Link sha256Link) {
+ this.sha256Link = sha256Link;
+ }
+
+ @ApiModelProperty(value = "Indicates if the client supplied a SHA-256 when uploading this version of the extension bundle.", readOnly = true)
+ public Boolean getSha256Supplied() {
+ return sha256Supplied;
+ }
+
+ public void setSha256Supplied(Boolean sha256Supplied) {
+ this.sha256Supplied = sha256Supplied;
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersionSummary.java b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersionSummary.java
new file mode 100644
index 0000000..f73d32e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersionSummary.java
@@ -0,0 +1,106 @@
+/*
+ * 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.extension.repo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import org.apache.nifi.registry.link.LinkableEntity;
+
+import javax.xml.bind.annotation.XmlRootElement;
+import java.util.Comparator;
+import java.util.Objects;
+
+@ApiModel
+@XmlRootElement
+public class ExtensionRepoVersionSummary extends LinkableEntity implements Comparable<ExtensionRepoVersionSummary> {
+
+ private String bucketName;
+
+ private String groupId;
+
+ private String artifactId;
+
+ private String version;
+
+ @ApiModelProperty(value = "The bucket name")
+ public String getBucketName() {
+ return bucketName;
+ }
+
+ public void setBucketName(String bucketName) {
+ this.bucketName = bucketName;
+ }
+
+ @ApiModelProperty("The group id")
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(String groupId) {
+ this.groupId = groupId;
+ }
+
+ @ApiModelProperty("The artifact id")
+ public String getArtifactId() {
+ return artifactId;
+ }
+
+ public void setArtifactId(String artifactId) {
+ this.artifactId = artifactId;
+ }
+
+ @ApiModelProperty("The version")
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ @Override
+ public int compareTo(ExtensionRepoVersionSummary o) {
+ return Comparator.comparing(ExtensionRepoVersionSummary::getVersion)
+ .thenComparing(ExtensionRepoVersionSummary::getArtifactId)
+ .thenComparing(ExtensionRepoVersionSummary::getGroupId)
+ .thenComparing(ExtensionRepoVersionSummary::getBucketName)
+ .compare(this, o);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(this.bucketName, this.groupId, this.artifactId, this.version);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ final ExtensionRepoVersionSummary other = (ExtensionRepoVersionSummary) obj;
+
+ return Objects.equals(this.getBucketName(), other.getBucketName())
+ && Objects.equals(this.getGroupId(), other.getGroupId())
+ && Objects.equals(this.getArtifactId(), other.getArtifactId())
+ && Objects.equals(this.getVersion(), other.getVersion());
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/pom.xml b/nifi-registry-core/nifi-registry-framework/pom.xml
index 8ce4a13..c226e42 100644
--- a/nifi-registry-core/nifi-registry-framework/pom.xml
+++ b/nifi-registry-core/nifi-registry-framework/pom.xml
@@ -182,6 +182,11 @@
<version>0.4.0-SNAPSHOT</version>
</dependency>
<dependency>
+ <groupId>org.apache.nifi.registry</groupId>
+ <artifactId>nifi-registry-bundle-utils</artifactId>
+ <version>0.4.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
@@ -233,10 +238,6 @@
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
- <groupId>org.hibernate</groupId>
- <artifactId>hibernate-validator</artifactId>
- </dependency>
- <dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
</dependency>
@@ -266,6 +267,11 @@
</exclusions>
</dependency>
<dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-validation</artifactId>
+ <version>${spring.boot.version}</version>
+ </dependency>
+ <dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>${flyway.version}</version>
@@ -315,7 +321,7 @@
<dependency>
<groupId>org.flywaydb.flyway-test-extensions</groupId>
<artifactId>flyway-spring-test</artifactId>
- <version>${flyway.version}</version>
+ <version>${flyway.tests.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java
index 7748acf..13954c6 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java
@@ -61,7 +61,7 @@
@Override
public void migrate(Flyway flyway) {
- final boolean newDatabase = isNewDatabase(flyway.getDataSource());
+ final boolean newDatabase = isNewDatabase(flyway.getConfiguration().getDataSource());
if (newDatabase) {
LOGGER.info("First time initializing database...");
} else {
@@ -90,7 +90,7 @@
if (newDatabase && existingLegacyDatabase) {
final LegacyDataSourceFactory legacyDataSourceFactory = new LegacyDataSourceFactory(properties);
final DataSource legacyDataSource = legacyDataSourceFactory.getDataSource();
- final DataSource primaryDataSource = flyway.getDataSource();
+ final DataSource primaryDataSource = flyway.getConfiguration().getDataSource();
migrateData(legacyDataSource, primaryDataSource);
}
}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java
index 4d32790..de9cf69 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java
@@ -19,16 +19,27 @@
import org.apache.nifi.registry.db.entity.BucketEntity;
import org.apache.nifi.registry.db.entity.BucketItemEntity;
import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionDependencyEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionEntity;
+import org.apache.nifi.registry.db.entity.ExtensionEntity;
+import org.apache.nifi.registry.db.entity.ExtensionEntityCategory;
import org.apache.nifi.registry.db.entity.FlowEntity;
import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
import org.apache.nifi.registry.db.mapper.BucketEntityRowMapper;
import org.apache.nifi.registry.db.mapper.BucketItemEntityRowMapper;
+import org.apache.nifi.registry.db.mapper.ExtensionBundleEntityRowMapper;
+import org.apache.nifi.registry.db.mapper.ExtensionBundleEntityWithBucketNameRowMapper;
+import org.apache.nifi.registry.db.mapper.ExtensionBundleVersionDependencyEntityRowMapper;
+import org.apache.nifi.registry.db.mapper.ExtensionBundleVersionEntityRowMapper;
+import org.apache.nifi.registry.db.mapper.ExtensionEntityRowMapper;
import org.apache.nifi.registry.db.mapper.FlowEntityRowMapper;
import org.apache.nifi.registry.db.mapper.FlowSnapshotEntityRowMapper;
import org.apache.nifi.registry.service.MetadataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
@@ -84,19 +95,7 @@
@Override
public void deleteBucket(final BucketEntity bucket) {
- final String snapshotDeleteSql = "DELETE FROM flow_snapshot WHERE flow_id IN ( " +
- "SELECT f.id FROM flow f, bucket_item item WHERE f.id = item.id AND item.bucket_id = ?" +
- ")";
- jdbcTemplate.update(snapshotDeleteSql, bucket.getId());
-
- final String flowDeleteSql = "DELETE FROM flow WHERE id IN ( " +
- "SELECT f.id FROM flow f, bucket_item item WHERE f.id = item.id AND item.bucket_id = ?" +
- ")";
- jdbcTemplate.update(flowDeleteSql, bucket.getId());
-
- final String itemDeleteSql = "DELETE FROM bucket_item WHERE bucket_id = ?";
- jdbcTemplate.update(itemDeleteSql, bucket.getId());
-
+ // NOTE: Cascading deletes will delete from all child tables
final String sql = "DELETE FROM bucket WHERE id = ?";
jdbcTemplate.update(sql, bucket.getId());
}
@@ -128,25 +127,26 @@
//----------------- BucketItems ---------------------------------
+ private static final String BASE_BUCKET_ITEMS_SQL =
+ "SELECT " +
+ "item.id as ID, " +
+ "item.name as NAME, " +
+ "item.description as DESCRIPTION, " +
+ "item.created as CREATED, " +
+ "item.modified as MODIFIED, " +
+ "item.item_type as ITEM_TYPE, " +
+ "b.id as BUCKET_ID, " +
+ "b.name as BUCKET_NAME ," +
+ "eb.bundle_type as BUNDLE_TYPE, " +
+ "eb.group_id as BUNDLE_GROUP_ID, " +
+ "eb.artifact_id as BUNDLE_ARTIFACT_ID " +
+ "FROM bucket_item item " +
+ "INNER JOIN bucket b ON item.bucket_id = b.id " +
+ "LEFT JOIN extension_bundle eb ON item.id = eb.id ";
+
@Override
public List<BucketItemEntity> getBucketItems(final String bucketIdentifier) {
- final String sql =
- "SELECT " +
- "item.id as ID, " +
- "item.name as NAME, " +
- "item.description as DESCRIPTION, " +
- "item.created as CREATED, " +
- "item.modified as MODIFIED, " +
- "item.item_type as ITEM_TYPE, " +
- "b.id as BUCKET_ID, " +
- "b.name as BUCKET_NAME " +
- "FROM " +
- "bucket_item item, bucket b " +
- "WHERE " +
- "item.bucket_id = b.id " +
- "AND " +
- "item.bucket_id = ?";
-
+ final String sql = BASE_BUCKET_ITEMS_SQL + " WHERE item.bucket_id = ?";
final List<BucketItemEntity> items = jdbcTemplate.query(sql, new Object[] { bucketIdentifier }, new BucketItemEntityRowMapper());
return getItemsWithCounts(items);
}
@@ -157,23 +157,7 @@
return Collections.emptyList();
}
- final StringBuilder sqlBuilder = new StringBuilder(
- "SELECT " +
- "item.id as ID, " +
- "item.name as NAME, " +
- "item.description as DESCRIPTION, " +
- "item.created as CREATED, " +
- "item.modified as MODIFIED, " +
- "item.item_type as ITEM_TYPE, " +
- "b.id as BUCKET_ID, " +
- "b.name as BUCKET_NAME " +
- "FROM " +
- "bucket_item item, bucket b " +
- "WHERE " +
- "item.bucket_id = b.id " +
- "AND " +
- "item.bucket_id IN (");
-
+ final StringBuilder sqlBuilder = new StringBuilder(BASE_BUCKET_ITEMS_SQL + " WHERE item.bucket_id IN (");
for (int i=0; i < bucketIds.size(); i++) {
if (i > 0) {
sqlBuilder.append(", ");
@@ -188,6 +172,7 @@
private List<BucketItemEntity> getItemsWithCounts(final Iterable<BucketItemEntity> items) {
final Map<String,Long> snapshotCounts = getFlowSnapshotCounts();
+ final Map<String,Long> extensionBundleVersionCounts = getExtensionBundleVersionCounts();
final List<BucketItemEntity> itemWithCounts = new ArrayList<>();
for (final BucketItemEntity item : items) {
@@ -197,6 +182,12 @@
final FlowEntity flowEntity = (FlowEntity) item;
flowEntity.setSnapshotCount(snapshotCount);
}
+ } else if (item.getType() == BucketItemEntityType.EXTENSION_BUNDLE) {
+ final Long versionCount = extensionBundleVersionCounts.get(item.getId());
+ if (versionCount != null) {
+ final ExtensionBundleEntity extensionBundleEntity = (ExtensionBundleEntity) item;
+ extensionBundleEntity.setVersionCount(versionCount);
+ }
}
itemWithCounts.add(item);
@@ -223,6 +214,24 @@
});
}
+ private Map<String,Long> getExtensionBundleVersionCounts() {
+ final String sql = "SELECT extension_bundle_id, count(*) FROM extension_bundle_version GROUP BY extension_bundle_id";
+
+ final Map<String,Long> results = new HashMap<>();
+ jdbcTemplate.query(sql, (rs) -> {
+ results.put(rs.getString(1), rs.getLong(2));
+ });
+ return results;
+ }
+
+ private Long getExtensionBundleVersionCount(final String extensionBundleIdentifier) {
+ final String sql = "SELECT count(*) FROM extension_bundle_version WHERE extension_bundle_id = ?";
+
+ return jdbcTemplate.queryForObject(sql, new Object[] {extensionBundleIdentifier}, (rs, num) -> {
+ return rs.getLong(1);
+ });
+ }
+
//----------------- Flows ---------------------------------
@Override
@@ -309,12 +318,7 @@
@Override
public void deleteFlow(final FlowEntity flow) {
- final String snapshotDeleteSql = "DELETE FROM flow_snapshot WHERE flow_id = ?";
- jdbcTemplate.update(snapshotDeleteSql, flow.getId());
-
- final String flowDeleteSql = "DELETE FROM flow WHERE id = ?";
- jdbcTemplate.update(flowDeleteSql, flow.getId());
-
+ // NOTE: Cascading deletes will delete from child tables
final String itemDeleteSql = "DELETE FROM bucket_item WHERE id = ?";
jdbcTemplate.update(itemDeleteSql, flow.getId());
}
@@ -401,7 +405,456 @@
jdbcTemplate.update(sql, flowSnapshot.getFlowId(), flowSnapshot.getVersion());
}
- //----------------- BucketItems ---------------------------------
+ //----------------- Extension Bundles ---------------------------------
+
+ @Override
+ public ExtensionBundleEntity createExtensionBundle(final ExtensionBundleEntity extensionBundle) {
+ final String itemSql =
+ "INSERT INTO bucket_item (" +
+ "ID, " +
+ "NAME, " +
+ "DESCRIPTION, " +
+ "CREATED, " +
+ "MODIFIED, " +
+ "ITEM_TYPE, " +
+ "BUCKET_ID) " +
+ "VALUES (?, ?, ?, ?, ?, ?, ?)";
+
+ jdbcTemplate.update(itemSql,
+ extensionBundle.getId(),
+ extensionBundle.getName(),
+ extensionBundle.getDescription(),
+ extensionBundle.getCreated(),
+ extensionBundle.getModified(),
+ extensionBundle.getType().toString(),
+ extensionBundle.getBucketId());
+
+ final String bundleSql =
+ "INSERT INTO extension_bundle (" +
+ "ID, " +
+ "BUCKET_ID, " +
+ "BUNDLE_TYPE, " +
+ "GROUP_ID, " +
+ "ARTIFACT_ID) " +
+ "VALUES (?, ?, ?, ?, ?)";
+
+ jdbcTemplate.update(bundleSql,
+ extensionBundle.getId(),
+ extensionBundle.getBucketId(),
+ extensionBundle.getBundleType().toString(),
+ extensionBundle.getGroupId(),
+ extensionBundle.getArtifactId());
+
+ return extensionBundle;
+ }
+
+ @Override
+ public ExtensionBundleEntity getExtensionBundle(final String extensionBundleId) {
+ final String sql =
+ "SELECT * " +
+ "FROM extension_bundle eb, bucket_item item " +
+ "WHERE eb.id = ? AND item.id = eb.id";
+ try {
+ final ExtensionBundleEntity entity = jdbcTemplate.queryForObject(sql, new ExtensionBundleEntityRowMapper(), extensionBundleId);
+
+ final Long versionCount = getExtensionBundleVersionCount(extensionBundleId);
+ if (versionCount != null) {
+ entity.setVersionCount(versionCount);
+ }
+
+ return entity;
+ } catch (EmptyResultDataAccessException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public ExtensionBundleEntity getExtensionBundle(final String bucketId, final String groupId, final String artifactId) {
+ final String sql =
+ "SELECT * " +
+ "FROM " +
+ "extension_bundle eb, " +
+ "bucket_item item " +
+ "WHERE " +
+ "item.id = eb.id AND " +
+ "eb.bucket_id = ? AND " +
+ "eb.group_id = ? AND " +
+ "eb.artifact_id = ?";
+ try {
+ final ExtensionBundleEntity entity = jdbcTemplate.queryForObject(sql, new ExtensionBundleEntityRowMapper(), bucketId, groupId, artifactId);
+
+ final Long versionCount = getExtensionBundleVersionCount(entity.getId());
+ if (versionCount != null) {
+ entity.setVersionCount(versionCount);
+ }
+
+ return entity;
+ } catch (EmptyResultDataAccessException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public List<ExtensionBundleEntity> getExtensionBundles(final Set<String> bucketIds) {
+ if (bucketIds == null || bucketIds.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ final String selectSql =
+ "SELECT " +
+ "item.id as ID, " +
+ "item.name as NAME, " +
+ "item.description as DESCRIPTION, " +
+ "item.created as CREATED, " +
+ "item.modified as MODIFIED, " +
+ "item.item_type as ITEM_TYPE, " +
+ "b.id as BUCKET_ID, " +
+ "b.name as BUCKET_NAME ," +
+ "eb.bundle_type as BUNDLE_TYPE, " +
+ "eb.group_id as BUNDLE_GROUP_ID, " +
+ "eb.artifact_id as BUNDLE_ARTIFACT_ID " +
+ "FROM " +
+ "extension_bundle eb, " +
+ "bucket_item item, " +
+ "bucket b " +
+ "WHERE " +
+ "item.id = eb.id AND " +
+ "b.id = item.bucket_id";
+
+ final StringBuilder sqlBuilder = new StringBuilder(selectSql).append(" AND item.bucket_id IN (");
+ for (int i=0; i < bucketIds.size(); i++) {
+ if (i > 0) {
+ sqlBuilder.append(", ");
+ }
+ sqlBuilder.append("?");
+ }
+ sqlBuilder.append(") ");
+ sqlBuilder.append("ORDER BY eb.group_id ASC, eb.artifact_id ASC");
+
+ final List<ExtensionBundleEntity> bundleEntities = jdbcTemplate.query(sqlBuilder.toString(), bucketIds.toArray(), new ExtensionBundleEntityWithBucketNameRowMapper());
+ return populateVersionCounts(bundleEntities);
+ }
+
+ @Override
+ public List<ExtensionBundleEntity> getExtensionBundlesByBucket(final String bucketId) {
+ final String sql =
+ "SELECT * " +
+ "FROM " +
+ "extension_bundle eb, " +
+ "bucket_item item " +
+ "WHERE " +
+ "item.id = eb.id AND " +
+ "item.bucket_id = ? " +
+ "ORDER BY eb.group_id ASC, eb.artifact_id ASC";
+
+ final List<ExtensionBundleEntity> bundles = jdbcTemplate.query(sql, new Object[]{bucketId}, new ExtensionBundleEntityRowMapper());
+ return populateVersionCounts(bundles);
+ }
+
+ @Override
+ public List<ExtensionBundleEntity> getExtensionBundlesByBucketAndGroup(String bucketId, String groupId) {
+ final String sql =
+ "SELECT * " +
+ "FROM " +
+ "extension_bundle eb, " +
+ "bucket_item item " +
+ "WHERE " +
+ "item.id = eb.id AND " +
+ "item.bucket_id = ? AND " +
+ "eb.group_id = ?" +
+ "ORDER BY eb.group_id ASC, eb.artifact_id ASC";
+
+ final List<ExtensionBundleEntity> bundles = jdbcTemplate.query(sql, new Object[]{bucketId, groupId}, new ExtensionBundleEntityRowMapper());
+ return populateVersionCounts(bundles);
+ }
+
+ private List<ExtensionBundleEntity> populateVersionCounts(final List<ExtensionBundleEntity> bundles) {
+ if (!bundles.isEmpty()) {
+ final Map<String, Long> versionCounts = getExtensionBundleVersionCounts();
+ for (final ExtensionBundleEntity entity : bundles) {
+ final Long versionCount = versionCounts.get(entity.getId());
+ if (versionCount != null) {
+ entity.setVersionCount(versionCount);
+ }
+ }
+ }
+
+ return bundles;
+ }
+
+ @Override
+ public void deleteExtensionBundle(final ExtensionBundleEntity extensionBundle) {
+ deleteExtensionBundle(extensionBundle.getId());
+ }
+
+ @Override
+ public void deleteExtensionBundle(final String extensionBundleId) {
+ // NOTE: All of the foreign key constraints for extension related tables are set to cascade on delete
+ final String itemDeleteSql = "DELETE FROM bucket_item WHERE id = ?";
+ jdbcTemplate.update(itemDeleteSql, extensionBundleId);
+ }
+
+ //----------------- Extension Bundle Versions ---------------------------------
+
+ @Override
+ public ExtensionBundleVersionEntity createExtensionBundleVersion(final ExtensionBundleVersionEntity extensionBundleVersion) {
+ final String sql =
+ "INSERT INTO extension_bundle_version (" +
+ "ID, " +
+ "EXTENSION_BUNDLE_ID, " +
+ "VERSION, " +
+ "CREATED, " +
+ "CREATED_BY, " +
+ "DESCRIPTION, " +
+ "SHA_256_HEX, " +
+ "SHA_256_SUPPLIED " +
+ ") VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
+
+ jdbcTemplate.update(sql,
+ extensionBundleVersion.getId(),
+ extensionBundleVersion.getExtensionBundleId(),
+ extensionBundleVersion.getVersion(),
+ extensionBundleVersion.getCreated(),
+ extensionBundleVersion.getCreatedBy(),
+ extensionBundleVersion.getDescription(),
+ extensionBundleVersion.getSha256Hex(),
+ extensionBundleVersion.getSha256Supplied() ? 1 : 0);
+
+ return extensionBundleVersion;
+ }
+
+ @Override
+ public ExtensionBundleVersionEntity getExtensionBundleVersion(final String extensionBundleId, final String version) {
+ final String sql =
+ "SELECT * " +
+ "FROM extension_bundle_version " +
+ "WHERE extension_bundle_id = ? AND version = ?";
+
+ try {
+ return jdbcTemplate.queryForObject(sql, new ExtensionBundleVersionEntityRowMapper(), extensionBundleId, version);
+ } catch (EmptyResultDataAccessException e) {
+ return null;
+ }
+ }
+
+ private static final String BASE_EXTENSION_BUNDLE_SQL =
+ "SELECT " +
+ "ebv.id AS ID," +
+ "ebv.extension_bundle_id AS EXTENSION_BUNDLE_ID, " +
+ "ebv.version AS VERSION, " +
+ "ebv.created AS CREATED, " +
+ "ebv.created_by AS CREATED_BY, " +
+ "ebv.description AS DESCRIPTION, " +
+ "ebv.sha_256_hex AS SHA_256_HEX, " +
+ "ebv.sha_256_supplied AS SHA_256_SUPPLIED " +
+ "FROM extension_bundle eb, extension_bundle_version ebv " +
+ "WHERE eb.id = ebv.extension_bundle_id ";
+
+ @Override
+ public ExtensionBundleVersionEntity getExtensionBundleVersion(final String bucketId, final String groupId, final String artifactId, final String version) {
+ final String sql = BASE_EXTENSION_BUNDLE_SQL +
+ "AND eb.bucket_id = ? " +
+ "AND eb.group_id = ? " +
+ "AND eb.artifact_id = ? " +
+ "AND ebv.version = ?";
+
+ try {
+ return jdbcTemplate.queryForObject(sql, new ExtensionBundleVersionEntityRowMapper(), bucketId, groupId, artifactId, version);
+ } catch (EmptyResultDataAccessException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public List<ExtensionBundleVersionEntity> getExtensionBundleVersions(final String extensionBundleId) {
+ final String sql = "SELECT * FROM extension_bundle_version WHERE extension_bundle_id = ?";
+ return jdbcTemplate.query(sql, new Object[]{extensionBundleId}, new ExtensionBundleVersionEntityRowMapper());
+ }
+
+ @Override
+ public List<ExtensionBundleVersionEntity> getExtensionBundleVersions(final String bucketId, final String groupId, final String artifactId) {
+ final String sql = BASE_EXTENSION_BUNDLE_SQL +
+ "AND eb.bucket_id = ? " +
+ "AND eb.group_id = ? " +
+ "AND eb.artifact_id = ? ";
+
+ final Object[] args = {bucketId, groupId, artifactId};
+ return jdbcTemplate.query(sql, args, new ExtensionBundleVersionEntityRowMapper());
+ }
+
+ @Override
+ public List<ExtensionBundleVersionEntity> getExtensionBundleVersionsGlobal(final String groupId, final String artifactId, final String version) {
+ final String sql = BASE_EXTENSION_BUNDLE_SQL +
+ "AND eb.group_id = ? " +
+ "AND eb.artifact_id = ? " +
+ "AND ebv.version = ?";
+
+ final Object[] args = {groupId, artifactId, version};
+ return jdbcTemplate.query(sql, args, new ExtensionBundleVersionEntityRowMapper());
+ }
+
+ @Override
+ public void deleteExtensionBundleVersion(final ExtensionBundleVersionEntity extensionBundleVersion) {
+ deleteExtensionBundleVersion(extensionBundleVersion.getId());
+ }
+
+ @Override
+ public void deleteExtensionBundleVersion(final String extensionBundleVersionId) {
+ // NOTE: All of the foreign key constraints for extension related tables are set to cascade on delete
+ final String sql = "DELETE FROM extension_bundle_version WHERE id = ?";
+ jdbcTemplate.update(sql, extensionBundleVersionId);
+ }
+
+ //------------ Extension Bundle Version Dependencies ------------
+
+ @Override
+ public ExtensionBundleVersionDependencyEntity createDependency(final ExtensionBundleVersionDependencyEntity dependencyEntity) {
+ final String dependencySql =
+ "INSERT INTO extension_bundle_version_dependency (" +
+ "ID, " +
+ "EXTENSION_BUNDLE_VERSION_ID, " +
+ "GROUP_ID, " +
+ "ARTIFACT_ID, " +
+ "VERSION " +
+ ") VALUES (?, ?, ?, ?, ?)";
+
+ jdbcTemplate.update(dependencySql,
+ dependencyEntity.getId(),
+ dependencyEntity.getExtensionBundleVersionId(),
+ dependencyEntity.getGroupId(),
+ dependencyEntity.getArtifactId(),
+ dependencyEntity.getVersion());
+
+ return dependencyEntity;
+ }
+
+ @Override
+ public List<ExtensionBundleVersionDependencyEntity> getDependenciesForBundleVersion(final String extensionBundleVersionId) {
+ final String sql = "SELECT * FROM extension_bundle_version_dependency WHERE extension_bundle_version_id = ?";
+ final Object[] args = {extensionBundleVersionId};
+ return jdbcTemplate.query(sql, args, new ExtensionBundleVersionDependencyEntityRowMapper());
+ }
+
+
+ //----------------- Extensions ---------------------------------
+
+ @Override
+ public ExtensionEntity createExtension(final ExtensionEntity extension) {
+ final String insertExtensionSql =
+ "INSERT INTO extension (" +
+ "ID, " +
+ "EXTENSION_BUNDLE_VERSION_ID, " +
+ "TYPE, " +
+ "TYPE_DESCRIPTION, " +
+ "IS_RESTRICTED, " +
+ "CATEGORY, " +
+ "TAGS " +
+ ") VALUES (?, ?, ?, ?, ?, ?, ?)";
+
+ jdbcTemplate.update(insertExtensionSql,
+ extension.getId(),
+ extension.getExtensionBundleVersionId(),
+ extension.getType(),
+ extension.getTypeDescription(),
+ extension.isRestricted() ? 1 : 0,
+ extension.getCategory().toString(),
+ extension.getTags()
+ );
+
+ final String insertTagSql = "INSERT INTO extension_tag (EXTENSION_ID, TAG) VALUES (?, ?);";
+
+ if (extension.getTags() != null) {
+ final String tags[] = extension.getTags().split("[,]");
+ for (final String tag : tags) {
+ if (tag != null) {
+ jdbcTemplate.update(insertTagSql, extension.getId(), tag.trim().toLowerCase());
+ }
+ }
+ }
+
+ return extension;
+ }
+
+ @Override
+ public ExtensionEntity getExtensionById(final String id) {
+ final String selectSql = "SELECT * FROM extension WHERE id = ?";
+ try {
+ return jdbcTemplate.queryForObject(selectSql, new ExtensionEntityRowMapper(), id);
+ } catch (EmptyResultDataAccessException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public List<ExtensionEntity> getAllExtensions() {
+ final String selectSql = "SELECT * FROM extension ORDER BY type ASC";
+ return jdbcTemplate.query(selectSql, new ExtensionEntityRowMapper());
+ }
+
+ @Override
+ public List<ExtensionEntity> getExtensionsByBundleVersionId(final String extensionBundleVersionId) {
+ final String selectSql =
+ "SELECT * " +
+ "FROM extension " +
+ "WHERE extension_bundle_version_id = ?";
+
+ final Object[] args = { extensionBundleVersionId };
+ return jdbcTemplate.query(selectSql, args, new ExtensionEntityRowMapper());
+ }
+
+ @Override
+ public List<ExtensionEntity> getExtensionsByBundleCoordinate(final String bucketId, final String groupId, final String artifactId, final String version) {
+ final String sql =
+ "SELECT * " +
+ "FROM extension_bundle eb, extension_bundle_version ebv, extension e " +
+ "WHERE eb.id = ebv.extension_bundle_id " +
+ "AND ebv.id = e.extension_bundle_version_id " +
+ "AND eb.bucket_id = ? " +
+ "AND eb.group_id = ? " +
+ "AND eb.artifact_id = ? " +
+ "AND ebv.version = ?";
+
+ final Object[] args = { bucketId, groupId, artifactId, version };
+ return jdbcTemplate.query(sql, args, new ExtensionEntityRowMapper());
+ }
+
+ @Override
+ public List<ExtensionEntity> getExtensionsByCategory(final ExtensionEntityCategory category) {
+ final String selectSql = "SELECT * FROM extension WHERE category = ?";
+ final Object[] args = { category.toString() };
+ return jdbcTemplate.query(selectSql, args, new ExtensionEntityRowMapper());
+ }
+
+ @Override
+ public List<ExtensionEntity> getExtensionsByTag(final String tag) {
+ final String selectSql =
+ "SELECT * " +
+ "FROM extension e, extension_tag et " +
+ "WHERE e.id = et.extension_id AND et.tag = ?";
+
+ final Object[] args = { tag };
+ return jdbcTemplate.query(selectSql, args, new ExtensionEntityRowMapper());
+ }
+
+ @Override
+ public Set<String> getAllExtensionTags() {
+ final String selectSql = "SELECT DISTINCT tag FROM extension_tag ORDER BY tag ASC";
+
+ final Set<String> tags = new LinkedHashSet<>();
+ final RowCallbackHandler handler = (rs) -> tags.add(rs.getString(1));
+ jdbcTemplate.query(selectSql, handler);
+ return tags;
+ }
+
+ @Override
+ public void deleteExtension(final ExtensionEntity extension) {
+ // NOTE: All of the foreign key constraints for extension related tables are set to cascade on delete
+ final String deleteSql = "DELETE FROM extension WHERE id = ?";
+ jdbcTemplate.update(deleteSql, extension.getId());
+ }
+
+
+ //----------------- Fields ---------------------------------
@Override
public Set<String> getBucketFields() {
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java
index e78b2b1..2bbdd7c 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java
@@ -21,7 +21,10 @@
*/
public enum BucketItemEntityType {
- FLOW(Values.FLOW);
+ FLOW(Values.FLOW),
+
+ EXTENSION_BUNDLE(Values.EXTENSION_BUNDLE);
+
private final String value;
@@ -37,6 +40,7 @@
// need these constants to reference from @DiscriminatorValue
public static class Values {
public static final String FLOW = "FLOW";
+ public static final String EXTENSION_BUNDLE = "EXTENSION_BUNDLE";
}
}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleEntity.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleEntity.java
new file mode 100644
index 0000000..f79d385
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleEntity.java
@@ -0,0 +1,73 @@
+/*
+ * 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.db.entity;
+
+public class ExtensionBundleEntity extends BucketItemEntity {
+
+ private String groupId;
+
+ private String artifactId;
+
+ private ExtensionBundleEntityType bundleType;
+
+ private long versionCount;
+
+ public ExtensionBundleEntity() {
+ setType(BucketItemEntityType.EXTENSION_BUNDLE);
+ }
+
+ public ExtensionBundleEntityType getBundleType() {
+ return bundleType;
+ }
+
+ public void setBundleType(ExtensionBundleEntityType bundleType) {
+ this.bundleType = bundleType;
+ }
+
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(String groupId) {
+ this.groupId = groupId;
+ }
+
+ public String getArtifactId() {
+ return artifactId;
+ }
+
+ public void setArtifactId(String artifactId) {
+ this.artifactId = artifactId;
+ }
+
+ public long getVersionCount() {
+ return versionCount;
+ }
+
+ public void setVersionCount(long versionCount) {
+ this.versionCount = versionCount;
+ }
+
+ @Override
+ public void setType(BucketItemEntityType type) {
+ if (BucketItemEntityType.EXTENSION_BUNDLE != type) {
+ throw new IllegalStateException("Must set type to " + BucketItemEntityType.Values.EXTENSION_BUNDLE);
+ }
+ super.setType(type);
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleEntityType.java
similarity index 77%
copy from nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
copy to nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleEntityType.java
index ec356fd..0f4950c 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleEntityType.java
@@ -14,17 +14,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.nifi.registry.web.link.builder;
-
-import javax.ws.rs.core.Link;
+package org.apache.nifi.registry.db.entity;
/**
- * Creates a Link for a given type.
- *
- * @param <T> the type to create a link for
+ * The possible types of extension bundles.
*/
-public interface LinkBuilder<T> {
+public enum ExtensionBundleEntityType {
- Link createLink(T t);
+ NIFI_NAR,
+
+ MINIFI_CPP;
}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionDependencyEntity.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionDependencyEntity.java
new file mode 100644
index 0000000..e6e2010
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionDependencyEntity.java
@@ -0,0 +1,73 @@
+/*
+ * 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.db.entity;
+
+public class ExtensionBundleVersionDependencyEntity {
+
+ // Database id for this specific dependency
+ private String id;
+
+ // Foreign key to the extension bundle version this dependency goes with
+ private String extensionBundleVersionId;
+
+ // The bundle coordinates for this dependency
+ private String groupId;
+ private String artifactId;
+ private String version;
+
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getExtensionBundleVersionId() {
+ return extensionBundleVersionId;
+ }
+
+ public void setExtensionBundleVersionId(String extensionBundleVersionId) {
+ this.extensionBundleVersionId = extensionBundleVersionId;
+ }
+
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(String groupId) {
+ this.groupId = groupId;
+ }
+
+ public String getArtifactId() {
+ return artifactId;
+ }
+
+ public void setArtifactId(String artifactId) {
+ this.artifactId = artifactId;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionEntity.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionEntity.java
new file mode 100644
index 0000000..08fc2c2
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionEntity.java
@@ -0,0 +1,107 @@
+/*
+ * 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.db.entity;
+
+import java.util.Date;
+
+public class ExtensionBundleVersionEntity {
+
+ // Database id for this specific version of an extension bundle
+ private String id;
+
+ // Foreign key to the extension bundle this version goes with
+ private String extensionBundleId;
+
+ // The version of this bundle
+ private String version;
+
+ // General info about this version of the bundle
+ private Date created;
+ private String createdBy;
+ private String description;
+
+ // The hex representation of the SHA-256 digest for the binary content of this version
+ private String sha256Hex;
+
+ // Indicates whether the SHA-256 was supplied by the client, which means it matched the server's calculation, or was not supplied by the client
+ private boolean sha256Supplied;
+
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getExtensionBundleId() {
+ return extensionBundleId;
+ }
+
+ public void setExtensionBundleId(String extensionBundleId) {
+ this.extensionBundleId = extensionBundleId;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public Date getCreated() {
+ return created;
+ }
+
+ public void setCreated(Date created) {
+ this.created = created;
+ }
+
+ public String getCreatedBy() {
+ return createdBy;
+ }
+
+ public void setCreatedBy(String createdBy) {
+ this.createdBy = createdBy;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getSha256Hex() {
+ return sha256Hex;
+ }
+
+ public void setSha256Hex(String sha256Hex) {
+ this.sha256Hex = sha256Hex;
+ }
+
+ public boolean getSha256Supplied() {
+ return sha256Supplied;
+ }
+
+ public void setSha256Supplied(boolean sha256Supplied) {
+ this.sha256Supplied = sha256Supplied;
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionEntity.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionEntity.java
new file mode 100644
index 0000000..c48ac9f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionEntity.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.db.entity;
+
+public class ExtensionEntity {
+
+ private String id;
+
+ private String extensionBundleVersionId;
+
+ private String type;
+
+ private String typeDescription;
+
+ private boolean restricted;
+
+ private ExtensionEntityCategory category;
+
+ // Comma separated list of tags so we don't have to query tag table for each extension
+ private String tags;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getExtensionBundleVersionId() {
+ return extensionBundleVersionId;
+ }
+
+ public void setExtensionBundleVersionId(String extensionBundleVersionId) {
+ this.extensionBundleVersionId = extensionBundleVersionId;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getTypeDescription() {
+ return typeDescription;
+ }
+
+ public void setTypeDescription(String typeDescription) {
+ this.typeDescription = typeDescription;
+ }
+
+ public boolean isRestricted() {
+ return restricted;
+ }
+
+ public void setRestricted(boolean restricted) {
+ this.restricted = restricted;
+ }
+
+ public ExtensionEntityCategory getCategory() {
+ return category;
+ }
+
+ public void setCategory(ExtensionEntityCategory category) {
+ this.category = category;
+ }
+
+ public String getTags() {
+ return tags;
+ }
+
+ public void setTags(String tags) {
+ this.tags = tags;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionEntityCategory.java
similarity index 77%
copy from nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
copy to nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionEntityCategory.java
index ec356fd..b072b4a 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionEntityCategory.java
@@ -14,17 +14,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.nifi.registry.web.link.builder;
+package org.apache.nifi.registry.db.entity;
-import javax.ws.rs.core.Link;
+public enum ExtensionEntityCategory {
-/**
- * Creates a Link for a given type.
- *
- * @param <T> the type to create a link for
- */
-public interface LinkBuilder<T> {
+ PROCESSOR,
- Link createLink(T t);
+ CONTROLLER_SERVICE,
+
+ REPORTING_TASK;
}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionTagEntity.java
similarity index 64%
copy from nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
copy to nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionTagEntity.java
index ec356fd..1612442 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionTagEntity.java
@@ -14,17 +14,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.nifi.registry.web.link.builder;
+package org.apache.nifi.registry.db.entity;
-import javax.ws.rs.core.Link;
+public class ExtensionTagEntity {
-/**
- * Creates a Link for a given type.
- *
- * @param <T> the type to create a link for
- */
-public interface LinkBuilder<T> {
+ private String extensionId;
- Link createLink(T t);
+ private String tag;
+
+ public String getExtensionId() {
+ return extensionId;
+ }
+
+ public void setExtensionId(String extensionId) {
+ this.extensionId = extensionId;
+ }
+
+ public String getTag() {
+ return tag;
+ }
+
+ public void setTag(String tag) {
+ this.tag = tag;
+ }
}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketItemEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketItemEntityRowMapper.java
index 7b3df05..82c0a4c 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketItemEntityRowMapper.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/BucketItemEntityRowMapper.java
@@ -18,6 +18,8 @@
import org.apache.nifi.registry.db.entity.BucketItemEntity;
import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntityType;
import org.apache.nifi.registry.db.entity.FlowEntity;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.lang.Nullable;
@@ -38,6 +40,13 @@
case FLOW:
item = new FlowEntity();
break;
+ case EXTENSION_BUNDLE:
+ final ExtensionBundleEntity bundleEntity = new ExtensionBundleEntity();
+ bundleEntity.setBundleType(ExtensionBundleEntityType.valueOf(rs.getString("BUNDLE_TYPE")));
+ bundleEntity.setGroupId(rs.getString("BUNDLE_GROUP_ID"));
+ bundleEntity.setArtifactId(rs.getString("BUNDLE_ARTIFACT_ID"));
+ item = bundleEntity;
+ break;
default:
// should never happen
item = new BucketItemEntity();
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleEntityRowMapper.java
new file mode 100644
index 0000000..6375411
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleEntityRowMapper.java
@@ -0,0 +1,50 @@
+/*
+ * 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.db.mapper;
+
+import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntityType;
+import org.springframework.jdbc.core.RowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class ExtensionBundleEntityRowMapper implements RowMapper<ExtensionBundleEntity> {
+
+ @Override
+ public ExtensionBundleEntity mapRow(final ResultSet rs, final int i) throws SQLException {
+ final ExtensionBundleEntity entity = new ExtensionBundleEntity();
+
+ // BucketItemEntity fields
+ entity.setId(rs.getString("ID"));
+ entity.setName(rs.getString("NAME"));
+ entity.setDescription(rs.getString("DESCRIPTION"));
+ entity.setCreated(rs.getTimestamp("CREATED"));
+ entity.setModified(rs.getTimestamp("MODIFIED"));
+ entity.setBucketId(rs.getString("BUCKET_ID"));
+ entity.setType(BucketItemEntityType.EXTENSION_BUNDLE);
+
+ // ExtensionBundleEntity fields
+ entity.setBundleType(ExtensionBundleEntityType.valueOf(rs.getString("BUNDLE_TYPE")));
+ entity.setGroupId(rs.getString("GROUP_ID"));
+ entity.setArtifactId(rs.getString("ARTIFACT_ID"));
+
+ return entity;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleEntityWithBucketNameRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleEntityWithBucketNameRowMapper.java
new file mode 100644
index 0000000..3fb22b6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleEntityWithBucketNameRowMapper.java
@@ -0,0 +1,33 @@
+/*
+ * 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.db.mapper;
+
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntity;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class ExtensionBundleEntityWithBucketNameRowMapper extends ExtensionBundleEntityRowMapper {
+
+ @Override
+ public ExtensionBundleEntity mapRow(final ResultSet rs, final int i) throws SQLException {
+ final ExtensionBundleEntity entity = super.mapRow(rs, i);
+ entity.setBucketName(rs.getString("BUCKET_NAME"));
+ return entity;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleVersionDependencyEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleVersionDependencyEntityRowMapper.java
new file mode 100644
index 0000000..044e245
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleVersionDependencyEntityRowMapper.java
@@ -0,0 +1,38 @@
+/*
+ * 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.db.mapper;
+
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionDependencyEntity;
+import org.springframework.jdbc.core.RowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class ExtensionBundleVersionDependencyEntityRowMapper implements RowMapper<ExtensionBundleVersionDependencyEntity> {
+
+ @Override
+ public ExtensionBundleVersionDependencyEntity mapRow(final ResultSet rs, final int i) throws SQLException {
+ final ExtensionBundleVersionDependencyEntity entity = new ExtensionBundleVersionDependencyEntity();
+ entity.setId(rs.getString("ID"));
+ entity.setExtensionBundleVersionId(rs.getString("EXTENSION_BUNDLE_VERSION_ID"));
+ entity.setGroupId(rs.getString("GROUP_ID"));
+ entity.setArtifactId(rs.getString("ARTIFACT_ID"));
+ entity.setVersion(rs.getString("VERSION"));
+ return entity;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleVersionEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleVersionEntityRowMapper.java
new file mode 100644
index 0000000..60ca48f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionBundleVersionEntityRowMapper.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.db.mapper;
+
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionEntity;
+import org.springframework.jdbc.core.RowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class ExtensionBundleVersionEntityRowMapper implements RowMapper<ExtensionBundleVersionEntity> {
+
+ @Override
+ public ExtensionBundleVersionEntity mapRow(final ResultSet rs, final int i) throws SQLException {
+ final ExtensionBundleVersionEntity entity = new ExtensionBundleVersionEntity();
+ entity.setId(rs.getString("ID"));
+ entity.setExtensionBundleId(rs.getString("EXTENSION_BUNDLE_ID"));
+ entity.setVersion(rs.getString("VERSION"));
+ entity.setSha256Hex(rs.getString("SHA_256_HEX"));
+ entity.setSha256Supplied(rs.getInt("SHA_256_SUPPLIED") == 1);
+
+ entity.setCreated(rs.getTimestamp("CREATED"));
+ entity.setCreatedBy(rs.getString("CREATED_BY"));
+ entity.setDescription(rs.getString("DESCRIPTION"));
+
+ return entity;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionEntityRowMapper.java
new file mode 100644
index 0000000..057fbdb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionEntityRowMapper.java
@@ -0,0 +1,41 @@
+/*
+ * 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.db.mapper;
+
+import org.apache.nifi.registry.db.entity.ExtensionEntity;
+import org.apache.nifi.registry.db.entity.ExtensionEntityCategory;
+import org.springframework.jdbc.core.RowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class ExtensionEntityRowMapper implements RowMapper<ExtensionEntity> {
+
+ @Override
+ public ExtensionEntity mapRow(ResultSet rs, int i) throws SQLException {
+ final ExtensionEntity entity = new ExtensionEntity();
+ entity.setId(rs.getString("ID"));
+ entity.setExtensionBundleVersionId(rs.getString("EXTENSION_BUNDLE_VERSION_ID"));
+ entity.setType(rs.getString("TYPE"));
+ entity.setTypeDescription(rs.getString("TYPE_DESCRIPTION"));
+ entity.setRestricted(rs.getInt("IS_RESTRICTED") == 1);
+ entity.setCategory(ExtensionEntityCategory.valueOf(rs.getString("CATEGORY")));
+ entity.setTags(rs.getString("TAGS"));
+ return entity;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionTagEntityRowMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionTagEntityRowMapper.java
new file mode 100644
index 0000000..a3be127
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/mapper/ExtensionTagEntityRowMapper.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.db.mapper;
+
+import org.apache.nifi.registry.db.entity.ExtensionTagEntity;
+import org.springframework.jdbc.core.RowMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public class ExtensionTagEntityRowMapper implements RowMapper<ExtensionTagEntity> {
+
+ @Override
+ public ExtensionTagEntity mapRow(final ResultSet rs, final int i) throws SQLException {
+ final ExtensionTagEntity entity = new ExtensionTagEntity();
+ entity.setExtensionId(rs.getString("EXTENSION_ID"));
+ entity.setTag(rs.getString("TAG"));
+ return entity;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventFactory.java
index b837d6d..5fc885b 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventFactory.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/event/EventFactory.java
@@ -17,6 +17,8 @@
package org.apache.nifi.registry.event;
import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
import org.apache.nifi.registry.flow.VersionedFlow;
import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
import org.apache.nifi.registry.hook.Event;
@@ -94,4 +96,41 @@
.build();
}
+ public static Event extensionBundleCreated(final ExtensionBundle bundle) {
+ return new StandardEvent.Builder()
+ .eventType(EventType.CREATE_EXTENSION_BUNDLE)
+ .addField(EventFieldName.BUCKET_ID, bundle.getBucketIdentifier())
+ .addField(EventFieldName.EXTENSION_BUNDLE_ID, bundle.getIdentifier())
+ .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity())
+ .build();
+ }
+
+ public static Event extensionBundleDeleted(final ExtensionBundle bundle) {
+ return new StandardEvent.Builder()
+ .eventType(EventType.DELETE_EXTENSION_BUNDLE)
+ .addField(EventFieldName.BUCKET_ID, bundle.getBucketIdentifier())
+ .addField(EventFieldName.EXTENSION_BUNDLE_ID, bundle.getIdentifier())
+ .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity())
+ .build();
+ }
+
+ public static Event extensionBundleVersionCreated(final ExtensionBundleVersion bundleVersion) {
+ return new StandardEvent.Builder()
+ .eventType(EventType.CREATE_EXTENSION_BUNDLE_VERSION)
+ .addField(EventFieldName.BUCKET_ID, bundleVersion.getVersionMetadata().getBucketId())
+ .addField(EventFieldName.EXTENSION_BUNDLE_ID, bundleVersion.getVersionMetadata().getExtensionBundleId())
+ .addField(EventFieldName.VERSION, String.valueOf(bundleVersion.getVersionMetadata().getVersion()))
+ .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity())
+ .build();
+ }
+
+ public static Event extensionBundleVersionDeleted(final ExtensionBundleVersion bundleVersion) {
+ return new StandardEvent.Builder()
+ .eventType(EventType.DELETE_EXTENSION_BUNDLE_VERSION)
+ .addField(EventFieldName.BUCKET_ID, bundleVersion.getVersionMetadata().getBucketId())
+ .addField(EventFieldName.EXTENSION_BUNDLE_ID, bundleVersion.getVersionMetadata().getExtensionBundleId())
+ .addField(EventFieldName.VERSION, String.valueOf(bundleVersion.getVersionMetadata().getVersion()))
+ .addField(EventFieldName.USER, NiFiUserUtils.getNiFiUserIdentity())
+ .build();
+ }
}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java
index ca3259d..16e0e93 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/extension/ExtensionManager.java
@@ -58,6 +58,7 @@
classes.add(Authorizer.class);
classes.add(IdentityProvider.class);
classes.add(EventHookProvider.class);
+ classes.add(ExtensionBundlePersistenceProvider.class);
EXTENSION_CLASSES = Collections.unmodifiableList(classes);
}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactory.java
index a3f3276..450868f 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactory.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/ProviderFactory.java
@@ -18,6 +18,7 @@
import java.util.List;
+import org.apache.nifi.registry.extension.ExtensionBundlePersistenceProvider;
import org.apache.nifi.registry.flow.FlowPersistenceProvider;
import org.apache.nifi.registry.hook.EventHookProvider;
@@ -43,4 +44,9 @@
*/
List<EventHookProvider> getEventHookProviders();
+ /**
+ * @return the configured ExtensionBundlePersistenceProvider
+ */
+ ExtensionBundlePersistenceProvider getExtensionBundlePersistenceProvider();
+
}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderFactory.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderFactory.java
index 65ba914..89b2586 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderFactory.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/StandardProviderFactory.java
@@ -16,6 +16,7 @@
*/
package org.apache.nifi.registry.provider;
+import org.apache.nifi.registry.extension.ExtensionBundlePersistenceProvider;
import org.apache.nifi.registry.extension.ExtensionManager;
import org.apache.nifi.registry.flow.FlowPersistenceProvider;
import org.apache.nifi.registry.hook.EventHookProvider;
@@ -75,6 +76,7 @@
private FlowPersistenceProvider flowPersistenceProvider;
private List<EventHookProvider> eventHookProviders;
+ private ExtensionBundlePersistenceProvider extensionBundlePersistenceProvider;
@Autowired
public StandardProviderFactory(final NiFiRegistryProperties properties, final ExtensionManager extensionManager) {
@@ -204,6 +206,45 @@
return eventHookProviders;
}
+ @Bean
+ @Override
+ public synchronized ExtensionBundlePersistenceProvider getExtensionBundlePersistenceProvider() {
+ if (extensionBundlePersistenceProvider == null) {
+ if (providersHolder.get() == null) {
+ throw new ProviderFactoryException("ProviderFactory must be initialized before obtaining a Provider");
+ }
+
+ final Providers providers = providersHolder.get();
+ final org.apache.nifi.registry.provider.generated.Provider jaxbExtensionBundleProvider = providers.getExtensionBundlePersistenceProvider();
+ final String extensionBundleProviderClassName = jaxbExtensionBundleProvider.getClazz();
+
+ try {
+ final ClassLoader classLoader = extensionManager.getExtensionClassLoader(extensionBundleProviderClassName);
+ if (classLoader == null) {
+ throw new IllegalStateException("Extension not found in any of the configured class loaders: " + extensionBundleProviderClassName);
+ }
+
+ final Class<?> rawProviderClass = Class.forName(extensionBundleProviderClassName, true, classLoader);
+
+ final Class<? extends ExtensionBundlePersistenceProvider> extensionBundleProviderClass =
+ rawProviderClass.asSubclass(ExtensionBundlePersistenceProvider.class);
+
+ final Constructor constructor = extensionBundleProviderClass.getConstructor();
+ extensionBundlePersistenceProvider = (ExtensionBundlePersistenceProvider) constructor.newInstance();
+
+ LOGGER.info("Instantiated ExtensionBundlePersistenceProvider with class name {}", new Object[] {extensionBundleProviderClassName});
+ } catch (Exception e) {
+ throw new ProviderFactoryException("Error creating ExtensionBundlePersistenceProvider with class name: " + extensionBundleProviderClassName, e);
+ }
+
+ final ProviderConfigurationContext configurationContext = createConfigurationContext(jaxbExtensionBundleProvider.getProperty());
+ extensionBundlePersistenceProvider.onConfigured(configurationContext);
+ LOGGER.info("Configured FlowPersistenceProvider with class name {}", new Object[] {extensionBundleProviderClassName});
+ }
+
+ return extensionBundlePersistenceProvider;
+ }
+
private ProviderConfigurationContext createConfigurationContext(final List<Property> configProperties) {
final Map<String,String> properties = new HashMap<>();
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemExtensionBundlePersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemExtensionBundlePersistenceProvider.java
new file mode 100644
index 0000000..1d8c9cc
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/FileSystemExtensionBundlePersistenceProvider.java
@@ -0,0 +1,231 @@
+/*
+ * 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.provider.extension;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.extension.ExtensionBundleContext;
+import org.apache.nifi.registry.extension.ExtensionBundlePersistenceException;
+import org.apache.nifi.registry.extension.ExtensionBundlePersistenceProvider;
+import org.apache.nifi.registry.flow.FlowPersistenceException;
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.apache.nifi.registry.provider.ProviderCreationException;
+import org.apache.nifi.registry.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+
+/**
+ * An {@link ExtensionBundlePersistenceProvider} that uses local file-system for storage.
+ */
+public class FileSystemExtensionBundlePersistenceProvider implements ExtensionBundlePersistenceProvider {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(FileSystemExtensionBundlePersistenceProvider.class);
+
+ static final String BUNDLE_STORAGE_DIR_PROP = "Extension Bundle Storage Directory";
+
+ static final String NAR_EXTENSION = ".nar";
+ static final String CPP_EXTENSION = ".cpp";
+
+ private File bundleStorageDir;
+
+ @Override
+ public void onConfigured(final ProviderConfigurationContext configurationContext)
+ throws ProviderCreationException {
+ final Map<String,String> props = configurationContext.getProperties();
+ if (!props.containsKey(BUNDLE_STORAGE_DIR_PROP)) {
+ throw new ProviderCreationException("The property " + BUNDLE_STORAGE_DIR_PROP + " must be provided");
+ }
+
+ final String bundleStorageDirValue = props.get(BUNDLE_STORAGE_DIR_PROP);
+ if (StringUtils.isBlank(bundleStorageDirValue)) {
+ throw new ProviderCreationException("The property " + BUNDLE_STORAGE_DIR_PROP + " cannot be null or blank");
+ }
+
+ try {
+ bundleStorageDir = new File(bundleStorageDirValue);
+ FileUtils.ensureDirectoryExistAndCanReadAndWrite(bundleStorageDir);
+ LOGGER.info("Configured ExtensionBundlePersistenceProvider with Extension Bundle Storage Directory {}",
+ new Object[] {bundleStorageDir.getAbsolutePath()});
+ } catch (IOException e) {
+ throw new ProviderCreationException(e);
+ }
+ }
+
+ @Override
+ public synchronized void saveBundleVersion(final ExtensionBundleContext context, final InputStream contentStream)
+ throws ExtensionBundlePersistenceException {
+
+ final File bundleVersionDir = getBundleVersionDirectory(bundleStorageDir, context.getBucketName(),
+ context.getBundleGroupId(), context.getBundleArtifactId(), context.getBundleVersion());
+ try {
+ FileUtils.ensureDirectoryExistAndCanReadAndWrite(bundleVersionDir);
+ } catch (IOException e) {
+ throw new FlowPersistenceException("Error accessing directory for extension bundle version at "
+ + bundleVersionDir.getAbsolutePath(), e);
+ }
+
+ final File bundleFile = getBundleFile(bundleVersionDir, context.getBundleArtifactId(),
+ context.getBundleVersion(), context.getBundleType());
+
+ if (bundleFile.exists()) {
+ throw new ExtensionBundlePersistenceException("Unable to save because an extension bundle already exists at "
+ + bundleFile.getAbsolutePath());
+ }
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Writing extension bundle to {}", new Object[]{bundleFile.getAbsolutePath()});
+ }
+
+ try (final OutputStream out = new FileOutputStream(bundleFile)) {
+ IOUtils.copy(contentStream, out);
+ out.flush();
+ } catch (Exception e) {
+ throw new FlowPersistenceException("Unable to write bundle file to disk due to " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public synchronized void getBundleVersion(final ExtensionBundleContext context, final OutputStream outputStream)
+ throws ExtensionBundlePersistenceException {
+
+ final File bundleVersionDir = getBundleVersionDirectory(bundleStorageDir, context.getBucketName(),
+ context.getBundleGroupId(), context.getBundleArtifactId(), context.getBundleVersion());
+
+ final File bundleFile = getBundleFile(bundleVersionDir, context.getBundleArtifactId(),
+ context.getBundleVersion(), context.getBundleType());
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Reading extension bundle from {}", new Object[]{bundleFile.getAbsolutePath()});
+ }
+
+ try (final InputStream in = new FileInputStream(bundleFile);
+ final BufferedInputStream bufIn = new BufferedInputStream(in)) {
+ IOUtils.copy(bufIn, outputStream);
+ outputStream.flush();
+ } catch (FileNotFoundException e) {
+ throw new ExtensionBundlePersistenceException("Extension bundle content was not found for: " + bundleFile.getAbsolutePath(), e);
+ } catch (IOException e) {
+ throw new ExtensionBundlePersistenceException("Error reading extension bundle content", e);
+ }
+ }
+
+ @Override
+ public synchronized void deleteBundleVersion(final ExtensionBundleContext context) throws ExtensionBundlePersistenceException {
+ final File bundleVersionDir = getBundleVersionDirectory(bundleStorageDir, context.getBucketName(),
+ context.getBundleGroupId(), context.getBundleArtifactId(), context.getBundleVersion());
+
+ final File bundleFile = getBundleFile(bundleVersionDir, context.getBundleArtifactId(),
+ context.getBundleVersion(), context.getBundleType());
+
+ if (!bundleFile.exists()) {
+ LOGGER.warn("Extension bundle content does not exist at {}", new Object[] {bundleFile.getAbsolutePath()});
+ return;
+ }
+
+ final boolean deleted = bundleFile.delete();
+ if (!deleted) {
+ throw new ExtensionBundlePersistenceException("Unable to delete extension bundle content at " + bundleFile.getAbsolutePath());
+ }
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Deleted extension bundle content at {}", new Object[] {bundleFile.getAbsolutePath()});
+ }
+ }
+
+ @Override
+ public synchronized void deleteAllBundleVersions(final String bucketId, final String bucketName, final String groupId, final String artifactId)
+ throws ExtensionBundlePersistenceException {
+
+ final File bundleDir = getBundleDirectory(bundleStorageDir, bucketName, groupId, artifactId);
+ if (!bundleDir.exists()) {
+ LOGGER.warn("Extension bundle directory does not exist at {}", new Object[] {bundleDir.getAbsolutePath()});
+ return;
+ }
+
+ // delete everything under the bundle directory
+ try {
+ org.apache.commons.io.FileUtils.cleanDirectory(bundleDir);
+ } catch (IOException e) {
+ throw new FlowPersistenceException("Error deleting extension bundles at " + bundleDir.getAbsolutePath(), e);
+ }
+
+ // delete the directory for the bundle
+ final boolean bundleDirDeleted = bundleDir.delete();
+ if (!bundleDirDeleted) {
+ LOGGER.error("Unable to delete extension bundle directory: " + bundleDir.getAbsolutePath());
+ }
+
+ // delete the directory for the group and bucket if there is nothing left
+ final File groupDir = bundleDir.getParentFile();
+ final File[] groupFiles = groupDir.listFiles();
+ if (groupFiles.length == 0) {
+ final boolean deletedGroup = groupDir.delete();
+ if (!deletedGroup) {
+ LOGGER.error("Unable to delete group directory: " + groupDir.getAbsolutePath());
+ } else {
+ final File bucketDir = groupDir.getParentFile();
+ final File[] bucketFiles = bucketDir.listFiles();
+ if (bucketFiles.length == 0){
+ final boolean deletedBucket = bucketDir.delete();
+ if (!deletedBucket) {
+ LOGGER.error("Unable to delete bucket directory: " + bucketDir.getAbsolutePath());
+ }
+ }
+ }
+ }
+ }
+
+ static File getBundleDirectory(final File bundleStorageDir, final String bucketName, final String groupId, final String artifactId) {
+ return new File(bundleStorageDir, sanitize(bucketName) + "/" + sanitize(groupId) + "/" + sanitize(artifactId));
+ }
+
+ static File getBundleVersionDirectory(final File bundleStorageDir, final String bucketName, final String groupId, final String artifactId, final String version) {
+ return new File(bundleStorageDir, sanitize(bucketName) + "/" + sanitize(groupId) + "/" + sanitize(artifactId) + "/" + sanitize(version));
+ }
+
+ static File getBundleFile(final File parentDir, final String artifactId, final String version, final ExtensionBundleContext.BundleType bundleType) {
+ final String bundleFileExtension = getBundleFileExtension(bundleType);
+ final String bundleFilename = sanitize(artifactId) + "-" + sanitize(version) + bundleFileExtension;
+ return new File(parentDir, bundleFilename);
+ }
+
+ static String sanitize(final String input) {
+ return FileUtils.sanitizeFilename(input).trim().toLowerCase();
+ }
+
+ static String getBundleFileExtension(final ExtensionBundleContext.BundleType bundleType) {
+ switch (bundleType) {
+ case NIFI_NAR:
+ return NAR_EXTENSION;
+ case MINIFI_CPP:
+ return CPP_EXTENSION;
+ default:
+ throw new IllegalArgumentException("Unknown bundle type: " + bundleType);
+ }
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardExtensionBundleContext.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardExtensionBundleContext.java
new file mode 100644
index 0000000..8b3e068
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/provider/extension/StandardExtensionBundleContext.java
@@ -0,0 +1,176 @@
+/*
+ * 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.provider.extension;
+
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.extension.ExtensionBundleContext;
+
+public class StandardExtensionBundleContext implements ExtensionBundleContext {
+
+ private final BundleType bundleType;
+ private final String bucketId;
+ private final String bucketName;
+ private final String bundleId;
+ private final String bundleGroupId;
+ private final String bundleArtifactId;
+ private final String bundleVersion;
+ private final String description;
+ private final String author;
+ private final long timestamp;
+
+ private StandardExtensionBundleContext(final Builder builder) {
+ this.bundleType = builder.bundleType;
+ this.bucketId = builder.bucketId;
+ this.bucketName = builder.bucketName;
+ this.bundleId = builder.bundleId;
+ this.bundleGroupId = builder.bundleGroupId;
+ this.bundleArtifactId = builder.bundleArtifactId;
+ this.bundleVersion = builder.bundleVersion;
+ this.description = builder.description;
+ this.author = builder.author;
+ this.timestamp = builder.timestamp;
+ Validate.notNull(this.bundleType);
+ Validate.notBlank(this.bucketId);
+ Validate.notBlank(this.bucketName);
+ Validate.notBlank(this.bundleId);
+ Validate.notBlank(this.bundleGroupId);
+ Validate.notBlank(this.bundleArtifactId);
+ Validate.notBlank(this.bundleVersion);
+ Validate.notBlank(this.author);
+ }
+
+
+ @Override
+ public BundleType getBundleType() {
+ return bundleType;
+ }
+
+ @Override
+ public String getBucketId() {
+ return bucketId;
+ }
+
+ @Override
+ public String getBucketName() {
+ return bucketName;
+ }
+
+ @Override
+ public String getBundleId() {
+ return bundleId;
+ }
+
+ @Override
+ public String getBundleGroupId() {
+ return bundleGroupId;
+ }
+
+ @Override
+ public String getBundleArtifactId() {
+ return bundleArtifactId;
+ }
+
+ @Override
+ public String getBundleVersion() {
+ return bundleVersion;
+ }
+
+ @Override
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ @Override
+ public String getAuthor() {
+ return author;
+ }
+
+ public static class Builder {
+
+ private BundleType bundleType;
+ private String bucketId;
+ private String bucketName;
+ private String bundleId;
+ private String bundleGroupId;
+ private String bundleArtifactId;
+ private String bundleVersion;
+ private String description;
+ private String author;
+ private long timestamp;
+
+ public Builder bundleType(final BundleType bundleType) {
+ this.bundleType = bundleType;
+ return this;
+ }
+
+ public Builder bucketId(final String bucketId) {
+ this.bucketId = bucketId;
+ return this;
+ }
+
+ public Builder bucketName(final String bucketName) {
+ this.bucketName = bucketName;
+ return this;
+ }
+
+ public Builder bundleId(final String bundleId) {
+ this.bundleId = bundleId;
+ return this;
+ }
+
+ public Builder bundleGroupId(final String bundleGroupId) {
+ this.bundleGroupId = bundleGroupId;
+ return this;
+ }
+
+ public Builder bundleArtifactId(final String bundleArtifactId) {
+ this.bundleArtifactId = bundleArtifactId;
+ return this;
+ }
+
+ public Builder bundleVersion(final String bundleVersion) {
+ this.bundleVersion = bundleVersion;
+ return this;
+ }
+
+ public Builder description(final String description) {
+ this.description = description;
+ return this;
+ }
+
+ public Builder author(final String author) {
+ this.author = author;
+ return this;
+ }
+
+ public Builder timestamp(final long timestamp) {
+ this.timestamp = timestamp;
+ return this;
+ }
+
+ public StandardExtensionBundleContext build() {
+ return new StandardExtensionBundleContext(this);
+ }
+
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
index 3436662..6974cb9 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/DataModelMapper.java
@@ -19,11 +19,19 @@
import org.apache.nifi.registry.bucket.Bucket;
import org.apache.nifi.registry.db.entity.BucketEntity;
import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntityType;
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionDependencyEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionEntity;
import org.apache.nifi.registry.db.entity.FlowEntity;
import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
import org.apache.nifi.registry.db.entity.KeyEntity;
import org.apache.nifi.registry.diff.ComponentDifference;
import org.apache.nifi.registry.diff.ComponentDifferenceGroup;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleType;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionDependency;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
import org.apache.nifi.registry.flow.VersionedComponent;
import org.apache.nifi.registry.flow.VersionedFlow;
import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
@@ -145,6 +153,122 @@
return null;
}
+ // -- Map ExtensionBundleType
+
+ public static ExtensionBundleEntityType map(final ExtensionBundleType bundleType) {
+ switch (bundleType) {
+ case NIFI_NAR:
+ return ExtensionBundleEntityType.NIFI_NAR;
+
+ case MINIFI_CPP:
+ return ExtensionBundleEntityType.MINIFI_CPP;
+ default:
+ throw new IllegalArgumentException("Unknown bundle type: " + bundleType);
+ }
+ }
+
+ public static ExtensionBundleType map(final ExtensionBundleEntityType bundleEntityType) {
+ switch (bundleEntityType) {
+ case NIFI_NAR:
+ return ExtensionBundleType.NIFI_NAR;
+ case MINIFI_CPP:
+ return ExtensionBundleType.MINIFI_CPP;
+ default:
+ throw new IllegalArgumentException("Unknown bundle type: " + bundleEntityType);
+ }
+ }
+
+ // -- Map ExtensionBundle
+
+ public static ExtensionBundleEntity map(final ExtensionBundle bundle) {
+ final ExtensionBundleEntity entity = new ExtensionBundleEntity();
+ entity.setId(bundle.getIdentifier());
+ entity.setName(bundle.getName());
+ entity.setDescription(bundle.getDescription());
+ entity.setCreated(new Date(bundle.getCreatedTimestamp()));
+ entity.setModified(new Date(bundle.getModifiedTimestamp()));
+ entity.setType(BucketItemEntityType.EXTENSION_BUNDLE);
+ entity.setBucketId(bundle.getBucketIdentifier());
+
+ entity.setGroupId(bundle.getGroupId());
+ entity.setArtifactId(bundle.getArtifactId());
+ entity.setBundleType(map(bundle.getBundleType()));
+ return entity;
+ }
+
+ public static ExtensionBundle map(final BucketEntity bucketEntity, final ExtensionBundleEntity bundleEntity) {
+ final ExtensionBundle bundle = new ExtensionBundle();
+ bundle.setIdentifier(bundleEntity.getId());
+ bundle.setName(bundleEntity.getName());
+ bundle.setDescription(bundleEntity.getDescription());
+ bundle.setCreatedTimestamp(bundleEntity.getCreated().getTime());
+ bundle.setModifiedTimestamp(bundleEntity.getModified().getTime());
+ bundle.setBucketIdentifier(bundleEntity.getBucketId());
+
+ if (bucketEntity != null) {
+ bundle.setBucketName(bucketEntity.getName());
+ } else {
+ bundle.setBucketName(bundleEntity.getBucketName());
+ }
+
+ bundle.setGroupId(bundleEntity.getGroupId());
+ bundle.setArtifactId(bundleEntity.getArtifactId());
+ bundle.setBundleType(map(bundleEntity.getBundleType()));
+ bundle.setVersionCount(bundleEntity.getVersionCount());
+ return bundle;
+ }
+
+ // -- Map ExtensionBundleVersion
+
+ public static ExtensionBundleVersionEntity map(final ExtensionBundleVersionMetadata bundleVersionMetadata) {
+ final ExtensionBundleVersionEntity entity = new ExtensionBundleVersionEntity();
+ entity.setId(bundleVersionMetadata.getId());
+ entity.setExtensionBundleId(bundleVersionMetadata.getExtensionBundleId());
+ entity.setVersion(bundleVersionMetadata.getVersion());
+ entity.setCreated(new Date(bundleVersionMetadata.getTimestamp()));
+ entity.setCreatedBy(bundleVersionMetadata.getAuthor());
+ entity.setDescription(bundleVersionMetadata.getDescription());
+ entity.setSha256Hex(bundleVersionMetadata.getSha256());
+ entity.setSha256Supplied(bundleVersionMetadata.getSha256Supplied());
+ return entity;
+ }
+
+ public static ExtensionBundleVersionMetadata map(final BucketEntity bucketEntity, final ExtensionBundleVersionEntity bundleVersionEntity) {
+ final ExtensionBundleVersionMetadata bundleVersionMetadata = new ExtensionBundleVersionMetadata();
+ bundleVersionMetadata.setId(bundleVersionEntity.getId());
+ bundleVersionMetadata.setExtensionBundleId(bundleVersionEntity.getExtensionBundleId());
+ bundleVersionMetadata.setVersion(bundleVersionEntity.getVersion());
+ bundleVersionMetadata.setTimestamp(bundleVersionEntity.getCreated().getTime());
+ bundleVersionMetadata.setAuthor(bundleVersionEntity.getCreatedBy());
+ bundleVersionMetadata.setDescription(bundleVersionEntity.getDescription());
+ bundleVersionMetadata.setSha256(bundleVersionEntity.getSha256Hex());
+ bundleVersionMetadata.setSha256Supplied(bundleVersionEntity.getSha256Supplied());
+
+ if (bucketEntity != null) {
+ bundleVersionMetadata.setBucketId(bucketEntity.getId());
+ }
+
+ return bundleVersionMetadata;
+ }
+
+ // -- Map ExtensionBundleVersionDependency
+
+ public static ExtensionBundleVersionDependencyEntity map(final ExtensionBundleVersionDependency bundleVersionDependency) {
+ final ExtensionBundleVersionDependencyEntity entity = new ExtensionBundleVersionDependencyEntity();
+ entity.setGroupId(bundleVersionDependency.getGroupId());
+ entity.setArtifactId(bundleVersionDependency.getArtifactId());
+ entity.setVersion(bundleVersionDependency.getVersion());
+ return entity;
+ }
+
+ public static ExtensionBundleVersionDependency map(final ExtensionBundleVersionDependencyEntity dependencyEntity) {
+ final ExtensionBundleVersionDependency dependency = new ExtensionBundleVersionDependency();
+ dependency.setGroupId(dependencyEntity.getGroupId());
+ dependency.setArtifactId(dependencyEntity.getArtifactId());
+ dependency.setVersion(dependencyEntity.getVersion());
+ return dependency;
+ }
+
// --- Map keys
public static Key map(final KeyEntity keyEntity) {
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/ExtensionBundleMetadataExtractors.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/ExtensionBundleMetadataExtractors.java
new file mode 100644
index 0000000..2746c0c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/ExtensionBundleMetadataExtractors.java
@@ -0,0 +1,45 @@
+/*
+ * 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.service;
+
+import org.apache.nifi.registry.extension.BundleExtractor;
+import org.apache.nifi.registry.extension.ExtensionBundleType;
+import org.apache.nifi.registry.extension.minificpp.MiNiFiCppBundleExtractor;
+import org.apache.nifi.registry.extension.nar.NarBundleExtractor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+public class ExtensionBundleMetadataExtractors {
+
+ private Map<ExtensionBundleType, BundleExtractor> extractors;
+
+ @Bean
+ public synchronized Map<ExtensionBundleType, BundleExtractor> getExtractors() {
+ if (extractors == null) {
+ extractors = new HashMap<>();
+ extractors.put(ExtensionBundleType.NIFI_NAR, new NarBundleExtractor());
+ extractors.put(ExtensionBundleType.MINIFI_CPP, new MiNiFiCppBundleExtractor());
+ }
+
+ return extractors;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java
index ea0b214..1dc90d4 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/MetadataService.java
@@ -18,6 +18,11 @@
import org.apache.nifi.registry.db.entity.BucketEntity;
import org.apache.nifi.registry.db.entity.BucketItemEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionDependencyEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionEntity;
+import org.apache.nifi.registry.db.entity.ExtensionEntity;
+import org.apache.nifi.registry.db.entity.ExtensionEntityCategory;
import org.apache.nifi.registry.db.entity.FlowEntity;
import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
@@ -215,6 +220,234 @@
// --------------------------------------------------------------------------------------------
/**
+ * Creates the given extension bundle.
+ *
+ * @param extensionBundle the extension bundle to create
+ * @return the created extension bundle
+ */
+ ExtensionBundleEntity createExtensionBundle(ExtensionBundleEntity extensionBundle);
+
+ /**
+ * Retrieves the extension bundle with the given id.
+ *
+ * @param extensionBundleId the id of the extension bundle
+ * @return the extension bundle with the id, or null if one does not exist
+ */
+ ExtensionBundleEntity getExtensionBundle(String extensionBundleId);
+
+ /**
+ * Retrieves the extension bundle in the given bucket with the given group and artifact id.
+ *
+ * @return the extension bundle, or null if one does not exist
+ */
+ ExtensionBundleEntity getExtensionBundle(String bucketId, String groupId, String artifactId);
+
+ /**
+ * Retrieves all extension bundles in the buckets with the given bucket ids.
+ *
+ * @param bucketIds the bucket ids
+ * @return the list of all extension bundles in the given buckets
+ */
+ List<ExtensionBundleEntity> getExtensionBundles(Set<String> bucketIds);
+
+ /**
+ * Retrieves the extension bundles for the given bucket.
+ *
+ * @param bucketId the bucket id
+ * @return the list of extension bundles for the bucket
+ */
+ List<ExtensionBundleEntity> getExtensionBundlesByBucket(String bucketId);
+
+ /**
+ * Retrieves the extension bundles for the given bucket and group.
+ *
+ * @param bucketId the bucket id
+ * @param groupId the group id
+ * @return the list of extension bundles for the bucket and group
+ */
+ List<ExtensionBundleEntity> getExtensionBundlesByBucketAndGroup(String bucketId, String groupId);
+
+ /**
+ * Deletes the given extension bundle.
+ *
+ * @param extensionBundle the extension bundle to delete
+ */
+ void deleteExtensionBundle(ExtensionBundleEntity extensionBundle);
+
+ /**
+ * Deletes the extension bundle with the given id.
+ *
+ * @param extensionBundleId the id extension bundle to delete
+ */
+ void deleteExtensionBundle(String extensionBundleId);
+
+ // --------------------------------------------------------------------------------------------
+
+ /**
+ * Creates a version of an extension bundle.
+ *
+ * @param extensionBundleVersion the bundle version to create
+ * @return the created bundle version
+ */
+ ExtensionBundleVersionEntity createExtensionBundleVersion(ExtensionBundleVersionEntity extensionBundleVersion);
+
+ /**
+ * Retrieves the extension bundle version for the given bundle id and version.
+ *
+ * @param extensionBundleId the id of the extension bundle
+ * @param version the version of the extension bundle
+ * @return the extension bundle version, or null if does not exist
+ */
+ ExtensionBundleVersionEntity getExtensionBundleVersion(String extensionBundleId, String version);
+
+ /**
+ * Retrieves the extension bundle version by bucket, group, artifact, version.
+ *
+ * @param bucketId the bucket id
+ * @param groupId the group id
+ * @param artifactId the artifact id
+ * @param version the version
+ * @return the extension bundle version, or null if does not exist
+ */
+ ExtensionBundleVersionEntity getExtensionBundleVersion(String bucketId, String groupId, String artifactId, String version);
+
+ /**
+ * Retrieves the extension bundle versions for the given extension bundle id.
+ *
+ * @param extensionBundleId the extension bundle id
+ * @return the list of extension bundle versions
+ */
+ List<ExtensionBundleVersionEntity> getExtensionBundleVersions(String extensionBundleId);
+
+ /**
+ * Retrieves the extension bundle version with the given group id and artifact id in the given bucket.
+ *
+ * @param bucketId the bucket id
+ * @param groupId the group id
+ * @param artifactId the artifact id
+ * @return the list of extension bundles
+ */
+ List<ExtensionBundleVersionEntity> getExtensionBundleVersions(String bucketId, String groupId, String artifactId);
+
+ /**
+ * Retrieves the extension bundle versions with the given group id, artifact id, and version across all buckets.
+ *
+ * @param groupId the group id
+ * @param artifactId the artifact id
+ * @param version the versions
+ * @return all bundle versions for the group id, artifact id, and version
+ */
+ List<ExtensionBundleVersionEntity> getExtensionBundleVersionsGlobal(String groupId, String artifactId, String version);
+
+ /**
+ * Deletes the extension bundle version.
+ *
+ * @param extensionBundleVersion the extension bundle version to delete
+ */
+ void deleteExtensionBundleVersion(ExtensionBundleVersionEntity extensionBundleVersion);
+
+ /**
+ * Deletes the extension bundle version.
+ *
+ * @param extensionBundleVersionId the id of the extension bundle version
+ */
+ void deleteExtensionBundleVersion(String extensionBundleVersionId);
+
+ // --------------------------------------------------------------------------------------------
+
+ /**
+ * Creates the given extension bundle version dependency.
+ *
+ * @param dependencyEntity the dependency entity
+ * @return the created dependency
+ */
+ ExtensionBundleVersionDependencyEntity createDependency(ExtensionBundleVersionDependencyEntity dependencyEntity);
+
+ /**
+ * Retrieves the bundle dependencies for the given bundle version.
+ *
+ * @param extensionBundleVersionId the id of the extension bundle version
+ * @return the list of dependencies
+ */
+ List<ExtensionBundleVersionDependencyEntity> getDependenciesForBundleVersion(String extensionBundleVersionId);
+
+ // --------------------------------------------------------------------------------------------
+
+ /**
+ * Creates the given extension.
+ *
+ * @param extension the extension to create
+ * @return the created extension
+ */
+ ExtensionEntity createExtension(ExtensionEntity extension);
+
+ /**
+ * Retrieves the extension with the given id.
+ *
+ * @param id the id of the extension
+ * @return the extension with the id, or null if one does not exist
+ */
+ ExtensionEntity getExtensionById(String id);
+
+ /**
+ * Retrieves all extensions.
+ *
+ * @return the list of all extensions
+ */
+ List<ExtensionEntity> getAllExtensions();
+
+ /**
+ * Retrieves the extensions for the given extension bundle version.
+ *
+ * @param extensionBundleVersionId the id of the extension bundle version
+ * @return the extensions in the given bundle
+ */
+ List<ExtensionEntity> getExtensionsByBundleVersionId(String extensionBundleVersionId);
+
+ /**
+ * Retrieves the extensions for the bundle in the given bucket with the given group, artifact, and version.
+ *
+ * @param bucketId the bucket of the bundle
+ * @param groupId the group of the bundle
+ * @param artifactId the artifact id of the bundle
+ * @param version the version of the bundle
+ * @return the extensions for the bundle
+ */
+ List<ExtensionEntity> getExtensionsByBundleCoordinate(String bucketId, String groupId, String artifactId, String version);
+
+ /**
+ * Retrieves the extensions for the given category (i.e. processor, controller service, reporting task).
+ *
+ * @param category the category
+ * @return the extensions for the given category
+ */
+ List<ExtensionEntity> getExtensionsByCategory(ExtensionEntityCategory category);
+
+ /**
+ * Retrieves the extensions with the given tag.
+ *
+ * @param tag the tag
+ * @return the extensions with the given tag
+ */
+ List<ExtensionEntity> getExtensionsByTag(String tag);
+
+ /**
+ * Retrieves the set of all extension tags.
+ *
+ * @return the set of all extension tags
+ */
+ Set<String> getAllExtensionTags();
+
+ /**
+ * Deletes the given extension.
+ *
+ * @param extension the extension to delete
+ */
+ void deleteExtension(ExtensionEntity extension);
+
+ // --------------------------------------------------------------------------------------------
+
+ /**
* @return the set of field names for Buckets
*/
Set<String> getBucketFields();
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
index 23f1d14..091803f 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/RegistryService.java
@@ -23,11 +23,20 @@
import org.apache.nifi.registry.bucket.BucketItem;
import org.apache.nifi.registry.db.entity.BucketEntity;
import org.apache.nifi.registry.db.entity.BucketItemEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntity;
import org.apache.nifi.registry.db.entity.FlowEntity;
import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
import org.apache.nifi.registry.diff.ComponentDifferenceGroup;
import org.apache.nifi.registry.diff.VersionedFlowDifference;
import org.apache.nifi.registry.exception.ResourceNotFoundException;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleType;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
import org.apache.nifi.registry.flow.FlowPersistenceProvider;
import org.apache.nifi.registry.flow.FlowSnapshotContext;
import org.apache.nifi.registry.flow.VersionedComponent;
@@ -44,6 +53,8 @@
import org.apache.nifi.registry.flow.diff.StandardFlowComparator;
import org.apache.nifi.registry.provider.flow.StandardFlowSnapshotContext;
import org.apache.nifi.registry.serialization.Serializer;
+import org.apache.nifi.registry.service.extension.ExtensionBundleVersionCoordinate;
+import org.apache.nifi.registry.service.extension.ExtensionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -55,7 +66,9 @@
import javax.validation.Validator;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -85,6 +98,7 @@
private final MetadataService metadataService;
private final FlowPersistenceProvider flowPersistenceProvider;
private final Serializer<VersionedProcessGroup> processGroupSerializer;
+ private final ExtensionService extensionService;
private final Validator validator;
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
@@ -95,14 +109,17 @@
public RegistryService(final MetadataService metadataService,
final FlowPersistenceProvider flowPersistenceProvider,
final Serializer<VersionedProcessGroup> processGroupSerializer,
+ final ExtensionService extensionService,
final Validator validator) {
this.metadataService = metadataService;
this.flowPersistenceProvider = flowPersistenceProvider;
this.processGroupSerializer = processGroupSerializer;
+ this.extensionService = extensionService;
this.validator = validator;
Validate.notNull(this.metadataService);
Validate.notNull(this.flowPersistenceProvider);
Validate.notNull(this.processGroupSerializer);
+ Validate.notNull(this.extensionService);
Validate.notNull(this.validator);
}
@@ -159,6 +176,25 @@
}
}
+ public Bucket getBucketByName(final String bucketName) {
+ if (bucketName == null) {
+ throw new IllegalArgumentException("Bucket name cannot be null");
+ }
+
+ readLock.lock();
+ try {
+ final List<BucketEntity> buckets = metadataService.getBucketsByName(bucketName);
+ if (buckets.isEmpty()) {
+ LOGGER.warn("The specified bucket name [{}] does not exist.", bucketName);
+ throw new ResourceNotFoundException("The specified bucket name does not exist in this registry.");
+ }
+
+ return DataModelMapper.map(buckets.get(0));
+ } finally {
+ readLock.unlock();
+ }
+ }
+
public List<Bucket> getBuckets() {
readLock.lock();
try {
@@ -298,11 +334,13 @@
}
private void addBucketItem(final List<BucketItem> bucketItems, final BucketItemEntity itemEntity) {
+ // Currently we don't populate the bucket name for items so we pass in null in the map methods
if (itemEntity instanceof FlowEntity) {
final FlowEntity flowEntity = (FlowEntity) itemEntity;
-
- // Currently we don't populate the bucket name for items
bucketItems.add(DataModelMapper.map(null, flowEntity));
+ } else if (itemEntity instanceof ExtensionBundleEntity) {
+ final ExtensionBundleEntity bundleEntity = (ExtensionBundleEntity) itemEntity;
+ bucketItems.add(DataModelMapper.map(null, bundleEntity));
} else {
LOGGER.error("Unknown type of BucketItemEntity: " + itemEntity.getClass().getCanonicalName());
}
@@ -977,6 +1015,128 @@
return differenceGroups.values().stream().collect(Collectors.toSet());
}
+ // ---------------------- ExtensionBundle methods ---------------------------------------------
+
+ public ExtensionBundleVersion createExtensionBundleVersion(final String bucketIdentifier, final ExtensionBundleType bundleType,
+ final InputStream inputStream, final String clientSha256) throws IOException {
+ writeLock.lock();
+ try {
+ return extensionService.createExtensionBundleVersion(bucketIdentifier, bundleType, inputStream, clientSha256);
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ public List<ExtensionBundle> getExtensionBundles(Set<String> bucketIdentifiers) {
+ readLock.lock();
+ try {
+ return extensionService.getExtensionBundles(bucketIdentifiers);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ public List<ExtensionBundle> getExtensionBundlesByBucket(final String bucketIdentifier) {
+ readLock.lock();
+ try {
+ return extensionService.getExtensionBundlesByBucket(bucketIdentifier);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ public ExtensionBundle getExtensionBundle(final String extensionBundleId) {
+ readLock.lock();
+ try {
+ return extensionService.getExtensionBundle(extensionBundleId);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ public ExtensionBundle deleteExtensionBundle(final ExtensionBundle extensionBundle) {
+ writeLock.lock();
+ try {
+ return extensionService.deleteExtensionBundle(extensionBundle);
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ public SortedSet<ExtensionBundleVersionMetadata> getExtensionBundleVersions(final String extensionBundleIdentifier) {
+ readLock.lock();
+ try {
+ return extensionService.getExtensionBundleVersions(extensionBundleIdentifier);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ public ExtensionBundleVersion getExtensionBundleVersion(ExtensionBundleVersionCoordinate versionCoordinate) {
+ readLock.lock();
+ try {
+ return extensionService.getExtensionBundleVersion(versionCoordinate);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ public void writeExtensionBundleVersionContent(final ExtensionBundleVersion bundleVersion, final OutputStream out) {
+ readLock.lock();
+ try {
+ extensionService.writeExtensionBundleVersionContent(bundleVersion, out);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ public ExtensionBundleVersion deleteExtensionBundleVersion(final ExtensionBundleVersion bundleVersion) {
+ writeLock.lock();
+ try {
+ return extensionService.deleteExtensionBundleVersion(bundleVersion);
+ } finally {
+ writeLock.unlock();
+ }
+ }
+
+ // ---------------------- Extension Repository methods ---------------------------------------------
+
+ public SortedSet<ExtensionRepoBucket> getExtensionRepoBuckets(final Set<String> bucketIds) {
+ readLock.lock();
+ try {
+ return extensionService.getExtensionRepoBuckets(bucketIds);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ public SortedSet<ExtensionRepoGroup> getExtensionRepoGroups(final Bucket bucket) {
+ readLock.lock();
+ try {
+ return extensionService.getExtensionRepoGroups(bucket);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ public SortedSet<ExtensionRepoArtifact> getExtensionRepoArtifacts(final Bucket bucket, final String groupId) {
+ readLock.lock();
+ try {
+ return extensionService.getExtensionRepoArtifacts(bucket, groupId);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
+ public SortedSet<ExtensionRepoVersionSummary> getExtensionRepoVersions(final Bucket bucket, final String groupId, final String artifactId) {
+ readLock.lock();
+ try {
+ return extensionService.getExtensionRepoVersions(bucket, groupId, artifactId);
+ } finally {
+ readLock.unlock();
+ }
+ }
+
// ---------------------- Field methods ---------------------------------------------
public Set<String> getBucketFields() {
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionBundleCoordinate.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionBundleCoordinate.java
new file mode 100644
index 0000000..ce78ce0
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionBundleCoordinate.java
@@ -0,0 +1,57 @@
+/*
+ * 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.service.extension;
+
+import org.apache.commons.lang3.Validate;
+
+/**
+ * The unique coordinate for an extension bundle.
+ *
+ * This is an alternative to using the single uuid identifier for the bundle.
+ */
+public class ExtensionBundleCoordinate {
+
+ private final String bucketId;
+ private final String groupId;
+ private final String artifactId;
+
+ public ExtensionBundleCoordinate(final String bucketId, final String groupId, final String artifactId) {
+ this.bucketId = bucketId;
+ this.groupId = groupId;
+ this.artifactId = artifactId;
+ Validate.notBlank(this.bucketId, "Bucket id cannot be null or blank");
+ Validate.notBlank(this.groupId, "Group id cannot be null or blank");
+ Validate.notBlank(this.artifactId, "Artifact id cannot be null or blank");
+ }
+
+ public String getBucketId() {
+ return bucketId;
+ }
+
+ public String getGroupId() {
+ return groupId;
+ }
+
+ public String getArtifactId() {
+ return artifactId;
+ }
+
+ @Override
+ public String toString() {
+ return bucketId + ":" + groupId + ":" + artifactId;
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionBundleVersionCoordinate.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionBundleVersionCoordinate.java
new file mode 100644
index 0000000..38904e5
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionBundleVersionCoordinate.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.service.extension;
+
+import org.apache.commons.lang3.Validate;
+
+/**
+ * The unique coordinate for a version of an extension bundle.
+ */
+public class ExtensionBundleVersionCoordinate extends ExtensionBundleCoordinate {
+
+ private final String version;
+
+ public ExtensionBundleVersionCoordinate(final String bucketId, final String groupId, final String artifactId, final String version) {
+ super(bucketId, groupId, artifactId);
+ this.version = version;
+ Validate.notBlank(this.version, "Version cannot be null or blank");
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + ":" + version;
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionService.java
new file mode 100644
index 0000000..3ab8a07
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/ExtensionService.java
@@ -0,0 +1,130 @@
+/*
+ * 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.service.extension;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleType;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+
+public interface ExtensionService {
+
+ /**
+ * Creates a version of an extension bundle.
+ *
+ * The InputStream is expected to contain the binary contents of a bundle in the format specified by bundleType.
+ *
+ * The metadata will be extracted from the bundle and used to determine if this is a new version of an existing bundle,
+ * or it will create a new bundle and this as the first version if one doesn't already exist.
+ *
+ * @param bucketIdentifier the bucket id
+ * @param bundleType the type of bundle
+ * @param inputStream the binary content of the bundle
+ * @param clientSha256 the SHA-256 hex supplied by the client
+ * @return the ExtensionBundleVersion representing all of the information about the bundle
+ * @throws IOException if an error occurs processing the InputStream
+ */
+ ExtensionBundleVersion createExtensionBundleVersion(String bucketIdentifier, ExtensionBundleType bundleType,
+ InputStream inputStream, String clientSha256) throws IOException;
+
+ /**
+ * Retrieves the extension bundles in the given buckets.
+ *
+ * @param bucketIdentifiers the bucket identifiers
+ * @return the bundles in the given buckets
+ */
+ List<ExtensionBundle> getExtensionBundles(Set<String> bucketIdentifiers);
+
+ /**
+ * Retrieves the extension bundles in the given bucket.
+ *
+ * @param bucketIdentifier the bucket identifier
+ * @return the bundles in the given bucket
+ */
+ List<ExtensionBundle> getExtensionBundlesByBucket(String bucketIdentifier);
+
+ /**
+ * Retrieve the extension bundle with the given id.
+ *
+ * @param extensionBundleIdentifier the extension bundle id
+ * @return the bundle
+ */
+ ExtensionBundle getExtensionBundle(String extensionBundleIdentifier);
+
+ /**
+ * Deletes the given extension bundle and all it's versions.
+ *
+ * @param extensionBundle the extension bundle to delete
+ * @return the deleted bundle
+ */
+ ExtensionBundle deleteExtensionBundle(ExtensionBundle extensionBundle);
+
+ /**
+ * Retrieves the versions of the given extension bundle.
+ *
+ * @param extensionBundleIdentifier the extension bundle id
+ * @return the sorted set of versions for the given bundle
+ */
+ SortedSet<ExtensionBundleVersionMetadata> getExtensionBundleVersions(String extensionBundleIdentifier);
+
+ /**
+ * Retrieves the full ExtensionBundleVersion object, including version metadata, bundle metadata, and bucket metadata.
+ *
+ * @param versionCoordinate the coordinate of the version
+ * @return the extension bundle version
+ */
+ ExtensionBundleVersion getExtensionBundleVersion(ExtensionBundleVersionCoordinate versionCoordinate);
+
+ /**
+ * Writes the binary content of the extension bundle version to the given OutputStream.
+ *
+ * @param extensionBundleVersion the version to write the content for
+ * @param out the output stream to write to
+ */
+ void writeExtensionBundleVersionContent(ExtensionBundleVersion extensionBundleVersion, OutputStream out);
+
+ /**
+ * Deletes the given version of the extension bundle.
+ *
+ * @param bundleVersion the version to delete
+ * @return the deleted extension bundle version
+ */
+ ExtensionBundleVersion deleteExtensionBundleVersion(ExtensionBundleVersion bundleVersion);
+
+ // ----- Extension Repo Methods -----
+
+ SortedSet<ExtensionRepoBucket> getExtensionRepoBuckets(Set<String> bucketIds);
+
+ SortedSet<ExtensionRepoGroup> getExtensionRepoGroups(Bucket bucket);
+
+ SortedSet<ExtensionRepoArtifact> getExtensionRepoArtifacts(Bucket bucket, String groupId);
+
+ SortedSet<ExtensionRepoVersionSummary> getExtensionRepoVersions(Bucket bucket, String groupId, String artifactId);
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java
new file mode 100644
index 0000000..e737b75
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/service/extension/StandardExtensionService.java
@@ -0,0 +1,620 @@
+/*
+ * 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.service.extension;
+
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.db.entity.BucketEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntityType;
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionDependencyEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionEntity;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
+import org.apache.nifi.registry.extension.BundleCoordinate;
+import org.apache.nifi.registry.extension.BundleDetails;
+import org.apache.nifi.registry.extension.BundleExtractor;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleContext;
+import org.apache.nifi.registry.extension.ExtensionBundlePersistenceProvider;
+import org.apache.nifi.registry.extension.ExtensionBundleType;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionDependency;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.provider.extension.StandardExtensionBundleContext;
+import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
+import org.apache.nifi.registry.service.DataModelMapper;
+import org.apache.nifi.registry.service.MetadataService;
+import org.apache.nifi.registry.util.FileUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Validator;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+@Service
+public class StandardExtensionService implements ExtensionService {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(StandardExtensionService.class);
+
+ private final MetadataService metadataService;
+ private final Map<ExtensionBundleType, BundleExtractor> extractors;
+ private final ExtensionBundlePersistenceProvider bundlePersistenceProvider;
+ private final Validator validator;
+ private final File extensionsWorkingDir;
+
+ @Autowired
+ public StandardExtensionService(final MetadataService metadataService,
+ final Map<ExtensionBundleType, BundleExtractor> extractors,
+ final ExtensionBundlePersistenceProvider bundlePersistenceProvider,
+ final Validator validator,
+ final NiFiRegistryProperties properties) {
+ this.metadataService = metadataService;
+ this.extractors = extractors;
+ this.bundlePersistenceProvider = bundlePersistenceProvider;
+ this.validator = validator;
+ this.extensionsWorkingDir = properties.getExtensionsWorkingDirectory();
+ Validate.notNull(this.metadataService);
+ Validate.notNull(this.extractors);
+ Validate.notNull(this.bundlePersistenceProvider);
+ Validate.notNull(this.validator);
+ Validate.notNull(this.extensionsWorkingDir);
+ }
+
+ private <T> void validate(T t, String invalidMessage) {
+ final Set<ConstraintViolation<T>> violations = validator.validate(t);
+ if (violations.size() > 0) {
+ throw new ConstraintViolationException(invalidMessage, violations);
+ }
+ }
+
+ @Override
+ public ExtensionBundleVersion createExtensionBundleVersion(final String bucketIdentifier, final ExtensionBundleType bundleType,
+ final InputStream inputStream, final String clientSha256) throws IOException {
+ if (StringUtils.isBlank(bucketIdentifier)) {
+ throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
+ }
+
+ if (bundleType == null) {
+ throw new IllegalArgumentException("Bundle type cannot be null");
+ }
+
+ if (inputStream == null) {
+ throw new IllegalArgumentException("Extension bundle input stream cannot be null");
+ }
+
+ if (!extractors.containsKey(bundleType)) {
+ throw new IllegalArgumentException("No metadata extractor is registered for bundle-type: " + bundleType);
+ }
+
+ // ensure the bucket exists
+ final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+ if (existingBucket == null) {
+ LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+ throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+ }
+
+ // ensure the extensions directory exists and we can read and write to it
+ FileUtils.ensureDirectoryExistAndCanReadAndWrite(extensionsWorkingDir);
+
+ final String extensionWorkingFilename = UUID.randomUUID().toString();
+ final File extensionWorkingFile = new File(extensionsWorkingDir, extensionWorkingFilename);
+ LOGGER.debug("Writing bundle contents to working directory at {}", new Object[]{extensionWorkingFile.getAbsolutePath()});
+
+ try {
+ // write the contents of the input stream to a temporary file in the extensions working directory
+ final MessageDigest sha256Digest = DigestUtils.getSha256Digest();
+ try (final DigestInputStream digestInputStream = new DigestInputStream(inputStream, sha256Digest);
+ final OutputStream out = new FileOutputStream(extensionWorkingFile)) {
+ IOUtils.copy(digestInputStream, out);
+ }
+
+ // get the hex of the SHA-256 computed by the server and compare to the client provided SHA-256, if one was provided
+ final String sha256Hex = Hex.encodeHexString(sha256Digest.digest());
+ final boolean sha256Supplied = !StringUtils.isBlank(clientSha256);
+ if (sha256Supplied && !sha256Hex.equalsIgnoreCase(clientSha256)) {
+ LOGGER.error("Client provided SHA-256 of '{}', but server calculated '{}'", new Object[]{clientSha256, sha256Hex});
+ throw new IllegalStateException("The SHA-256 of the received extension bundle does not match the SHA-256 provided by the client");
+ }
+
+ // extract the details of the bundle from the temp file in the working directory
+ final BundleDetails bundleDetails;
+ try (final InputStream in = new FileInputStream(extensionWorkingFile)) {
+ final BundleExtractor extractor = extractors.get(bundleType);
+ bundleDetails = extractor.extract(in);
+ }
+
+ final BundleCoordinate bundleCoordinate = bundleDetails.getBundleCoordinate();
+ final Set<BundleCoordinate> dependencyCoordinates = bundleDetails.getDependencyBundleCoordinates();
+
+ final String groupId = bundleCoordinate.getGroupId();
+ final String artifactId = bundleCoordinate.getArtifactId();
+ final String version = bundleCoordinate.getVersion();
+ LOGGER.debug("Extracted bundle details - '{}' - '{}' - '{}'", new Object[]{groupId, artifactId, version});
+
+ // a bundle with the same group, artifact, and version can exist in multiple buckets, but only if it contains the same binary content,
+ // we can determine that by comparing the SHA-256 digest of the incoming bundle against existing bundles with the same group, artifact, version
+ final List<ExtensionBundleVersionEntity> allExistingVersions = metadataService.getExtensionBundleVersionsGlobal(groupId, artifactId, version);
+ for (final ExtensionBundleVersionEntity existingVersionEntity : allExistingVersions) {
+ if (!existingVersionEntity.getSha256Hex().equals(sha256Hex)) {
+ throw new IllegalStateException("Found existing extension bundle with same group, artifact, and version, but different SHA-256 check-sum");
+ }
+ }
+
+ // get the existing extension bundle entity, or create a new one if one does not exist in the bucket with the group + artifact
+ final long currentTime = System.currentTimeMillis();
+ final ExtensionBundleEntity extensionBundle = getOrCreateExtensionBundle(bucketIdentifier, groupId, artifactId, bundleType, currentTime);
+
+ // ensure there isn't already a version of the bundle with the same version
+ final ExtensionBundleVersionEntity existingVersion = metadataService.getExtensionBundleVersion(bucketIdentifier, groupId, artifactId, version);
+ if (existingVersion != null) {
+ LOGGER.warn("The specified version [{}] already exists for extension bundle [{}].", new Object[]{version, extensionBundle.getId()});
+ throw new IllegalStateException("The specified version already exists for the given extension bundle");
+ }
+
+ // create the version metadata instance and validate it has all the required fields
+ final String userIdentity = NiFiUserUtils.getNiFiUserIdentity();
+ final ExtensionBundleVersionMetadata versionMetadata = new ExtensionBundleVersionMetadata();
+ versionMetadata.setId(UUID.randomUUID().toString());
+ versionMetadata.setExtensionBundleId(extensionBundle.getId());
+ versionMetadata.setBucketId(bucketIdentifier);
+ versionMetadata.setVersion(version);
+ versionMetadata.setTimestamp(currentTime);
+ versionMetadata.setAuthor(userIdentity);
+ versionMetadata.setSha256(sha256Hex);
+ versionMetadata.setSha256Supplied(sha256Supplied);
+
+ validate(versionMetadata, "Cannot create extension bundle version");
+
+ // create the version dependency instances and validate they have the required fields
+ final Set<ExtensionBundleVersionDependency> versionDependencies = new HashSet<>();
+ for (final BundleCoordinate dependencyCoordinate : dependencyCoordinates) {
+ final ExtensionBundleVersionDependency versionDependency = new ExtensionBundleVersionDependency();
+ versionDependency.setGroupId(dependencyCoordinate.getGroupId());
+ versionDependency.setArtifactId(dependencyCoordinate.getArtifactId());
+ versionDependency.setVersion(dependencyCoordinate.getVersion());
+
+ validate(versionDependency, "Cannot create extension bundle version dependency");
+ versionDependencies.add(versionDependency);
+ }
+
+ // create the bundle version in the metadata db
+ final ExtensionBundleVersionEntity versionEntity = DataModelMapper.map(versionMetadata);
+ metadataService.createExtensionBundleVersion(versionEntity);
+
+ // create the bundle version dependencies in the metadata db
+ for (final ExtensionBundleVersionDependency versionDependency : versionDependencies) {
+ final ExtensionBundleVersionDependencyEntity versionDependencyEntity = DataModelMapper.map(versionDependency);
+ versionDependencyEntity.setId(UUID.randomUUID().toString());
+ versionDependencyEntity.setExtensionBundleVersionId(versionEntity.getId());
+ metadataService.createDependency(versionDependencyEntity);
+ }
+
+ // persist the content of the bundle to the persistence provider
+ final ExtensionBundleContext context = new StandardExtensionBundleContext.Builder()
+ .bundleType(getProviderBundleType(bundleType))
+ .bucketId(existingBucket.getId())
+ .bucketName(existingBucket.getName())
+ .bundleId(extensionBundle.getId())
+ .bundleGroupId(extensionBundle.getGroupId())
+ .bundleArtifactId(extensionBundle.getArtifactId())
+ .bundleVersion(versionMetadata.getVersion())
+ .author(versionMetadata.getAuthor())
+ .timestamp(versionMetadata.getTimestamp())
+ .build();
+
+ try (final InputStream in = new FileInputStream(extensionWorkingFile);
+ final InputStream bufIn = new BufferedInputStream(in)) {
+ bundlePersistenceProvider.saveBundleVersion(context, bufIn);
+ LOGGER.debug("Bundle saved to persistence provider - '{}' - '{}' - '{}'",
+ new Object[]{groupId, artifactId, version});
+ }
+
+ // get the updated extension bundle so it contains the correct version count
+ final ExtensionBundleEntity updatedBundle = metadataService.getExtensionBundle(bucketIdentifier, groupId, artifactId);
+
+ // create the full ExtensionBundleVersion instance to return
+ final ExtensionBundleVersion extensionBundleVersion = new ExtensionBundleVersion();
+ extensionBundleVersion.setVersionMetadata(versionMetadata);
+ extensionBundleVersion.setExtensionBundle(DataModelMapper.map(existingBucket, updatedBundle));
+ extensionBundleVersion.setBucket(DataModelMapper.map(existingBucket));
+ extensionBundleVersion.setDependencies(versionDependencies);
+ return extensionBundleVersion;
+
+ } finally {
+ if (extensionWorkingFile.exists()) {
+ try {
+ extensionWorkingFile.delete();
+ } catch (Exception e) {
+ LOGGER.warn("Error removing temporary extension bundle file at {}",
+ new Object[]{extensionWorkingFile.getAbsolutePath()});
+ }
+ }
+ }
+ }
+
+ private ExtensionBundleEntity getOrCreateExtensionBundle(final String bucketId, final String groupId,
+ final String artifactId, final ExtensionBundleType bundleType,
+ final long currentTime) {
+ ExtensionBundleEntity existingBundleEntity = metadataService.getExtensionBundle(bucketId, groupId, artifactId);
+ if (existingBundleEntity == null) {
+ final ExtensionBundle bundle = new ExtensionBundle();
+ bundle.setIdentifier(UUID.randomUUID().toString());
+ bundle.setBucketIdentifier(bucketId);
+ bundle.setName(groupId + ":" + artifactId);
+ bundle.setGroupId(groupId);
+ bundle.setArtifactId(artifactId);
+ bundle.setBundleType(bundleType);
+ bundle.setCreatedTimestamp(currentTime);
+ bundle.setModifiedTimestamp(currentTime);
+
+ validate(bundle, "Cannot create extension bundle");
+ existingBundleEntity = metadataService.createExtensionBundle(DataModelMapper.map(bundle));
+ } else {
+ final ExtensionBundleEntityType bundleEntityType = DataModelMapper.map(bundleType);
+ if (bundleEntityType != existingBundleEntity.getBundleType()) {
+ throw new IllegalStateException("A bundle already exists with the same group id and artifact id, but a different bundle type");
+ }
+ }
+
+ return existingBundleEntity;
+ }
+
+ private ExtensionBundleContext.BundleType getProviderBundleType(final ExtensionBundleType bundleType) {
+ switch (bundleType) {
+ case NIFI_NAR:
+ return ExtensionBundleContext.BundleType.NIFI_NAR;
+ case MINIFI_CPP:
+ return ExtensionBundleContext.BundleType.MINIFI_CPP;
+ default:
+ throw new IllegalArgumentException("Unknown bundle type: " + bundleType.toString());
+ }
+ }
+
+ @Override
+ public List<ExtensionBundle> getExtensionBundles(Set<String> bucketIdentifiers) {
+ if (bucketIdentifiers == null) {
+ throw new IllegalArgumentException("Bucket identifiers cannot be null");
+ }
+
+ final List<ExtensionBundleEntity> bundleEntities = metadataService.getExtensionBundles(bucketIdentifiers);
+ return bundleEntities.stream().map(b -> DataModelMapper.map(null, b)).collect(Collectors.toList());
+ }
+
+ @Override
+ public List<ExtensionBundle> getExtensionBundlesByBucket(final String bucketIdentifier) {
+ if (StringUtils.isBlank(bucketIdentifier)) {
+ throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
+ }
+
+ // ensure the bucket exists
+ final BucketEntity existingBucket = metadataService.getBucketById(bucketIdentifier);
+ if (existingBucket == null) {
+ LOGGER.warn("The specified bucket id [{}] does not exist.", bucketIdentifier);
+ throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+ }
+
+ final List<ExtensionBundleEntity> bundleEntities = metadataService.getExtensionBundlesByBucket(bucketIdentifier);
+ return bundleEntities.stream().map(b -> DataModelMapper.map(existingBucket, b)).collect(Collectors.toList());
+ }
+
+ @Override
+ public ExtensionBundle getExtensionBundle(final String extensionBundleIdentifier) {
+ if (StringUtils.isBlank(extensionBundleIdentifier)) {
+ throw new IllegalArgumentException("Extension bundle identifier cannot be null or blank");
+ }
+
+ final ExtensionBundleEntity existingBundle = metadataService.getExtensionBundle(extensionBundleIdentifier);
+ if (existingBundle == null) {
+ LOGGER.warn("The specified extension bundle id [{}] does not exist.", extensionBundleIdentifier);
+ throw new ResourceNotFoundException("The specified extension bundle ID does not exist.");
+ }
+
+ final BucketEntity existingBucket = metadataService.getBucketById(existingBundle.getBucketId());
+ return DataModelMapper.map(existingBucket, existingBundle);
+ }
+
+ @Override
+ public ExtensionBundle deleteExtensionBundle(final ExtensionBundle extensionBundle) {
+ if (extensionBundle == null) {
+ throw new IllegalArgumentException("Extension bundle cannot be null");
+ }
+
+ // delete the bundle from the database
+ metadataService.deleteExtensionBundle(extensionBundle.getIdentifier());
+
+ // delete all content associated with the bundle in the persistence provider
+ bundlePersistenceProvider.deleteAllBundleVersions(
+ extensionBundle.getBucketIdentifier(),
+ extensionBundle.getBucketName(),
+ extensionBundle.getGroupId(),
+ extensionBundle.getArtifactId());
+
+ return extensionBundle;
+ }
+
+ @Override
+ public SortedSet<ExtensionBundleVersionMetadata> getExtensionBundleVersions(final String extensionBundleIdentifier) {
+ if (StringUtils.isBlank(extensionBundleIdentifier)) {
+ throw new IllegalArgumentException("Extension bundle identifier cannot be null or blank");
+ }
+
+ // ensure the bundle exists
+ final ExtensionBundleEntity existingBundle = metadataService.getExtensionBundle(extensionBundleIdentifier);
+ if (existingBundle == null) {
+ LOGGER.warn("The specified extension bundle id [{}] does not exist.", extensionBundleIdentifier);
+ throw new ResourceNotFoundException("The specified extension bundle ID does not exist in this bucket.");
+ }
+
+ return getExtensionBundleVersionsSet(existingBundle);
+ }
+
+ private SortedSet<ExtensionBundleVersionMetadata> getExtensionBundleVersionsSet(ExtensionBundleEntity existingBundle) {
+ final SortedSet<ExtensionBundleVersionMetadata> sortedVersions = new TreeSet<>(Collections.reverseOrder());
+
+ final List<ExtensionBundleVersionEntity> existingVersions = metadataService.getExtensionBundleVersions(existingBundle.getId());
+ if (existingVersions != null) {
+ final BucketEntity existingBucket = metadataService.getBucketById(existingBundle.getBucketId());
+ existingVersions.stream().forEach(s -> sortedVersions.add(DataModelMapper.map(existingBucket, s)));
+ }
+
+ return sortedVersions;
+ }
+
+ @Override
+ public ExtensionBundleVersion getExtensionBundleVersion(ExtensionBundleVersionCoordinate versionCoordinate) {
+ if (versionCoordinate == null) {
+ throw new IllegalArgumentException("Extension bundle version coordinate cannot be null");
+ }
+
+ // ensure the bucket exists
+ final BucketEntity existingBucket = metadataService.getBucketById(versionCoordinate.getBucketId());
+ if (existingBucket == null) {
+ LOGGER.warn("The specified bucket id [{}] does not exist.", versionCoordinate.getBucketId());
+ throw new ResourceNotFoundException("The specified bucket ID does not exist in this registry.");
+ }
+
+ // ensure the bundle exists
+ final ExtensionBundleEntity existingBundle = metadataService.getExtensionBundle(
+ versionCoordinate.getBucketId(),
+ versionCoordinate.getGroupId(),
+ versionCoordinate.getArtifactId());
+
+ if (existingBundle == null) {
+ LOGGER.warn("The specified extension bundle [{}] does not exist.", versionCoordinate.toString());
+ throw new ResourceNotFoundException("The specified extension bundle does not exist in this bucket.");
+ }
+
+ //ensure the version of the bundle exists
+ final ExtensionBundleVersionEntity existingVersion = metadataService.getExtensionBundleVersion(
+ versionCoordinate.getBucketId(),
+ versionCoordinate.getGroupId(),
+ versionCoordinate.getArtifactId(),
+ versionCoordinate.getVersion());
+
+ if (existingVersion == null) {
+ LOGGER.warn("The specified extension bundle version [{}] does not exist.", versionCoordinate.toString());
+ throw new ResourceNotFoundException("The specified extension bundle version does not exist in this bucket.");
+ }
+
+ // get the dependencies for the bundle version
+ final List<ExtensionBundleVersionDependencyEntity> existingVersionDependencies = metadataService
+ .getDependenciesForBundleVersion(existingVersion.getId());
+
+ // convert the dependency db entities
+ final Set<ExtensionBundleVersionDependency> dependencies = existingVersionDependencies.stream()
+ .map(d -> DataModelMapper.map(d))
+ .collect(Collectors.toSet());
+
+ // create the full ExtensionBundleVersion instance to return
+ final ExtensionBundleVersion extensionBundleVersion = new ExtensionBundleVersion();
+ extensionBundleVersion.setVersionMetadata(DataModelMapper.map(existingBucket, existingVersion));
+ extensionBundleVersion.setExtensionBundle(DataModelMapper.map(existingBucket, existingBundle));
+ extensionBundleVersion.setBucket(DataModelMapper.map(existingBucket));
+ extensionBundleVersion.setDependencies(dependencies);
+ return extensionBundleVersion;
+ }
+
+ @Override
+ public void writeExtensionBundleVersionContent(final ExtensionBundleVersion bundleVersion, final OutputStream out) {
+ // get the content from the persistence provider and write it to the output stream
+ final ExtensionBundleContext context = getExtensionBundleContext(bundleVersion);
+ bundlePersistenceProvider.getBundleVersion(context, out);
+ }
+
+ @Override
+ public ExtensionBundleVersion deleteExtensionBundleVersion(final ExtensionBundleVersion bundleVersion) {
+ if (bundleVersion == null) {
+ throw new IllegalArgumentException("Extension bundle version cannot be null");
+ }
+
+ // delete from the metadata db
+ final String extensionBundleVersionId = bundleVersion.getVersionMetadata().getId();
+ metadataService.deleteExtensionBundleVersion(extensionBundleVersionId);
+
+ // delete content associated with the bundle version in the persistence provider
+ final ExtensionBundleContext context = new StandardExtensionBundleContext.Builder()
+ .bundleType(getProviderBundleType(bundleVersion.getExtensionBundle().getBundleType()))
+ .bucketId(bundleVersion.getBucket().getIdentifier())
+ .bucketName(bundleVersion.getBucket().getName())
+ .bundleId(bundleVersion.getExtensionBundle().getIdentifier())
+ .bundleGroupId(bundleVersion.getExtensionBundle().getGroupId())
+ .bundleArtifactId(bundleVersion.getExtensionBundle().getArtifactId())
+ .bundleVersion(bundleVersion.getVersionMetadata().getVersion())
+ .author(bundleVersion.getVersionMetadata().getAuthor())
+ .timestamp(bundleVersion.getVersionMetadata().getTimestamp())
+ .build();
+
+ bundlePersistenceProvider.deleteBundleVersion(context);
+
+ return bundleVersion;
+ }
+
+ // ------ Extension Repository Methods -------
+
+ @Override
+ public SortedSet<ExtensionRepoBucket> getExtensionRepoBuckets(final Set<String> bucketIds) {
+ if (bucketIds == null) {
+ throw new IllegalArgumentException("Bucket ids cannot be null");
+ }
+
+ if (bucketIds.isEmpty()) {
+ return new TreeSet<>();
+ }
+
+ final SortedSet<ExtensionRepoBucket> repoBuckets = new TreeSet<>();
+
+ final List<BucketEntity> buckets = metadataService.getBuckets(bucketIds);
+ buckets.forEach(b -> {
+ final ExtensionRepoBucket repoBucket = new ExtensionRepoBucket();
+ repoBucket.setBucketName(b.getName());
+ repoBuckets.add(repoBucket);
+ });
+
+ return repoBuckets;
+ }
+
+ @Override
+ public SortedSet<ExtensionRepoGroup> getExtensionRepoGroups(final Bucket bucket) {
+ if (bucket == null) {
+ throw new IllegalArgumentException("Bucket cannot be null");
+ }
+
+ final SortedSet<ExtensionRepoGroup> repoGroups = new TreeSet<>();
+
+ final List<ExtensionBundleEntity> bundleEntities = metadataService.getExtensionBundlesByBucket(bucket.getIdentifier());
+ bundleEntities.forEach(b -> {
+ final ExtensionRepoGroup repoGroup = new ExtensionRepoGroup();
+ repoGroup.setBucketName(bucket.getName());
+ repoGroup.setGroupId(b.getGroupId());
+ repoGroups.add(repoGroup);
+ });
+
+ return repoGroups;
+ }
+
+ @Override
+ public SortedSet<ExtensionRepoArtifact> getExtensionRepoArtifacts(final Bucket bucket, final String groupId) {
+ if (bucket == null) {
+ throw new IllegalArgumentException("Bucket cannot be null");
+ }
+
+ if (StringUtils.isBlank(groupId)) {
+ throw new IllegalArgumentException("Group id cannot be null or blank");
+ }
+
+ final SortedSet<ExtensionRepoArtifact> repoArtifacts = new TreeSet<>();
+
+ final List<ExtensionBundleEntity> bundleEntities = metadataService.getExtensionBundlesByBucketAndGroup(bucket.getIdentifier(), groupId);
+ bundleEntities.forEach(b -> {
+ final ExtensionRepoArtifact repoArtifact = new ExtensionRepoArtifact();
+ repoArtifact.setBucketName(bucket.getName());
+ repoArtifact.setGroupId(b.getGroupId());
+ repoArtifact.setArtifactId(b.getArtifactId());
+ repoArtifacts.add(repoArtifact);
+ });
+
+ return repoArtifacts;
+ }
+
+ @Override
+ public SortedSet<ExtensionRepoVersionSummary> getExtensionRepoVersions(final Bucket bucket, final String groupId, final String artifactId) {
+ if (bucket == null) {
+ throw new IllegalArgumentException("Bucket cannot be null");
+ }
+
+ if (StringUtils.isBlank(groupId)) {
+ throw new IllegalArgumentException("Group id cannot be null or blank");
+ }
+
+ if (StringUtils.isBlank(artifactId)) {
+ throw new IllegalArgumentException("Artifact id cannot be null or blank");
+ }
+
+ final SortedSet<ExtensionRepoVersionSummary> repoVersions = new TreeSet<>();
+
+ final List<ExtensionBundleVersionEntity> versionEntities = metadataService.getExtensionBundleVersions(bucket.getIdentifier(), groupId, artifactId);
+ if (!versionEntities.isEmpty()) {
+ final ExtensionBundleEntity bundleEntity = metadataService.getExtensionBundle(bucket.getIdentifier(), groupId, artifactId);
+ if (bundleEntity == null) {
+ // should never happen if the list of versions is not empty, but just in case
+ throw new ResourceNotFoundException("The specified extension bundle does not exist in this bucket");
+ }
+
+ versionEntities.forEach(v -> {
+ final ExtensionRepoVersionSummary repoVersion = new ExtensionRepoVersionSummary();
+ repoVersion.setBucketName(bucket.getName());
+ repoVersion.setGroupId(bundleEntity.getGroupId());
+ repoVersion.setArtifactId(bundleEntity.getArtifactId());
+ repoVersion.setVersion(v.getVersion());
+ repoVersions.add(repoVersion);
+ });
+ }
+
+ return repoVersions;
+ }
+
+ // ------ Helper Methods -------
+
+ private ExtensionBundleContext getExtensionBundleContext(final ExtensionBundleVersion bundleVersion) {
+ return getExtensionBundleContext(bundleVersion.getBucket(), bundleVersion.getExtensionBundle(), bundleVersion.getVersionMetadata());
+ }
+
+ private ExtensionBundleContext getExtensionBundleContext(final Bucket bucket, final ExtensionBundle bundle,
+ final ExtensionBundleVersionMetadata bundleVersionMetadata) {
+ return new StandardExtensionBundleContext.Builder()
+ .bundleType(getProviderBundleType(bundle.getBundleType()))
+ .bucketId(bucket.getIdentifier())
+ .bucketName(bucket.getName())
+ .bundleId(bundle.getIdentifier())
+ .bundleGroupId(bundle.getGroupId())
+ .bundleArtifactId(bundle.getArtifactId())
+ .bundleVersion(bundleVersionMetadata.getVersion())
+ .author(bundleVersionMetadata.getAuthor())
+ .timestamp(bundleVersionMetadata.getTimestamp())
+ .build();
+ }
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.extension.ExtensionBundlePersistenceProvider b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.extension.ExtensionBundlePersistenceProvider
new file mode 100644
index 0000000..00dcbb8
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/META-INF/services/org.apache.nifi.registry.extension.ExtensionBundlePersistenceProvider
@@ -0,0 +1,15 @@
+# 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.
+org.apache.nifi.registry.provider.extension.FileSystemExtensionBundlePersistenceProvider
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V3__AddExtensions.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V3__AddExtensions.sql
new file mode 100644
index 0000000..da66cd1
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V3__AddExtensions.sql
@@ -0,0 +1,71 @@
+-- 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.
+
+CREATE TABLE EXTENSION_BUNDLE (
+ ID VARCHAR(50) NOT NULL,
+ BUCKET_ID VARCHAR(50) NOT NULL,
+ BUNDLE_TYPE VARCHAR(200) NOT NULL,
+ GROUP_ID VARCHAR(500) NOT NULL,
+ ARTIFACT_ID VARCHAR(500) NOT NULL,
+ CONSTRAINT PK__EXTENSION_BUNDLE_ID PRIMARY KEY (ID),
+ CONSTRAINT FK__EXTENSION_BUNDLE_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) ON DELETE CASCADE,
+ CONSTRAINT FK__EXTENSION_BUNDLE_BUCKET_ID FOREIGN KEY(BUCKET_ID) REFERENCES BUCKET(ID) ON DELETE CASCADE,
+ CONSTRAINT UNIQUE__EXTENSION_BUNDLE_BUCKET_GROUP_ARTIFACT UNIQUE (BUCKET_ID, GROUP_ID, ARTIFACT_ID)
+);
+
+CREATE TABLE EXTENSION_BUNDLE_VERSION (
+ ID VARCHAR(50) NOT NULL,
+ EXTENSION_BUNDLE_ID VARCHAR(50) NOT NULL,
+ VERSION VARCHAR(100) NOT NULL,
+ CREATED TIMESTAMP NOT NULL,
+ CREATED_BY VARCHAR(4096) NOT NULL,
+ DESCRIPTION TEXT,
+ SHA_256_HEX VARCHAR(512) NOT NULL,
+ SHA_256_SUPPLIED INT NOT NULL,
+ CONSTRAINT PK__EXTENSION_BUNDLE_VERSION_ID PRIMARY KEY (ID),
+ CONSTRAINT FK__EXTENSION_BUNDLE_VERSION_EXTENSION_BUNDLE_ID FOREIGN KEY (EXTENSION_BUNDLE_ID) REFERENCES EXTENSION_BUNDLE(ID) ON DELETE CASCADE,
+ CONSTRAINT UNIQUE__EXTENSION_BUNDLE_VERSION_EXTENSION_BUNDLE_ID_VERSION UNIQUE (EXTENSION_BUNDLE_ID, VERSION)
+);
+
+CREATE TABLE EXTENSION_BUNDLE_VERSION_DEPENDENCY (
+ ID VARCHAR(50) NOT NULL,
+ EXTENSION_BUNDLE_VERSION_ID VARCHAR(50) NOT NULL,
+ GROUP_ID VARCHAR(500) NOT NULL,
+ ARTIFACT_ID VARCHAR(500) NOT NULL,
+ VERSION VARCHAR(100) NOT NULL,
+ CONSTRAINT PK__EXTENSION_BUNDLE_VERSION_DEPENDENCY_ID PRIMARY KEY (ID),
+ CONSTRAINT FK__EXTENSION_BUNDLE_VERSION_DEPENDENCY_EXTENSION_BUNDLE_VERSION_ID FOREIGN KEY (EXTENSION_BUNDLE_VERSION_ID) REFERENCES EXTENSION_BUNDLE_VERSION(ID) ON DELETE CASCADE,
+ CONSTRAINT UNIQUE__EXTENSION_BUNDLE_VERSION_DEPENDENCY_BUNDLE_ID_GROUP_ARTIFACT_VERSION UNIQUE (EXTENSION_BUNDLE_VERSION_ID, GROUP_ID, ARTIFACT_ID, VERSION)
+);
+
+CREATE TABLE EXTENSION (
+ ID VARCHAR(50) NOT NULL,
+ EXTENSION_BUNDLE_VERSION_ID VARCHAR(50) NOT NULL,
+ TYPE VARCHAR(500) NOT NULL,
+ TYPE_DESCRIPTION TEXT NOT NULL,
+ IS_RESTRICTED INT NOT NULL,
+ CATEGORY VARCHAR(100) NOT NULL,
+ TAGS TEXT,
+ CONSTRAINT PK__EXTENSION_ID PRIMARY KEY (ID),
+ CONSTRAINT FK__EXTENSION_EXTENSION_BUNDLE_VERSION_ID FOREIGN KEY (EXTENSION_BUNDLE_VERSION_ID) REFERENCES EXTENSION_BUNDLE_VERSION(ID) ON DELETE CASCADE,
+ CONSTRAINT UNIQUE__EXTENSION_EXTENSION_BUNDLE_VERSION_ID_AND_TYPE UNIQUE (EXTENSION_BUNDLE_VERSION_ID, TYPE)
+);
+
+CREATE TABLE EXTENSION_TAG (
+ EXTENSION_ID VARCHAR(50) NOT NULL,
+ TAG VARCHAR(200) NOT NULL,
+ CONSTRAINT PK__EXTENSION_TAG_EXTENSION_ID_AND_TAG PRIMARY KEY (EXTENSION_ID, TAG),
+ CONSTRAINT FK__EXTENSION_TAG_EXTENSION_ID FOREIGN KEY (EXTENSION_ID) REFERENCES EXTENSION(ID) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V4__AddCascadeOnDelete.sql b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V4__AddCascadeOnDelete.sql
new file mode 100644
index 0000000..5b0e6c6
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/resources/db/migration/V4__AddCascadeOnDelete.sql
@@ -0,0 +1,23 @@
+-- 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.
+
+ALTER TABLE BUCKET_ITEM DROP CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID;
+ALTER TABLE BUCKET_ITEM ADD CONSTRAINT FK__BUCKET_ITEM_BUCKET_ID FOREIGN KEY (BUCKET_ID) REFERENCES BUCKET(ID) ON DELETE CASCADE;
+
+ALTER TABLE FLOW DROP CONSTRAINT FK__FLOW_BUCKET_ITEM_ID;
+ALTER TABLE FLOW ADD CONSTRAINT FK__FLOW_BUCKET_ITEM_ID FOREIGN KEY (ID) REFERENCES BUCKET_ITEM(ID) ON DELETE CASCADE;
+
+ALTER TABLE FLOW_SNAPSHOT DROP CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID;
+ALTER TABLE FLOW_SNAPSHOT ADD CONSTRAINT FK__FLOW_SNAPSHOT_FLOW_ID FOREIGN KEY (FLOW_ID) REFERENCES FLOW(ID) ON DELETE CASCADE;
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/xsd/providers.xsd b/nifi-registry-core/nifi-registry-framework/src/main/xsd/providers.xsd
index ce82dcc..4e9f5d1 100644
--- a/nifi-registry-core/nifi-registry-framework/src/main/xsd/providers.xsd
+++ b/nifi-registry-core/nifi-registry-framework/src/main/xsd/providers.xsd
@@ -44,6 +44,7 @@
<xs:sequence>
<xs:element name="flowPersistenceProvider" type="Provider" minOccurs="1" maxOccurs="1" />
<xs:element name="eventHookProvider" type="Provider" minOccurs="0" maxOccurs="unbounded" />
+ <xs:element name="extensionBundlePersistenceProvider" type="Provider" minOccurs="1" maxOccurs="1" />
</xs:sequence>
</xs:complexType>
</xs:element>
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java
index 35ba757..a2bacd4 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/TestDatabaseMetadataService.java
@@ -19,6 +19,12 @@
import org.apache.nifi.registry.db.entity.BucketEntity;
import org.apache.nifi.registry.db.entity.BucketItemEntity;
import org.apache.nifi.registry.db.entity.BucketItemEntityType;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleEntityType;
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionDependencyEntity;
+import org.apache.nifi.registry.db.entity.ExtensionBundleVersionEntity;
+import org.apache.nifi.registry.db.entity.ExtensionEntity;
+import org.apache.nifi.registry.db.entity.ExtensionEntityCategory;
import org.apache.nifi.registry.db.entity.FlowEntity;
import org.apache.nifi.registry.db.entity.FlowSnapshotEntity;
import org.apache.nifi.registry.service.MetadataService;
@@ -30,9 +36,11 @@
import java.util.Date;
import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -383,4 +391,448 @@
assertNull(deletedEntity);
}
+ //----------------- Extension Bundles ---------------------------------
+
+ @Test
+ public void testGetExtensionBundleById() {
+ final ExtensionBundleEntity entity = metadataService.getExtensionBundle("eb1");
+ assertNotNull(entity);
+
+ assertEquals("eb1", entity.getId());
+ assertEquals("nifi-example-processors-nar", entity.getName());
+ assertEquals("Example processors bundle", entity.getDescription());
+ assertNotNull(entity.getCreated());
+ assertNotNull(entity.getModified());
+ assertEquals(BucketItemEntityType.EXTENSION_BUNDLE, entity.getType());
+ assertEquals("3", entity.getBucketId());
+
+ assertEquals(ExtensionBundleEntityType.NIFI_NAR, entity.getBundleType());
+
+ assertEquals("org.apache.nifi", entity.getGroupId());
+ assertEquals("nifi-example-processors-nar", entity.getArtifactId());
+ }
+
+ @Test
+ public void testGetExtensionBundleDoesNotExist() {
+ final ExtensionBundleEntity entity = metadataService.getExtensionBundle("does-not-exist");
+ assertNull(entity);
+ }
+
+ @Test
+ public void testGetExtensionBundleByGroupArtifact() {
+ final String bucketId = "3";
+ final String group = "org.apache.nifi";
+ final String artifact = "nifi-example-service-api-nar";
+
+ final ExtensionBundleEntity entity = metadataService.getExtensionBundle(bucketId, group, artifact);
+ assertNotNull(entity);
+ assertEquals(bucketId, entity.getBucketId());
+
+ assertEquals(group, entity.getGroupId());
+ assertEquals(artifact, entity.getArtifactId());
+ }
+
+ @Test
+ public void testGetExtensionBundleByGroupArtifactDoesNotExist() {
+ final String bucketId = "3";
+ final String group = "org.apache.nifi";
+ final String artifact = "does-not-exist";
+
+ final ExtensionBundleEntity entity = metadataService.getExtensionBundle(bucketId, group, artifact);
+ assertNull(entity);
+ }
+
+ @Test
+ public void testGetExtensionBundles() {
+ final Set<String> bucketIds = new HashSet<>();
+ bucketIds.add("1");
+ bucketIds.add("2");
+ bucketIds.add("3");
+
+ final List<ExtensionBundleEntity> bundles = metadataService.getExtensionBundles(bucketIds);
+ assertNotNull(bundles);
+ assertEquals(3, bundles.size());
+
+ bundles.forEach(b -> {
+ assertTrue(b.getVersionCount() > 0);
+ assertNotNull(b.getBucketName());
+ });
+ }
+
+ @Test
+ public void testGetExtensionBundlesByBucket() {
+ final List<ExtensionBundleEntity> bundles = metadataService.getExtensionBundlesByBucket("3");
+ assertNotNull(bundles);
+ assertEquals(3, bundles.size());
+
+ final List<ExtensionBundleEntity> bundles2 = metadataService.getExtensionBundlesByBucket("6");
+ assertNotNull(bundles2);
+ assertEquals(0, bundles2.size());
+ }
+
+ @Test
+ public void testGetExtensionBundlesByBucketAndGroup() {
+ final List<ExtensionBundleEntity> bundles = metadataService.getExtensionBundlesByBucketAndGroup("3", "org.apache.nifi");
+ assertNotNull(bundles);
+ assertEquals(3, bundles.size());
+
+ final List<ExtensionBundleEntity> bundles2 = metadataService.getExtensionBundlesByBucketAndGroup("3", "does-not-exist");
+ assertNotNull(bundles2);
+ assertEquals(0, bundles2.size());
+ }
+
+ @Test
+ public void testCreateExtensionBundle() {
+ final ExtensionBundleEntity entity = new ExtensionBundleEntity();
+ entity.setId(UUID.randomUUID().toString());
+ entity.setBucketId("3");
+ entity.setName("nifi-foo-nar");
+ entity.setDescription("This is foo nar");
+ entity.setCreated(new Date());
+ entity.setModified(new Date());
+ entity.setGroupId("org.apache.nifi");
+ entity.setArtifactId("nifi-foo-nar");
+ entity.setBundleType(ExtensionBundleEntityType.NIFI_NAR);
+
+ final ExtensionBundleEntity createdEntity = metadataService.createExtensionBundle(entity);
+ assertNotNull(createdEntity);
+
+ final List<ExtensionBundleEntity> bundles = metadataService.getExtensionBundlesByBucket("3");
+ assertNotNull(bundles);
+ assertEquals(4, bundles.size());
+ }
+
+ @Test
+ public void testDeleteExtensionBundle() {
+ final List<ExtensionBundleEntity> bundles = metadataService.getExtensionBundlesByBucket("3");
+ assertNotNull(bundles);
+ assertEquals(3, bundles.size());
+
+ final ExtensionBundleEntity existingBundle = bundles.get(0);
+ metadataService.deleteExtensionBundle(existingBundle);
+
+ final ExtensionBundleEntity deletedBundle = metadataService.getExtensionBundle(existingBundle.getId());
+ assertNull(deletedBundle);
+
+ final List<ExtensionBundleEntity> bundlesAfterDelete = metadataService.getExtensionBundlesByBucket("3");
+ assertNotNull(bundlesAfterDelete);
+ assertEquals(2, bundlesAfterDelete.size());
+ }
+
+ @Test
+ public void testDeleteBucketWithExtensionBundles() {
+ final List<ExtensionBundleEntity> bundles = metadataService.getExtensionBundlesByBucket("3");
+ assertNotNull(bundles);
+ assertEquals(3, bundles.size());
+
+ final BucketEntity bucket = metadataService.getBucketById("3");
+ assertNotNull(bucket);
+ metadataService.deleteBucket(bucket);
+
+ final List<ExtensionBundleEntity> bundlesAfterDelete = metadataService.getExtensionBundlesByBucket("3");
+ assertNotNull(bundlesAfterDelete);
+ assertEquals(0, bundlesAfterDelete.size());
+ }
+
+ //----------------- Extension Bundle Versions ---------------------------------
+
+ @Test
+ public void testCreateExtensionBundleVersion() {
+ final ExtensionBundleVersionEntity bundleVersion = new ExtensionBundleVersionEntity();
+ bundleVersion.setId(UUID.randomUUID().toString());
+ bundleVersion.setExtensionBundleId("eb1");
+ bundleVersion.setVersion("1.1.0");
+ bundleVersion.setCreated(new Date());
+ bundleVersion.setCreatedBy("user2");
+ bundleVersion.setDescription("This is v1.1.0");
+ bundleVersion.setSha256Hex("123456789");
+ bundleVersion.setSha256Supplied(false);
+
+ metadataService.createExtensionBundleVersion(bundleVersion);
+
+ final ExtensionBundleVersionEntity createdBundleVersion = metadataService.getExtensionBundleVersion("eb1", "1.1.0");
+ assertNotNull(createdBundleVersion);
+ assertEquals(bundleVersion.getId(), createdBundleVersion.getId());
+ assertFalse(bundleVersion.getSha256Supplied());
+ }
+
+ @Test
+ public void testGetExtensionBundleVersionByBundleIdAndVersion() {
+ final ExtensionBundleVersionEntity bundleVersion = metadataService.getExtensionBundleVersion("eb1", "1.0.0");
+ assertNotNull(bundleVersion);
+ assertEquals("eb1-v1", bundleVersion.getId());
+ assertEquals("eb1", bundleVersion.getExtensionBundleId());
+ assertEquals("1.0.0", bundleVersion.getVersion());
+ assertNotNull(bundleVersion.getCreated());
+ assertEquals("user1", bundleVersion.getCreatedBy());
+ assertEquals("First version of eb1", bundleVersion.getDescription());
+ assertTrue(bundleVersion.getSha256Supplied());
+ }
+
+ @Test
+ public void testGetExtensionBundleVersionByBundleIdAndVersionDoesNotExist() {
+ final ExtensionBundleVersionEntity bundleVersion = metadataService.getExtensionBundleVersion("does-not-exist", "1.0.0");
+ assertNull(bundleVersion);
+ }
+
+ @Test
+ public void testGetExtensionBundleVersionByBucketGroupArtifactVersion() {
+ final String bucketId = "3";
+ final String groupId = "org.apache.nifi";
+ final String artifactId = "nifi-example-processors-nar";
+ final String version = "1.0.0";
+
+ final ExtensionBundleVersionEntity bundleVersion = metadataService.getExtensionBundleVersion(bucketId, groupId, artifactId, version);
+ assertNotNull(bundleVersion);
+ assertEquals("eb1-v1", bundleVersion.getId());
+ assertTrue(bundleVersion.getSha256Supplied());
+ }
+
+ @Test
+ public void testGetExtensionBundleVersionByBucketGroupArtifactVersionWhenDoesNotExist() {
+ final String bucketId = "3";
+ final String groupId = "org.apache.nifi";
+ final String artifactId = "nifi-example-processors-nar";
+ final String version = "FOO";
+
+ final ExtensionBundleVersionEntity bundleVersion = metadataService.getExtensionBundleVersion(bucketId, groupId, artifactId, version);
+ assertNull(bundleVersion);
+ }
+
+ @Test
+ public void testGetExtensionBundleVersionsByBundleId() {
+ final List<ExtensionBundleVersionEntity> bundleVersions = metadataService.getExtensionBundleVersions("eb1");
+ assertNotNull(bundleVersions);
+ assertEquals(1, bundleVersions.size());
+
+ final ExtensionBundleVersionEntity bundleVersion = bundleVersions.get(0);
+ assertEquals("eb1", bundleVersion.getExtensionBundleId());
+ }
+
+ @Test
+ public void testGetExtensionBundleVersionsByBundleIdWhenDoesNotExist() {
+ final List<ExtensionBundleVersionEntity> bundleVersions = metadataService.getExtensionBundleVersions("does-not-exist");
+ assertNotNull(bundleVersions);
+ assertEquals(0, bundleVersions.size());
+ }
+
+ @Test
+ public void testGetExtensionBundleVersionsByBucketGroupArtifact() {
+ final String bucketId = "3";
+ final String groupId = "org.apache.nifi";
+ final String artifactId = "nifi-example-processors-nar";
+
+ final List<ExtensionBundleVersionEntity> bundleVersions = metadataService.getExtensionBundleVersions(bucketId, groupId, artifactId);
+ assertNotNull(bundleVersions);
+ assertEquals(1, bundleVersions.size());
+
+ final ExtensionBundleVersionEntity bundleVersion = bundleVersions.get(0);
+ assertEquals("eb1-v1", bundleVersion.getId());
+ }
+
+ @Test
+ public void testGetExtensionBundleVersionsByBucketGroupArtifactWhenDoesNotExist() {
+ final String bucketId = "3";
+ final String groupId = "org.apache.nifi";
+ final String artifactId = "does-not-exist";
+
+ final List<ExtensionBundleVersionEntity> bundleVersions = metadataService.getExtensionBundleVersions(bucketId, groupId, artifactId);
+ assertNotNull(bundleVersions);
+ assertEquals(0, bundleVersions.size());
+ }
+
+ @Test
+ public void testGetExtensionBundleVersionsGlobal() {
+ final String groupId = "org.apache.nifi";
+ final String artifactId = "nifi-example-processors-nar";
+ final String version = "1.0.0";
+
+ final List<ExtensionBundleVersionEntity> bundleVersions = metadataService.getExtensionBundleVersionsGlobal(groupId, artifactId, version);
+ assertNotNull(bundleVersions);
+ assertEquals(1, bundleVersions.size());
+
+ final ExtensionBundleVersionEntity bundleVersion = bundleVersions.get(0);
+ assertEquals("eb1-v1", bundleVersion.getId());
+ }
+
+ @Test
+ public void testDeleteExtensionBundleVersion() {
+ final ExtensionBundleVersionEntity bundleVersion = metadataService.getExtensionBundleVersion("eb1", "1.0.0");
+ assertNotNull(bundleVersion);
+
+ metadataService.deleteExtensionBundleVersion(bundleVersion);
+
+ final ExtensionBundleVersionEntity deletedBundleVersion = metadataService.getExtensionBundleVersion("eb1", "1.0.0");
+ assertNull(deletedBundleVersion);
+ }
+
+ // ---------- Extension Bundle Version Dependencies ------------
+
+ @Test
+ public void testCreateExtensionBundleVersionDependency() {
+ final ExtensionBundleVersionEntity versionEntity = metadataService.getExtensionBundleVersion("eb1", "1.0.0");
+ assertNotNull(versionEntity);
+
+ final List<ExtensionBundleVersionDependencyEntity> dependencies = metadataService.getDependenciesForBundleVersion(versionEntity.getId());
+ assertNotNull(dependencies);
+ assertEquals(1, dependencies.size());
+
+ final ExtensionBundleVersionDependencyEntity dependencyEntity = new ExtensionBundleVersionDependencyEntity();
+ dependencyEntity.setId(UUID.randomUUID().toString());
+ dependencyEntity.setExtensionBundleVersionId(versionEntity.getId());
+ dependencyEntity.setGroupId("com.foo");
+ dependencyEntity.setArtifactId("foo-nar");
+ dependencyEntity.setVersion("1.1.1");
+
+ metadataService.createDependency(dependencyEntity);
+
+ final List<ExtensionBundleVersionDependencyEntity> dependencies2 = metadataService.getDependenciesForBundleVersion(versionEntity.getId());
+ assertNotNull(dependencies2);
+ assertEquals(2, dependencies2.size());
+ }
+
+ @Test
+ public void testGetExtensionBundleVersionDependencies() {
+ final List<ExtensionBundleVersionDependencyEntity> dependencies = metadataService.getDependenciesForBundleVersion("eb1-v1");
+ assertNotNull(dependencies);
+ assertEquals(1, dependencies.size());
+
+ final ExtensionBundleVersionDependencyEntity dependency = dependencies.get(0);
+ assertEquals("eb1-v1-dep1", dependency.getId());
+ assertEquals("eb1-v1", dependency.getExtensionBundleVersionId());
+ assertEquals("org.apache.nifi", dependency.getGroupId());
+ assertEquals("nifi-example-service-api-nar", dependency.getArtifactId());
+ assertEquals("2.0.0", dependency.getVersion());
+ }
+
+ @Test
+ public void testGetExtensionBundleVersionDependenciesWhenNoneExist() {
+ final List<ExtensionBundleVersionDependencyEntity> dependencies = metadataService.getDependenciesForBundleVersion("DOES-NOT-EXIST");
+ assertNotNull(dependencies);
+ assertEquals(0, dependencies.size());
+ }
+
+ //----------------- Extensions ---------------------------------
+
+ @Test
+ public void testCreateExtension() {
+ final ExtensionEntity extension = new ExtensionEntity();
+ extension.setId("4");
+ extension.setExtensionBundleVersionId("eb1-v1");
+ extension.setType("com.example.FooBarProcessor");
+ extension.setTypeDescription("This the FoorBarProcessor");
+ extension.setCategory(ExtensionEntityCategory.PROCESSOR);
+ extension.setRestricted(false);
+ extension.setTags("tag1, tag2");
+
+ metadataService.createExtension(extension);
+
+ final ExtensionEntity retrievedExtension = metadataService.getExtensionById(extension.getId());
+ assertEquals(extension.getId(), retrievedExtension.getId());
+ assertEquals(extension.getExtensionBundleVersionId(), retrievedExtension.getExtensionBundleVersionId());
+ assertEquals(extension.getType(), retrievedExtension.getType());
+ assertEquals(extension.getTypeDescription(), retrievedExtension.getTypeDescription());
+ assertEquals(extension.getCategory(), retrievedExtension.getCategory());
+ assertEquals(extension.isRestricted(), retrievedExtension.isRestricted());
+ assertEquals(extension.getTags(), retrievedExtension.getTags());
+
+ final List<ExtensionEntity> tag1Extensions = metadataService.getExtensionsByTag("tag1");
+ assertNotNull(tag1Extensions);
+ assertEquals(1, tag1Extensions.size());
+ assertEquals(extension.getId(), tag1Extensions.get(0).getId());
+
+ final List<ExtensionEntity> tag2Extensions = metadataService.getExtensionsByTag("tag2");
+ assertNotNull(tag2Extensions);
+ assertEquals(1, tag2Extensions.size());
+ assertEquals(extension.getId(), tag2Extensions.get(0).getId());
+ }
+
+ @Test
+ public void testGetExtensionById() {
+ final ExtensionEntity extension = metadataService.getExtensionById("e1");
+ assertNotNull(extension);
+ assertEquals("e1", extension.getId());
+ assertEquals("org.apache.nifi.ExampleProcessor", extension.getType());
+ }
+
+ @Test
+ public void testGetExtensionByIdDoesNotExist() {
+ final ExtensionEntity extension = metadataService.getExtensionById("does-not-exist");
+ assertNull(extension);
+ }
+
+ @Test
+ public void testGetAllExtensions() {
+ final List<ExtensionEntity> extensions = metadataService.getAllExtensions();
+ assertNotNull(extensions);
+ assertEquals(3, extensions.size());
+ }
+
+ @Test
+ public void testGetExtensionsByBundleVersionId() {
+ final List<ExtensionEntity> extensions = metadataService.getExtensionsByBundleVersionId("eb1-v1");
+ assertNotNull(extensions);
+ assertEquals(2, extensions.size());
+ }
+
+ @Test
+ public void testGetExtensionsByBundleVersionIdDoesNotExist() {
+ final List<ExtensionEntity> extensions = metadataService.getExtensionsByBundleVersionId("does-not-exist");
+ assertNotNull(extensions);
+ assertEquals(0, extensions.size());
+ }
+
+ @Test
+ public void testGetExtensionsByBundleCoordinate() {
+ final String bucketId = "3";
+ final String groupId = "org.apache.nifi";
+ final String artifactId = "nifi-example-processors-nar";
+ final String version = "1.0.0";
+
+ final List<ExtensionEntity> extensions = metadataService.getExtensionsByBundleCoordinate(bucketId, groupId, artifactId, version);
+ assertNotNull(extensions);
+ assertEquals(2, extensions.size());
+ }
+
+ @Test
+ public void testGetExtensionsByBundleCoordinateDoesNotExist() {
+ final String bucketId = "3";
+ final String groupId = "org.apache.nifi";
+ final String artifactId = "does-not-exist";
+ final String version = "1.0.0";
+
+ final List<ExtensionEntity> extensions = metadataService.getExtensionsByBundleCoordinate(bucketId, groupId, artifactId, version);
+ assertNotNull(extensions);
+ assertEquals(0, extensions.size());
+ }
+
+ @Test
+ public void testGetExtensionsByCategory() {
+ final List<ExtensionEntity> services = metadataService.getExtensionsByCategory(ExtensionEntityCategory.CONTROLLER_SERVICE);
+ assertNotNull(services);
+ assertEquals(1, services.size());
+
+ final List<ExtensionEntity> processors = metadataService.getExtensionsByCategory(ExtensionEntityCategory.PROCESSOR);
+ assertNotNull(processors);
+ assertEquals(2, processors.size());
+ }
+
+ @Test
+ public void testGetExtensionTags() {
+ final Set<String> tags = metadataService.getAllExtensionTags();
+ assertNotNull(tags);
+ assertEquals(4, tags.size());
+ }
+
+ @Test
+ public void testDeleteExtension() {
+ final ExtensionEntity extension = metadataService.getExtensionById("e1");
+ assertNotNull(extension);
+
+ metadataService.deleteExtension(extension);
+
+ final ExtensionEntity deletedExtension = metadataService.getExtensionById("e1");
+ assertNull(deletedExtension);
+ }
+
}
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyDatabaseService.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyDatabaseService.java
index df37e8e..116ba35 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyDatabaseService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/db/migration/TestLegacyDatabaseService.java
@@ -52,9 +52,11 @@
jdbcTemplate = new JdbcTemplate(dataSource);
- flyway = new Flyway();
- flyway.setDataSource(dataSource);
- flyway.setLocations("db/original");
+ flyway = Flyway.configure()
+ .dataSource(dataSource)
+ .locations("db/original")
+ .load();
+
flyway.migrate();
bucketEntityV1 = new BucketEntityV1();
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventFactory.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventFactory.java
index a9ad911..e5075c0 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventFactory.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/event/TestEventFactory.java
@@ -17,6 +17,10 @@
package org.apache.nifi.registry.event;
import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleType;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
import org.apache.nifi.registry.flow.VersionedFlow;
import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
@@ -36,6 +40,8 @@
private Bucket bucket;
private VersionedFlow versionedFlow;
private VersionedFlowSnapshot versionedFlowSnapshot;
+ private ExtensionBundle extensionBundle;
+ private ExtensionBundleVersion extensionBundleVersion;
@Before
public void setup() {
@@ -60,6 +66,24 @@
versionedFlowSnapshot = new VersionedFlowSnapshot();
versionedFlowSnapshot.setSnapshotMetadata(metadata);
versionedFlowSnapshot.setFlowContents(new VersionedProcessGroup());
+
+ extensionBundle = new ExtensionBundle();
+ extensionBundle.setIdentifier(UUID.randomUUID().toString());
+ extensionBundle.setBucketIdentifier(bucket.getIdentifier());
+ extensionBundle.setBundleType(ExtensionBundleType.NIFI_NAR);
+ extensionBundle.setGroupId("org.apache.nifi");
+ extensionBundle.setArtifactId("nifi-foo-nar");
+
+ final ExtensionBundleVersionMetadata bundleVersionMetadata = new ExtensionBundleVersionMetadata();
+ bundleVersionMetadata.setId(UUID.randomUUID().toString());
+ bundleVersionMetadata.setVersion("1.0.0");
+ bundleVersionMetadata.setBucketId(bucket.getIdentifier());
+ bundleVersionMetadata.setExtensionBundleId(extensionBundle.getIdentifier());
+
+ extensionBundleVersion = new ExtensionBundleVersion();
+ extensionBundleVersion.setVersionMetadata(bundleVersionMetadata);
+ extensionBundleVersion.setExtensionBundle(extensionBundle);
+ extensionBundleVersion.setBucket(bucket);
}
@Test
@@ -166,4 +190,57 @@
assertEquals("", event.getField(EventFieldName.COMMENT).getValue());
}
+ @Test
+ public void testExtensionBundleCreated() {
+ final Event event = EventFactory.extensionBundleCreated(extensionBundle);
+ event.validate();
+
+ assertEquals(EventType.CREATE_EXTENSION_BUNDLE, event.getEventType());
+ assertEquals(3, event.getFields().size());
+
+ assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue());
+ assertEquals(extensionBundle.getIdentifier(), event.getField(EventFieldName.EXTENSION_BUNDLE_ID).getValue());
+ assertEquals("unknown", event.getField(EventFieldName.USER).getValue());
+ }
+
+ @Test
+ public void testExtensionBundleDeleted() {
+ final Event event = EventFactory.extensionBundleDeleted(extensionBundle);
+ event.validate();
+
+ assertEquals(EventType.DELETE_EXTENSION_BUNDLE, event.getEventType());
+ assertEquals(3, event.getFields().size());
+
+ assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue());
+ assertEquals(extensionBundle.getIdentifier(), event.getField(EventFieldName.EXTENSION_BUNDLE_ID).getValue());
+ assertEquals("unknown", event.getField(EventFieldName.USER).getValue());
+ }
+
+ @Test
+ public void testExtensionBundleVersionCreated() {
+ final Event event = EventFactory.extensionBundleVersionCreated(extensionBundleVersion);
+ event.validate();
+
+ assertEquals(EventType.CREATE_EXTENSION_BUNDLE_VERSION, event.getEventType());
+ assertEquals(4, event.getFields().size());
+
+ assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue());
+ assertEquals(extensionBundle.getIdentifier(), event.getField(EventFieldName.EXTENSION_BUNDLE_ID).getValue());
+ assertEquals(extensionBundleVersion.getVersionMetadata().getVersion(), event.getField(EventFieldName.VERSION).getValue());
+ assertEquals("unknown", event.getField(EventFieldName.USER).getValue());
+ }
+
+ @Test
+ public void testExtensionBundleVersionDeleted() {
+ final Event event = EventFactory.extensionBundleVersionDeleted(extensionBundleVersion);
+ event.validate();
+
+ assertEquals(EventType.DELETE_EXTENSION_BUNDLE_VERSION, event.getEventType());
+ assertEquals(4, event.getFields().size());
+
+ assertEquals(bucket.getIdentifier(), event.getField(EventFieldName.BUCKET_ID).getValue());
+ assertEquals(extensionBundle.getIdentifier(), event.getField(EventFieldName.EXTENSION_BUNDLE_ID).getValue());
+ assertEquals(extensionBundleVersion.getVersionMetadata().getVersion(), event.getField(EventFieldName.VERSION).getValue());
+ assertEquals("unknown", event.getField(EventFieldName.USER).getValue());
+ }
}
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockExtensionBundlePersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockExtensionBundlePersistenceProvider.java
new file mode 100644
index 0000000..ba7f12f
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/MockExtensionBundlePersistenceProvider.java
@@ -0,0 +1,62 @@
+/*
+ * 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.provider;
+
+import org.apache.nifi.registry.extension.ExtensionBundleContext;
+import org.apache.nifi.registry.extension.ExtensionBundlePersistenceException;
+import org.apache.nifi.registry.extension.ExtensionBundlePersistenceProvider;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+
+public class MockExtensionBundlePersistenceProvider implements ExtensionBundlePersistenceProvider {
+
+ private Map<String,String> properties;
+
+ @Override
+ public void saveBundleVersion(ExtensionBundleContext context, InputStream contentStream)
+ throws ExtensionBundlePersistenceException {
+
+ }
+
+ @Override
+ public void getBundleVersion(ExtensionBundleContext context, OutputStream outputStream) throws ExtensionBundlePersistenceException {
+
+ }
+
+ @Override
+ public void deleteBundleVersion(ExtensionBundleContext context) throws ExtensionBundlePersistenceException {
+
+ }
+
+ @Override
+ public void deleteAllBundleVersions(String bucketId, String bucketName, String groupId, String artifactId) throws ExtensionBundlePersistenceException {
+
+ }
+
+ @Override
+ public void onConfigured(ProviderConfigurationContext configurationContext)
+ throws ProviderCreationException {
+ properties = configurationContext.getProperties();
+ }
+
+ public Map<String,String> getProperties() {
+ return properties;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
index 30f66ef..2105cff 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/TestStandardProviderFactory.java
@@ -16,6 +16,7 @@
*/
package org.apache.nifi.registry.provider;
+import org.apache.nifi.registry.extension.ExtensionBundlePersistenceProvider;
import org.apache.nifi.registry.extension.ExtensionManager;
import org.apache.nifi.registry.flow.FlowPersistenceProvider;
import org.apache.nifi.registry.properties.NiFiRegistryProperties;
@@ -47,6 +48,14 @@
assertNotNull(mockFlowProvider.getProperties());
assertEquals("flow foo", mockFlowProvider.getProperties().get("Flow Property 1"));
assertEquals("flow bar", mockFlowProvider.getProperties().get("Flow Property 2"));
+
+ final ExtensionBundlePersistenceProvider bundlePersistenceProvider = providerFactory.getExtensionBundlePersistenceProvider();
+ assertNotNull(bundlePersistenceProvider);
+
+ final MockExtensionBundlePersistenceProvider mockBundlePersistenceProvider = (MockExtensionBundlePersistenceProvider) bundlePersistenceProvider;
+ assertNotNull(mockBundlePersistenceProvider.getProperties());
+ assertEquals("extension foo", mockBundlePersistenceProvider.getProperties().get("Extension Property 1"));
+ assertEquals("extension bar", mockBundlePersistenceProvider.getProperties().get("Extension Property 2"));
}
@Test(expected = ProviderFactoryException.class)
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemExtensionBundlePersistenceProvider.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemExtensionBundlePersistenceProvider.java
new file mode 100644
index 0000000..5a611bb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/provider/extension/TestFileSystemExtensionBundlePersistenceProvider.java
@@ -0,0 +1,295 @@
+/*
+ * 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.provider.extension;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.nifi.registry.extension.ExtensionBundleContext;
+import org.apache.nifi.registry.extension.ExtensionBundlePersistenceException;
+import org.apache.nifi.registry.extension.ExtensionBundlePersistenceProvider;
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.mockito.Mockito.when;
+
+public class TestFileSystemExtensionBundlePersistenceProvider {
+
+ static final String EXTENSION_STORAGE_DIR = "target/extension_storage";
+
+ static final ProviderConfigurationContext CONFIGURATION_CONTEXT = new ProviderConfigurationContext() {
+ @Override
+ public Map<String, String> getProperties() {
+ final Map<String,String> props = new HashMap<>();
+ props.put(FileSystemExtensionBundlePersistenceProvider.BUNDLE_STORAGE_DIR_PROP, EXTENSION_STORAGE_DIR);
+ return props;
+ }
+ };
+
+ private File bundleStorageDir;
+ private ExtensionBundlePersistenceProvider fileSystemBundleProvider;
+
+ @Before
+ public void setup() throws IOException {
+ bundleStorageDir = new File(EXTENSION_STORAGE_DIR);
+ if (bundleStorageDir.exists()) {
+ org.apache.commons.io.FileUtils.cleanDirectory(bundleStorageDir);
+ bundleStorageDir.delete();
+ }
+
+ Assert.assertFalse(bundleStorageDir.exists());
+
+ fileSystemBundleProvider = new FileSystemExtensionBundlePersistenceProvider();
+ fileSystemBundleProvider.onConfigured(CONFIGURATION_CONTEXT);
+ Assert.assertTrue(bundleStorageDir.exists());
+ }
+
+ @Test
+ public void testSaveSuccessfully() throws IOException {
+ // first version in b1
+ final String content1 = "g1-a1-1.0.0";
+ createAndSaveBundleVersion(fileSystemBundleProvider, "b1", "g1", "a1", "1.0.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, content1);
+ verifyBundleVersion(bundleStorageDir, "b1", "g1", "a1", "1.0.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, content1);
+
+ // second version in b1
+ final String content2 = "g1-a1-1.1.0";
+ createAndSaveBundleVersion(fileSystemBundleProvider, "b1", "g1", "a1", "1.1.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, content2);
+ verifyBundleVersion(bundleStorageDir, "b1", "g1", "a1", "1.1.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, content2);
+
+ // same bundle but in b2
+ final String content3 = "g1-a1-1.1.0";
+ createAndSaveBundleVersion(fileSystemBundleProvider, "b2", "g1", "a1", "1.1.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, content3);
+ verifyBundleVersion(bundleStorageDir, "b2", "g1", "a1", "1.1.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, content2);
+ }
+
+ @Test
+ public void testSaveWhenBundleVersionAlreadyExists() throws IOException {
+ final String content1 = "g1-a1-1.0.0";
+ createAndSaveBundleVersion(fileSystemBundleProvider, "b1", "g1", "a1", "1.0.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, content1);
+ verifyBundleVersion(bundleStorageDir, "b1", "g1", "a1", "1.0.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, content1);
+
+ // try to save same bundle version that already exists
+ try {
+ final String newContent = "new content";
+ createAndSaveBundleVersion(fileSystemBundleProvider, "b1", "g1", "a1", "1.0.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, newContent);
+ Assert.fail("Should have thrown exception");
+ } catch (ExtensionBundlePersistenceException e) {
+
+ }
+
+ // verify existing content wasn't modified
+ verifyBundleVersion(bundleStorageDir, "b1", "g1", "a1", "1.0.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, content1);
+ }
+
+ @Test
+ public void testSaveAndGet() throws IOException {
+ final String bucketName = "b1";
+ final String groupId = "g1";
+ final String artifactId = "a1";
+
+ final String content1 = groupId + "-" + artifactId + "-" + "1.0.0";
+ createAndSaveBundleVersion(fileSystemBundleProvider, bucketName, groupId, artifactId, "1.0.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, content1);
+
+ final String content2 = groupId + "-" + artifactId + "-" + "1.1.0";
+ createAndSaveBundleVersion(fileSystemBundleProvider, bucketName, groupId, artifactId, "1.1.0",
+ ExtensionBundleContext.BundleType.NIFI_NAR, content2);
+
+ try (final OutputStream out = new ByteArrayOutputStream()) {
+ final ExtensionBundleContext context = getExtensionBundleContext(
+ bucketName, groupId, artifactId, "1.0.0", ExtensionBundleContext.BundleType.NIFI_NAR);
+ fileSystemBundleProvider.getBundleVersion(context, out);
+
+ final String retrievedContent1 = new String(((ByteArrayOutputStream) out).toByteArray(), StandardCharsets.UTF_8);
+ Assert.assertEquals(content1, retrievedContent1);
+ }
+
+ try (final OutputStream out = new ByteArrayOutputStream()) {
+ final ExtensionBundleContext context = getExtensionBundleContext(
+ bucketName, groupId, artifactId, "1.1.0", ExtensionBundleContext.BundleType.NIFI_NAR);
+ fileSystemBundleProvider.getBundleVersion(context, out);
+
+ final String retrievedContent2 = new String(((ByteArrayOutputStream) out).toByteArray(), StandardCharsets.UTF_8);
+ Assert.assertEquals(content2, retrievedContent2);
+ }
+ }
+
+ @Test(expected = ExtensionBundlePersistenceException.class)
+ public void testGetWhenDoesNotExist() throws IOException {
+ final String bucketName = "b1";
+ final String groupId = "g1";
+ final String artifactId = "a1";
+
+ try (final OutputStream out = new ByteArrayOutputStream()) {
+ final ExtensionBundleContext context = getExtensionBundleContext(
+ bucketName, groupId, artifactId, "1.0.0", ExtensionBundleContext.BundleType.NIFI_NAR);
+ fileSystemBundleProvider.getBundleVersion(context, out);
+ Assert.fail("Should have thrown exception");
+ }
+ }
+
+ @Test
+ public void testDeleteExtensionBundleVersion() throws IOException {
+ final String bucketName = "b1";
+ final String groupId = "g1";
+ final String artifactId = "a1";
+ final String version = "1.0.0";
+ final ExtensionBundleContext.BundleType bundleType = ExtensionBundleContext.BundleType.NIFI_NAR;
+
+ // create and verify the bundle version
+ final String content1 = groupId + "-" + artifactId + "-" + "1.0.0";
+ createAndSaveBundleVersion(fileSystemBundleProvider, bucketName, groupId, artifactId, version, bundleType, content1);
+ verifyBundleVersion(bundleStorageDir, bucketName, groupId, artifactId, version, bundleType, content1);
+
+ // delete the bundle version
+ fileSystemBundleProvider.deleteBundleVersion(getExtensionBundleContext(bucketName, groupId, artifactId, version, bundleType));
+
+ // verify it was deleted
+ final File bundleVersionDir = FileSystemExtensionBundlePersistenceProvider.getBundleVersionDirectory(
+ bundleStorageDir, bucketName, groupId, artifactId, version);
+
+ final File bundleFile = FileSystemExtensionBundlePersistenceProvider.getBundleFile(
+ bundleVersionDir, artifactId, version, bundleType);
+ Assert.assertFalse(bundleFile.exists());
+ }
+
+ @Test
+ public void testDeleteExtensionBundleVersionWhenDoesNotExist() throws IOException {
+ final String bucketName = "b1";
+ final String groupId = "g1";
+ final String artifactId = "a1";
+ final String version = "1.0.0";
+ final ExtensionBundleContext.BundleType bundleType = ExtensionBundleContext.BundleType.NIFI_NAR;
+
+ // verify the bundle version does not already exist
+ final File bundleVersionDir = FileSystemExtensionBundlePersistenceProvider.getBundleVersionDirectory(
+ bundleStorageDir, bucketName, groupId, artifactId, version);
+
+ final File bundleFile = FileSystemExtensionBundlePersistenceProvider.getBundleFile(
+ bundleVersionDir, artifactId, version, bundleType);
+ Assert.assertFalse(bundleFile.exists());
+
+ // delete the bundle version
+ fileSystemBundleProvider.deleteBundleVersion(getExtensionBundleContext(bucketName, groupId, artifactId, version, bundleType));
+ }
+
+ @Test
+ public void testDeleteAllBundleVersions() throws IOException {
+ final String bucketName = "b1";
+ final String groupId = "g1";
+ final String artifactId = "a1";
+ final String version1 = "1.0.0";
+ final String version2 = "2.0.0";
+ final ExtensionBundleContext.BundleType bundleType = ExtensionBundleContext.BundleType.NIFI_NAR;
+
+ // create and verify the bundle version 1
+ final String content1 = groupId + "-" + artifactId + "-" + version1;
+ createAndSaveBundleVersion(fileSystemBundleProvider, bucketName, groupId, artifactId, version1, bundleType, content1);
+ verifyBundleVersion(bundleStorageDir, bucketName, groupId, artifactId, version1, bundleType, content1);
+
+ // create and verify the bundle version 2
+ final String content2 = groupId + "-" + artifactId + "-" + version2;
+ createAndSaveBundleVersion(fileSystemBundleProvider, bucketName, groupId, artifactId, version2, bundleType, content2);
+ verifyBundleVersion(bundleStorageDir, bucketName, groupId, artifactId, version2, bundleType, content2);
+
+ fileSystemBundleProvider.deleteAllBundleVersions(bucketName, bucketName, groupId, artifactId);
+ Assert.assertEquals(0, bundleStorageDir.listFiles().length);
+ }
+
+ @Test
+ public void testDeleteAllBundleVersionsWhenDoesNotExist() throws IOException {
+ final String bucketName = "b1";
+ final String groupId = "g1";
+ final String artifactId = "a1";
+
+ Assert.assertEquals(0, bundleStorageDir.listFiles().length);
+ fileSystemBundleProvider.deleteAllBundleVersions(bucketName, bucketName, groupId, artifactId);
+ Assert.assertEquals(0, bundleStorageDir.listFiles().length);
+ }
+
+ private void createAndSaveBundleVersion(final ExtensionBundlePersistenceProvider persistenceProvider,
+ final String bucketName,
+ final String groupId,
+ final String artifactId,
+ final String version,
+ final ExtensionBundleContext.BundleType bundleType,
+ final String content) throws IOException {
+
+ final ExtensionBundleContext context = getExtensionBundleContext(bucketName, groupId, artifactId, version, bundleType);
+
+ try (final InputStream in = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8))) {
+ persistenceProvider.saveBundleVersion(context, in);
+ }
+ }
+
+ private static ExtensionBundleContext getExtensionBundleContext(final String bucketName,
+ final String groupId,
+ final String artifactId,
+ final String version,
+ final ExtensionBundleContext.BundleType bundleType) {
+ final ExtensionBundleContext context = Mockito.mock(ExtensionBundleContext.class);
+ when(context.getBucketName()).thenReturn(bucketName);
+ when(context.getBundleGroupId()).thenReturn(groupId);
+ when(context.getBundleArtifactId()).thenReturn(artifactId);
+ when(context.getBundleVersion()).thenReturn(version);
+ when(context.getBundleType()).thenReturn(bundleType);
+ return context;
+ }
+
+ private static void verifyBundleVersion(final File storageDir,
+ final String bucketName,
+ final String groupId,
+ final String artifactId,
+ final String version,
+ final ExtensionBundleContext.BundleType bundleType,
+ final String contentString) throws IOException {
+
+ final File bundleVersionDir = FileSystemExtensionBundlePersistenceProvider.getBundleVersionDirectory(
+ storageDir, bucketName, groupId, artifactId, version);
+
+ final File bundleFile = FileSystemExtensionBundlePersistenceProvider.getBundleFile(
+ bundleVersionDir, artifactId, version, bundleType);
+ Assert.assertTrue(bundleFile.exists());
+
+ try (InputStream in = new FileInputStream(bundleFile)) {
+ Assert.assertEquals(contentString, IOUtils.toString(in, StandardCharsets.UTF_8));
+ }
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java
index 242cf28..9a408b2 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/security/ldap/tenants/LdapUserGroupProviderTest.java
@@ -67,7 +67,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
-import static org.mockito.Matchers.anyString;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
index 95e2d1a..0af08c1 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
+++ b/nifi-registry-core/nifi-registry-framework/src/test/java/org/apache/nifi/registry/service/TestRegistryService.java
@@ -32,6 +32,8 @@
import org.apache.nifi.registry.flow.VersionedProcessor;
import org.apache.nifi.registry.serialization.Serializer;
import org.apache.nifi.registry.serialization.VersionedProcessGroupSerializer;
+import org.apache.nifi.registry.service.extension.ExtensionService;
+import org.apache.nifi.registry.service.extension.StandardExtensionService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
@@ -73,6 +75,7 @@
private MetadataService metadataService;
private FlowPersistenceProvider flowPersistenceProvider;
private Serializer<VersionedProcessGroup> snapshotSerializer;
+ private ExtensionService extensionService;
private Validator validator;
private RegistryService registryService;
@@ -82,11 +85,12 @@
metadataService = mock(MetadataService.class);
flowPersistenceProvider = mock(FlowPersistenceProvider.class);
snapshotSerializer = mock(VersionedProcessGroupSerializer.class);
+ extensionService = mock(StandardExtensionService.class);
final ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
- registryService = new RegistryService(metadataService, flowPersistenceProvider, snapshotSerializer, validator);
+ registryService = new RegistryService(metadataService, flowPersistenceProvider, snapshotSerializer, extensionService, validator);
}
// ---------------------- Test Bucket methods ---------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql b/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql
index 8f3cfa8..b8e0d3a 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/db/migration/V999999.1__test-setup.sql
@@ -67,4 +67,225 @@
-- test data for signing keys
insert into signing_key (id, tenant_identity, key_value)
- values ('1', 'unit_test_tenant_identity', '0123456789abcdef');
\ No newline at end of file
+ values ('1', 'unit_test_tenant_identity', '0123456789abcdef');
+
+-- test data for extension bundles
+
+-- processors bundle, depends on service api bundle
+insert into bucket_item (
+ id,
+ name,
+ description,
+ created,
+ modified,
+ item_type,
+ bucket_id
+) values (
+ 'eb1',
+ 'nifi-example-processors-nar',
+ 'Example processors bundle',
+ parsedatetime('2018-11-02 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'),
+ parsedatetime('2018-11-02 12:56:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'),
+ 'EXTENSION_BUNDLE',
+ '3'
+);
+
+insert into extension_bundle (
+ id,
+ bucket_id,
+ bundle_type,
+ group_id,
+ artifact_id
+) values (
+ 'eb1',
+ '3',
+ 'NIFI_NAR',
+ 'org.apache.nifi',
+ 'nifi-example-processors-nar'
+);
+
+insert into extension_bundle_version (
+ id,
+ extension_bundle_id,
+ version,
+ created,
+ created_by,
+ description,
+ sha_256_hex,
+ sha_256_supplied
+) values (
+ 'eb1-v1',
+ 'eb1',
+ '1.0.0',
+ parsedatetime('2018-11-02 13:00:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'),
+ 'user1',
+ 'First version of eb1',
+ '123456789',
+ '1'
+);
+
+insert into extension_bundle_version_dependency (
+ id,
+ extension_bundle_version_id,
+ group_id,
+ artifact_id,
+ version
+) values (
+ 'eb1-v1-dep1',
+ 'eb1-v1',
+ 'org.apache.nifi',
+ 'nifi-example-service-api-nar',
+ '2.0.0'
+);
+
+-- service impl bundle, depends on service api bundle
+insert into bucket_item (
+ id,
+ name,
+ description,
+ created,
+ modified,
+ item_type,
+ bucket_id
+) values (
+ 'eb2',
+ 'nifi-example-services-nar',
+ 'Example services bundle',
+ parsedatetime('2018-11-02 12:57:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'),
+ parsedatetime('2018-11-02 12:57:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'),
+ 'EXTENSION_BUNDLE',
+ '3'
+);
+
+insert into extension_bundle (
+ id,
+ bucket_id,
+ bundle_type,
+ group_id,
+ artifact_id
+) values (
+ 'eb2',
+ '3',
+ 'NIFI_NAR',
+ 'org.apache.nifi',
+ 'nifi-example-services-nar'
+);
+
+insert into extension_bundle_version (
+ id,
+ extension_bundle_id,
+ version,
+ created,
+ created_by,
+ description,
+ sha_256_hex,
+ sha_256_supplied
+) values (
+ 'eb2-v1',
+ 'eb2',
+ '1.0.0',
+ parsedatetime('2018-11-02 13:00:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'),
+ 'user1',
+ 'First version of eb2',
+ '123456789',
+ '1'
+);
+
+insert into extension_bundle_version_dependency (
+ id,
+ extension_bundle_version_id,
+ group_id,
+ artifact_id,
+ version
+) values (
+ 'eb2-v1-dep1',
+ 'eb2-v1',
+ 'org.apache.nifi',
+ 'nifi-example-service-api-nar',
+ '2.0.0'
+);
+
+-- service api bundle
+insert into bucket_item (
+ id,
+ name,
+ description,
+ created,
+ modified,
+ item_type,
+ bucket_id
+) values (
+ 'eb3',
+ 'nifi-example-service-api-nar',
+ 'Example service API bundle',
+ parsedatetime('2018-11-02 12:58:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'),
+ parsedatetime('2017-11-02 12:58:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'),
+ 'EXTENSION_BUNDLE',
+ '3'
+);
+
+insert into extension_bundle (
+ id,
+ bucket_id,
+ bundle_type,
+ group_id,
+ artifact_id
+) values (
+ 'eb3',
+ '3',
+ 'NIFI_NAR',
+ 'org.apache.nifi',
+ 'nifi-example-service-api-nar'
+);
+
+insert into extension_bundle_version (
+ id,
+ extension_bundle_id,
+ version,
+ created,
+ created_by,
+ description,
+ sha_256_hex,
+ sha_256_supplied
+) values (
+ 'eb3-v1',
+ 'eb3',
+ '2.0.0',
+ parsedatetime('2018-11-02 13:00:00.000 UTC', 'yyyy-MM-dd hh:mm:ss.SSS z'),
+ 'user1',
+ 'First version of eb3',
+ '123456789',
+ '1'
+);
+
+-- test data for extensions
+
+insert into extension (
+ id, extension_bundle_version_id, type, type_description, is_restricted, category, tags
+) values (
+ 'e1', 'eb1-v1', 'org.apache.nifi.ExampleProcessor', 'This is Example Processor 1', 0, 'PROCESSOR', 'example, processor'
+);
+
+insert into extension (
+ id, extension_bundle_version_id, type, type_description, is_restricted, category, tags)
+values (
+ 'e2', 'eb1-v1', 'org.apache.nifi.ExampleProcessorRestricted', 'This is Example Processor Restricted', 1, 'PROCESSOR', 'example, processor, restricted'
+);
+
+insert into extension (
+ id, extension_bundle_version_id, type, type_description, is_restricted, category, tags)
+values (
+ 'e3', 'eb2-v1', 'org.apache.nifi.ExampleService', 'This is Example Service', 0, 'CONTROLLER_SERVICE', 'example, service'
+);
+
+-- test data for extension tags
+
+insert into extension_tag (extension_id, tag) values ('e1', 'example');
+insert into extension_tag (extension_id, tag) values ('e1', 'processor');
+
+insert into extension_tag (extension_id, tag) values ('e2', 'example');
+insert into extension_tag (extension_id, tag) values ('e2', 'processor');
+insert into extension_tag (extension_id, tag) values ('e2', 'restricted');
+
+insert into extension_tag (extension_id, tag) values ('e3', 'example');
+insert into extension_tag (extension_id, tag) values ('e3', 'service');
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/hook/bad-script-provider.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/hook/bad-script-provider.xml
index 568e756..1e23386 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/hook/bad-script-provider.xml
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/hook/bad-script-provider.xml
@@ -27,4 +27,10 @@
<property name="Working Directory"></property>
</eventHookProvider>
+ <extensionBundlePersistenceProvider>
+ <class>org.apache.nifi.registry.provider.MockExtensionBundlePersistenceProvider</class>
+ <property name="Extension Property 1">extension foo</property>
+ <property name="Extension Property 2">extension bar</property>
+ </extensionBundlePersistenceProvider>
+
</providers>
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-class-not-found.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-class-not-found.xml
index 9adba54..7031ca9 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-class-not-found.xml
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-class-not-found.xml
@@ -21,4 +21,10 @@
<property name="Flow Property 2">bar</property>
</flowPersistenceProvider>
+ <extensionBundlePersistenceProvider>
+ <class>org.apache.nifi.registry.provider.MockExtensionBundlePersistenceProvider</class>
+ <property name="Extension Property 1">extension foo</property>
+ <property name="Extension Property 2">extension bar</property>
+ </extensionBundlePersistenceProvider>
+
</providers>
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-good.xml b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-good.xml
index 32414e5..fc963f1 100644
--- a/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-good.xml
+++ b/nifi-registry-core/nifi-registry-framework/src/test/resources/provider/providers-good.xml
@@ -21,4 +21,10 @@
<property name="Flow Property 2">flow bar</property>
</flowPersistenceProvider>
+ <extensionBundlePersistenceProvider>
+ <class>org.apache.nifi.registry.provider.MockExtensionBundlePersistenceProvider</class>
+ <property name="Extension Property 1">extension foo</property>
+ <property name="Extension Property 2">extension bar</property>
+ </extensionBundlePersistenceProvider>
+
</providers>
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
index 1dcb0f7..89a8e60 100644
--- a/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
+++ b/nifi-registry-core/nifi-registry-properties/src/main/java/org/apache/nifi/registry/properties/NiFiRegistryProperties.java
@@ -16,16 +16,16 @@
*/
package org.apache.nifi.registry.properties;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.io.File;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
public class NiFiRegistryProperties extends Properties {
private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryProperties.class);
@@ -58,6 +58,8 @@
public static final String PROVIDERS_CONFIGURATION_FILE = "nifi.registry.providers.configuration.file";
+ public static final String EXTENSIONS_WORKING_DIR = "nifi.registry.extensions.working.directory";
+
// Original DB properties
public static final String DATABASE_DIRECTORY = "nifi.registry.db.directory";
public static final String DATABASE_URL_APPEND = "nifi.registry.db.url.append";
@@ -86,6 +88,7 @@
public static final String DEFAULT_SECURITY_AUTHORIZERS_CONFIGURATION_FILE = "./conf/authorizers.xml";
public static final String DEFAULT_SECURITY_IDENTITY_PROVIDER_CONFIGURATION_FILE = "./conf/identity-providers.xml";
public static final String DEFAULT_AUTHENTICATION_EXPIRATION = "12 hours";
+ public static final String DEFAULT_EXTENSIONS_WORKING_DIR = "./work/extensions";
public int getWebThreads() {
int webThreads = 200;
@@ -158,6 +161,10 @@
return new File(getProperty(WEB_WORKING_DIR, DEFAULT_WEB_WORKING_DIR));
}
+ public File getExtensionsWorkingDirectory() {
+ return new File(getProperty(EXTENSIONS_WORKING_DIR, DEFAULT_EXTENSIONS_WORKING_DIR));
+ }
+
public File getProvidersConfigurationFile() {
return getPropertyAsFile(PROVIDERS_CONFIGURATION_FILE, DEFAULT_PROVIDERS_CONFIGURATION_FILE);
}
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleContext.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleContext.java
new file mode 100644
index 0000000..2681849
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleContext.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.extension;
+
+/**
+ * The context that will be passed to the {@link ExtensionBundlePersistenceProvider} when saving a new version of an extension bundle.
+ */
+public interface ExtensionBundleContext {
+
+ enum BundleType {
+ NIFI_NAR,
+ MINIFI_CPP;
+ }
+
+ /**
+ * @return the id of the bucket the bundle belongs to
+ */
+ String getBucketId();
+
+ /**
+ * @return the name of the bucket the bundle belongs to
+ */
+ String getBucketName();
+
+ /**
+ * @return the type of the bundle
+ */
+ BundleType getBundleType();
+
+ /**
+ * @return the NiFi Registry id of the bundle
+ */
+ String getBundleId();
+
+ /**
+ * @return the group id of the bundle
+ */
+ String getBundleGroupId();
+
+ /**
+ * @return the artifact id of the bundle
+ */
+ String getBundleArtifactId();
+
+ /**
+ * @return the version of the bundle
+ */
+ String getBundleVersion();
+
+ /**
+ * @return the comments for the version of the bundle
+ */
+ String getDescription();
+
+ /**
+ * @return the timestamp the bundle was created
+ */
+ long getTimestamp();
+
+ /**
+ * @return the user that created the bundle
+ */
+ String getAuthor();
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundlePersistenceException.java
similarity index 64%
copy from nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
copy to nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundlePersistenceException.java
index ec356fd..4722604 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundlePersistenceException.java
@@ -14,17 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.nifi.registry.web.link.builder;
-
-import javax.ws.rs.core.Link;
+package org.apache.nifi.registry.extension;
/**
- * Creates a Link for a given type.
- *
- * @param <T> the type to create a link for
+ * An Exception for errors encountered when a ExtensionBundlePersistenceProvider saves or retrieves a bundle.
*/
-public interface LinkBuilder<T> {
+public class ExtensionBundlePersistenceException extends RuntimeException {
- Link createLink(T t);
+ public ExtensionBundlePersistenceException(String message) {
+ super(message);
+ }
+
+ public ExtensionBundlePersistenceException(String message, Throwable cause) {
+ super(message, cause);
+ }
}
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundlePersistenceProvider.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundlePersistenceProvider.java
new file mode 100644
index 0000000..9ef2646
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/extension/ExtensionBundlePersistenceProvider.java
@@ -0,0 +1,67 @@
+/*
+ * 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.extension;
+
+import org.apache.nifi.registry.provider.Provider;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Responsible for storing and retrieving the binary content of a version of an extension bundle.
+ */
+public interface ExtensionBundlePersistenceProvider extends Provider {
+
+ /**
+ * Persists the binary content of a version of an extension bundle.
+ *
+ * @param context the context about the bundle version being persisted
+ * @param contentStream the stream of binary content to persist
+ * @throws ExtensionBundlePersistenceException if an error occurs storing the content
+ */
+ void saveBundleVersion(ExtensionBundleContext context, InputStream contentStream) throws ExtensionBundlePersistenceException;
+
+ /**
+ * Writes the binary content of the bundle specified by the bucket-group-artifact-version to the provided OutputStream.
+ *
+ * @param context the context about the bundle version being retrieved
+ * @param outputStream the output stream to write the contents to
+ * @throws ExtensionBundlePersistenceException if an error occurs retrieving the content
+ */
+ void getBundleVersion(ExtensionBundleContext context, OutputStream outputStream) throws ExtensionBundlePersistenceException;
+
+ /**
+ * Deletes the content of the bundle version specified by bucket-group-artifact-version.
+ *
+ * @param context the context about the bundle version being deleted
+ * @throws ExtensionBundlePersistenceException if an error occurs deleting the content
+ */
+ void deleteBundleVersion(ExtensionBundleContext context) throws ExtensionBundlePersistenceException;
+
+ /**
+ * Deletes the content for all versions of the bundle specified by bucket-group-artifact.
+ *
+ * @param bucketId the id of the bucket where the bundle is located
+ * @param bucketName the bucket name where the bundle is located
+ * @param groupId the group id of the bundle
+ * @param artifactId the artifact id of the bundle
+ * @throws ExtensionBundlePersistenceException if an error occurs deleting the content
+ */
+ void deleteAllBundleVersions(String bucketId, String bucketName, String groupId, String artifactId)
+ throws ExtensionBundlePersistenceException;
+
+}
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventFieldName.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventFieldName.java
index 35b0cfe..3f2fa6e 100644
--- a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventFieldName.java
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventFieldName.java
@@ -23,6 +23,7 @@
BUCKET_ID,
FLOW_ID,
+ EXTENSION_BUNDLE_ID,
VERSION,
USER,
COMMENT;
diff --git a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventType.java b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventType.java
index c11a60c..0af35dc 100644
--- a/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventType.java
+++ b/nifi-registry-core/nifi-registry-provider-api/src/main/java/org/apache/nifi/registry/hook/EventType.java
@@ -41,6 +41,17 @@
EventFieldName.VERSION,
EventFieldName.USER,
EventFieldName.COMMENT),
+ CREATE_EXTENSION_BUNDLE(
+ EventFieldName.BUCKET_ID,
+ EventFieldName.EXTENSION_BUNDLE_ID,
+ EventFieldName.USER
+ ),
+ CREATE_EXTENSION_BUNDLE_VERSION(
+ EventFieldName.BUCKET_ID,
+ EventFieldName.EXTENSION_BUNDLE_ID,
+ EventFieldName.VERSION,
+ EventFieldName.USER
+ ),
REGISTRY_START(),
UPDATE_BUCKET(
EventFieldName.BUCKET_ID,
@@ -55,7 +66,19 @@
DELETE_FLOW(
EventFieldName.BUCKET_ID,
EventFieldName.FLOW_ID,
- EventFieldName.USER);
+ EventFieldName.USER),
+ DELETE_EXTENSION_BUNDLE(
+ EventFieldName.BUCKET_ID,
+ EventFieldName.EXTENSION_BUNDLE_ID,
+ EventFieldName.USER
+ ),
+ DELETE_EXTENSION_BUNDLE_VERSION(
+ EventFieldName.BUCKET_ID,
+ EventFieldName.EXTENSION_BUNDLE_ID,
+ EventFieldName.VERSION,
+ EventFieldName.USER
+ )
+ ;
private List<EventFieldName> fieldNames;
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
index fb77a07..ce4377f 100644
--- a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
@@ -42,6 +42,9 @@
# providers properties #
nifi.registry.providers.configuration.file=${nifi.registry.providers.configuration.file}
+# extensions working dir #
+nifi.registry.extensions.working.directory=${nifi.registry.extensions.working.directory}
+
# legacy database properties, used to migrate data from original DB to new DB below
# NOTE: Users upgrading from 0.1.0 should leave these populated, but new installs after 0.1.0 should leave these empty
nifi.registry.db.directory=${nifi.registry.db.directory}
diff --git a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/providers.xml b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/providers.xml
index faf8d4f..306c073 100644
--- a/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/providers.xml
+++ b/nifi-registry-core/nifi-registry-resources/src/main/resources/conf/providers.xml
@@ -51,4 +51,9 @@
</eventHookProvider>
-->
+ <extensionBundlePersistenceProvider>
+ <class>org.apache.nifi.registry.provider.extension.FileSystemExtensionBundlePersistenceProvider</class>
+ <property name="Extension Bundle Storage Directory">./extension_bundles</property>
+ </extensionBundlePersistenceProvider>
+
</providers>
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry-core/nifi-registry-web-api/pom.xml
index e0af632..6e9fa10 100644
--- a/nifi-registry-core/nifi-registry-web-api/pom.xml
+++ b/nifi-registry-core/nifi-registry-web-api/pom.xml
@@ -231,6 +231,13 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
<version>${spring.boot.version}</version>
+ <exclusions>
+ <!-- spring-boot-starter-jersey brings in a Spring 4.x version of spring-aop which causes problems -->
+ <exclusion>
+ <groupId>org.springframework</groupId>
+ <artifactId>spring-aop</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
<!-- Exclude micrometer-core because it creates a class cast issue with logback, revisit later -->
<dependency>
@@ -308,6 +315,10 @@
<artifactId>swagger-annotations</artifactId>
</dependency>
<dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-multipart</artifactId>
+ </dependency>
+ <dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java
index d06555d..2ffefbb 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java
@@ -54,6 +54,13 @@
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
final Properties defaultProperties = new Properties();
+ // Spring Boot 2.1.0 disabled bean overriding so this re-enables it
+ defaultProperties.setProperty("spring.main.allow-bean-definition-overriding", "true");
+
+ // Disable unnecessary Spring MVC filters that cause problems with Jersey
+ defaultProperties.setProperty("spring.mvc.hiddenmethod.filter.enabled", "false");
+ defaultProperties.setProperty("spring.mvc.formcontent.filter.enabled", "false");
+
// Enable Actuator Endpoints
defaultProperties.setProperty("management.endpoints.web.expose", "*");
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
index a5ab5ef..7394e8c 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
@@ -18,12 +18,16 @@
import org.apache.nifi.registry.web.api.AccessPolicyResource;
import org.apache.nifi.registry.web.api.AccessResource;
+import org.apache.nifi.registry.web.api.BucketExtensionResource;
import org.apache.nifi.registry.web.api.BucketFlowResource;
import org.apache.nifi.registry.web.api.BucketResource;
import org.apache.nifi.registry.web.api.ConfigResource;
+import org.apache.nifi.registry.web.api.ExtensionRepositoryResource;
+import org.apache.nifi.registry.web.api.ExtensionResource;
import org.apache.nifi.registry.web.api.FlowResource;
import org.apache.nifi.registry.web.api.ItemResource;
import org.apache.nifi.registry.web.api.TenantResource;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.filter.HttpMethodOverrideFilter;
@@ -57,11 +61,17 @@
register(AccessResource.class);
register(BucketResource.class);
register(BucketFlowResource.class);
+ register(BucketExtensionResource.class);
+ register(ExtensionResource.class);
+ register(ExtensionRepositoryResource.class);
register(FlowResource.class);
register(ItemResource.class);
register(TenantResource.class);
register(ConfigResource.class);
+ // register multipart feature
+ register(MultiPartFeature.class);
+
// include bean validation errors in response
property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
index 776a693..22c5211 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ApplicationResource.java
@@ -71,9 +71,8 @@
}
}
- protected String generateResourceUri(final String... path) {
+ protected URI getBaseUri() {
final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder();
- uriBuilder.segment(path);
URI uri = uriBuilder.build();
try {
@@ -126,7 +125,13 @@
} catch (final URISyntaxException use) {
throw new UriBuilderException(use);
}
- return uri.toString();
+ return uri;
+ }
+
+ protected String generateResourceUri(final String... path) {
+ final URI baseUri = getBaseUri();
+ final URI fullUri = UriBuilder.fromUri(baseUri).segment(path).build();
+ return fullUri.toString();
}
/**
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketExtensionResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketExtensionResource.java
new file mode 100644
index 0000000..4477acd
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketExtensionResource.java
@@ -0,0 +1,179 @@
+/*
+ * 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.web.api;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
+import io.swagger.annotations.Extension;
+import io.swagger.annotations.ExtensionProperty;
+import org.apache.nifi.registry.event.EventFactory;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleType;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.service.RegistryService;
+import org.apache.nifi.registry.web.link.LinkService;
+import org.apache.nifi.registry.web.security.PermissionsService;
+import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
+import org.glassfish.jersey.media.multipart.FormDataParam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+@Component
+@Path("/buckets/{bucketId}/extensions")
+@Api(
+ value = "bucket_extensions",
+ description = "Create extension bundles scoped to an existing bucket in the registry.",
+ authorizations = { @Authorization("Authorization") }
+)
+public class BucketExtensionResource extends AuthorizableApplicationResource {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(BucketExtensionResource.class);
+
+ private final RegistryService registryService;
+ private final LinkService linkService;
+ private final PermissionsService permissionsService;
+
+ @Autowired
+ public BucketExtensionResource(
+ final RegistryService registryService,
+ final LinkService linkService,
+ final PermissionsService permissionsService,
+ final AuthorizationService authorizationService,
+ final EventService eventService) {
+ super(authorizationService, eventService);
+ this.registryService = registryService;
+ this.linkService = linkService;
+ this.permissionsService =permissionsService;
+ }
+
+ @POST
+ @Path("bundles/{bundleType}")
+ @Consumes(MediaType.MULTIPART_FORM_DATA)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Creates a version of an extension bundle by uploading a binary artifact",
+ notes = "If an extension bundle already exists in the given bucket with the same group id and artifact id " +
+ "as that of the bundle being uploaded, then it will be added as a new version to the existing bundle. " +
+ "If an extension bundle does not already exist in the given bucket with the same group id and artifact id, " +
+ "then a new extension bundle will be created and this version will be added to the new bundle. " +
+ "Client's may optionally supply a SHA-256 in hex format through the multi-part form field 'sha256'. " +
+ "If supplied, then this value will be compared against the SHA-256 computed by the server, and the bundle " +
+ "will be rejected if the values do not match. If not supplied, the bundle will be accepted, but will be marked " +
+ "to indicate that the client did not supply a SHA-256 during creation.",
+ response = ExtensionBundleVersion.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "write"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response createExtensionBundleVersion(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId,
+ @PathParam("bundleType")
+ @ApiParam("The type of the bundle")
+ final String bundleType,
+ @FormDataParam("file")
+ final InputStream fileInputStream,
+ @FormDataParam("file")
+ final FormDataContentDisposition fileMetaData,
+ @FormDataParam("sha256")
+ final String clientSha256) throws IOException {
+
+ authorizeBucketAccess(RequestAction.WRITE, bucketId);
+
+ final ExtensionBundleType extensionBundleType = ExtensionBundleType.fromString(bundleType);
+ LOGGER.debug("Creating extension bundle version for bundle type {}", new Object[]{extensionBundleType});
+
+ final ExtensionBundleVersion createdBundleVersion = registryService.createExtensionBundleVersion(
+ bucketId, extensionBundleType, fileInputStream, clientSha256);
+
+ publish(EventFactory.extensionBundleCreated(createdBundleVersion.getExtensionBundle()));
+ publish(EventFactory.extensionBundleVersionCreated(createdBundleVersion));
+
+ linkService.populateLinks(createdBundleVersion.getVersionMetadata());
+ linkService.populateLinks(createdBundleVersion.getExtensionBundle());
+ linkService.populateLinks(createdBundleVersion.getBucket());
+
+ permissionsService.populateItemPermissions(createdBundleVersion.getExtensionBundle());
+
+ return Response.status(Response.Status.OK).entity(createdBundleVersion).build();
+ }
+
+ @GET
+ @Path("bundles")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets all extension bundles in the given bucket",
+ response = ExtensionBundle.class,
+ responseContainer = "List",
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionBundles(
+ @PathParam("bucketId")
+ @ApiParam("The bucket identifier")
+ final String bucketId
+ ) {
+ authorizeBucketAccess(RequestAction.READ, bucketId);
+
+ final List<ExtensionBundle> bundles = registryService.getExtensionBundlesByBucket(bucketId);
+ permissionsService.populateItemPermissions(bundles);
+ linkService.populateLinks(bundles);
+
+ return Response.status(Response.Status.OK).entity(bundles).build();
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
index 942a3d4..ccf41d9 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketFlowResource.java
@@ -119,7 +119,7 @@
publish(EventFactory.flowCreated(createdFlow));
permissionsService.populateItemPermissions(createdFlow);
- linkService.populateFlowLinks(createdFlow);
+ linkService.populateLinks(createdFlow);
return Response.status(Response.Status.OK).entity(createdFlow).build();
}
@@ -151,7 +151,7 @@
final List<VersionedFlow> flows = registryService.getFlows(bucketId);
permissionsService.populateItemPermissions(flows);
- linkService.populateFlowLinks(flows);
+ linkService.populateLinks(flows);
return Response.status(Response.Status.OK).entity(flows).build();
}
@@ -187,7 +187,7 @@
final VersionedFlow flow = registryService.getFlow(bucketId, flowId);
permissionsService.populateItemPermissions(flow);
- linkService.populateFlowLinks(flow);
+ linkService.populateLinks(flow);
return Response.status(Response.Status.OK).entity(flow).build();
}
@@ -230,7 +230,7 @@
final VersionedFlow updatedFlow = registryService.updateFlow(flow);
publish(EventFactory.flowUpdated(updatedFlow));
permissionsService.populateItemPermissions(updatedFlow);
- linkService.populateFlowLinks(updatedFlow);
+ linkService.populateLinks(updatedFlow);
return Response.status(Response.Status.OK).entity(updatedFlow).build();
}
@@ -311,11 +311,11 @@
publish(EventFactory.flowVersionCreated(createdSnapshot));
if (createdSnapshot.getSnapshotMetadata() != null) {
- linkService.populateSnapshotLinks(createdSnapshot.getSnapshotMetadata());
+ linkService.populateLinks(createdSnapshot.getSnapshotMetadata());
}
if (createdSnapshot.getBucket() != null) {
permissionsService.populateBucketPermissions(createdSnapshot.getBucket());
- linkService.populateBucketLinks(createdSnapshot.getBucket());
+ linkService.populateLinks(createdSnapshot.getBucket());
}
return Response.status(Response.Status.OK).entity(createdSnapshot).build();
}
@@ -351,7 +351,7 @@
final SortedSet<VersionedFlowSnapshotMetadata> snapshots = registryService.getFlowSnapshots(bucketId, flowId);
if (snapshots != null ) {
- linkService.populateSnapshotLinks(snapshots);
+ linkService.populateLinks(snapshots);
}
return Response.status(Response.Status.OK).entity(snapshots).build();
@@ -421,7 +421,7 @@
authorizeBucketAccess(RequestAction.READ, bucketId);
final VersionedFlowSnapshotMetadata latest = registryService.getLatestFlowSnapshotMetadata(bucketId, flowId);
- linkService.populateSnapshotLinks(latest);
+ linkService.populateLinks(latest);
return Response.status(Response.Status.OK).entity(latest).build();
}
@@ -502,16 +502,16 @@
private void populateLinksAndPermissions(VersionedFlowSnapshot snapshot) {
if (snapshot.getSnapshotMetadata() != null) {
- linkService.populateSnapshotLinks(snapshot.getSnapshotMetadata());
+ linkService.populateLinks(snapshot.getSnapshotMetadata());
}
if (snapshot.getFlow() != null) {
- linkService.populateFlowLinks(snapshot.getFlow());
+ linkService.populateLinks(snapshot.getFlow());
}
if (snapshot.getBucket() != null) {
permissionsService.populateBucketPermissions(snapshot.getBucket());
- linkService.populateBucketLinks(snapshot.getBucket());
+ linkService.populateLinks(snapshot.getBucket());
}
}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
index e905973..e7c0df4 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/BucketResource.java
@@ -118,7 +118,7 @@
publish(EventFactory.bucketCreated(createdBucket));
permissionsService.populateBucketPermissions(createdBucket);
- linkService.populateBucketLinks(createdBucket);
+ linkService.populateLinks(createdBucket);
return Response.status(Response.Status.OK).entity(createdBucket).build();
}
@@ -152,7 +152,7 @@
final List<Bucket> buckets = registryService.getBuckets(authorizedBucketIds);
permissionsService.populateBucketPermissions(buckets);
- linkService.populateBucketLinks(buckets);
+ linkService.populateLinks(buckets);
return Response.status(Response.Status.OK).entity(buckets).build();
}
@@ -182,7 +182,7 @@
authorizeBucketAccess(RequestAction.READ, bucketId);
final Bucket bucket = registryService.getBucket(bucketId);
permissionsService.populateBucketPermissions(bucket);
- linkService.populateBucketLinks(bucket);
+ linkService.populateLinks(bucket);
return Response.status(Response.Status.OK).entity(bucket).build();
}
@@ -233,7 +233,7 @@
publish(EventFactory.bucketUpdated(updatedBucket));
permissionsService.populateBucketPermissions(updatedBucket);
- linkService.populateBucketLinks(updatedBucket);
+ linkService.populateLinks(updatedBucket);
return Response.status(Response.Status.OK).entity(updatedBucket).build();
}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepositoryResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepositoryResource.java
new file mode 100644
index 0000000..46be907
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionRepositoryResource.java
@@ -0,0 +1,374 @@
+/*
+ * 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.web.api;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
+import io.swagger.annotations.Extension;
+import io.swagger.annotations.ExtensionProperty;
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.service.RegistryService;
+import org.apache.nifi.registry.service.extension.ExtensionBundleVersionCoordinate;
+import org.apache.nifi.registry.web.link.LinkService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Link;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.StreamingOutput;
+import java.util.ArrayList;
+import java.util.Set;
+import java.util.SortedSet;
+
+@Component
+@Path("/extensions/repo")
+@Api(
+ value = "extension_repository",
+ description = "Interact with extension bundles via the hierarchy of bucket/group/artifact/version.",
+ authorizations = { @Authorization("Authorization") }
+)
+public class ExtensionRepositoryResource extends AuthorizableApplicationResource {
+
+ public static final String CONTENT_DISPOSITION_HEADER = "content-disposition";
+ private final RegistryService registryService;
+ private final LinkService linkService;
+
+ @Autowired
+ public ExtensionRepositoryResource(
+ final RegistryService registryService,
+ final LinkService linkService,
+ final AuthorizationService authorizationService,
+ final EventService eventService) {
+ super(authorizationService, eventService);
+ this.registryService = registryService;
+ this.linkService = linkService;
+ }
+
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets the names of the buckets the current user is authorized for in order to browse the repo by bucket",
+ response = ExtensionRepoBucket.class,
+ responseContainer = "List"
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionRepoBuckets() {
+
+ final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
+ if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
+ // not authorized for any bucket, return empty list of items
+ return Response.status(Response.Status.OK).entity(new ArrayList<BucketItem>()).build();
+ }
+
+ final SortedSet<ExtensionRepoBucket> repoBuckets = registryService.getExtensionRepoBuckets(authorizedBucketIds);
+ linkService.populateFullLinks(repoBuckets, getBaseUri());
+ return Response.status(Response.Status.OK).entity(repoBuckets).build();
+ }
+
+ @GET
+ @Path("{bucketName}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets the groups in the extension repository in the bucket with the given name",
+ response = ExtensionRepoGroup.class,
+ responseContainer = "List",
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionRepoGroups(
+ @PathParam("bucketName")
+ @ApiParam("The bucket name")
+ final String bucketName
+ ) {
+ final Bucket bucket = registryService.getBucketByName(bucketName);
+ authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+ final SortedSet<ExtensionRepoGroup> repoGroups = registryService.getExtensionRepoGroups(bucket);
+ linkService.populateFullLinks(repoGroups, getBaseUri());
+ return Response.status(Response.Status.OK).entity(repoGroups).build();
+ }
+
+ @GET
+ @Path("{bucketName}/{groupId}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets the artifacts in the extension repository with the given group in the bucket with the given name",
+ response = ExtensionRepoArtifact.class,
+ responseContainer = "List",
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionRepoArtifacts(
+ @PathParam("bucketName")
+ @ApiParam("The bucket name")
+ final String bucketName,
+ @PathParam("groupId")
+ @ApiParam("The group id")
+ final String groupId
+ ) {
+ final Bucket bucket = registryService.getBucketByName(bucketName);
+ authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+ final SortedSet<ExtensionRepoArtifact> repoArtifacts = registryService.getExtensionRepoArtifacts(bucket, groupId);
+ linkService.populateFullLinks(repoArtifacts, getBaseUri());
+ return Response.status(Response.Status.OK).entity(repoArtifacts).build();
+ }
+
+ @GET
+ @Path("{bucketName}/{groupId}/{artifactId}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets the versions of the artifact in the extension repository specified by the given bucket, group, artifact, and version",
+ response = ExtensionRepoVersionSummary.class,
+ responseContainer = "List",
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionBundleVersions(
+ @PathParam("bucketName")
+ @ApiParam("The bucket name")
+ final String bucketName,
+ @PathParam("groupId")
+ @ApiParam("The group identifier")
+ final String groupId,
+ @PathParam("artifactId")
+ @ApiParam("The artifact identifier")
+ final String artifactId
+ ) {
+ final Bucket bucket = registryService.getBucketByName(bucketName);
+ authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+ final SortedSet<ExtensionRepoVersionSummary> repoVersions = registryService.getExtensionRepoVersions(bucket, groupId, artifactId);
+ linkService.populateFullLinks(repoVersions, getBaseUri());
+ return Response.status(Response.Status.OK).entity(repoVersions).build();
+ }
+
+ @GET
+ @Path("{bucketName}/{groupId}/{artifactId}/{version}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets the information about the version specified by the given bucket, group, artifact, and version",
+ response = ExtensionRepoVersion.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionBundleVersion(
+ @PathParam("bucketName")
+ @ApiParam("The bucket name")
+ final String bucketName,
+ @PathParam("groupId")
+ @ApiParam("The group identifier")
+ final String groupId,
+ @PathParam("artifactId")
+ @ApiParam("The artifact identifier")
+ final String artifactId,
+ @PathParam("version")
+ @ApiParam("The version")
+ final String version
+ ) {
+ final Bucket bucket = registryService.getBucketByName(bucketName);
+ authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+ final ExtensionBundleVersionCoordinate versionCoordinate = new ExtensionBundleVersionCoordinate(
+ bucket.getIdentifier(), groupId, artifactId, version);
+
+ final ExtensionBundleVersion bundleVersion = registryService.getExtensionBundleVersion(versionCoordinate);
+
+ final String downloadUri = generateResourceUri(
+ "extensions", "repo",
+ bundleVersion.getBucket().getName(),
+ bundleVersion.getExtensionBundle().getGroupId(),
+ bundleVersion.getExtensionBundle().getArtifactId(),
+ bundleVersion.getVersionMetadata().getVersion(),
+ "content");
+
+ final String sha256Uri = generateResourceUri(
+ "extensions", "repo",
+ bundleVersion.getBucket().getName(),
+ bundleVersion.getExtensionBundle().getGroupId(),
+ bundleVersion.getExtensionBundle().getArtifactId(),
+ bundleVersion.getVersionMetadata().getVersion(),
+ "sha256");
+
+ final ExtensionRepoVersion repoVersion = new ExtensionRepoVersion();
+ repoVersion.setDownloadLink(Link.fromUri(downloadUri).rel("content").build());
+ repoVersion.setSha256Link(Link.fromUri(sha256Uri).rel("sha256").build());
+ repoVersion.setSha256Supplied(bundleVersion.getVersionMetadata().getSha256Supplied());
+
+ return Response.ok(repoVersion).build();
+ }
+
+ @GET
+ @Path("{bucketName}/{groupId}/{artifactId}/{version}/content")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_OCTET_STREAM)
+ @ApiOperation(
+ value = "Gets the binary content of the extension bundle specified by the given bucket, group, artifact, and version",
+ response = byte[].class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionBundleVersionContent(
+ @PathParam("bucketName")
+ @ApiParam("The bucket name")
+ final String bucketName,
+ @PathParam("groupId")
+ @ApiParam("The group identifier")
+ final String groupId,
+ @PathParam("artifactId")
+ @ApiParam("The artifact identifier")
+ final String artifactId,
+ @PathParam("version")
+ @ApiParam("The version")
+ final String version
+ ) {
+ final Bucket bucket = registryService.getBucketByName(bucketName);
+ authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+ final ExtensionBundleVersionCoordinate versionCoordinate = new ExtensionBundleVersionCoordinate(
+ bucket.getIdentifier(), groupId, artifactId, version);
+
+ final ExtensionBundleVersion bundleVersion = registryService.getExtensionBundleVersion(versionCoordinate);
+ final StreamingOutput streamingOutput = (output) -> registryService.writeExtensionBundleVersionContent(bundleVersion, output);
+
+ return Response.ok(streamingOutput)
+ .header(CONTENT_DISPOSITION_HEADER,"attachment; filename = " + bundleVersion.getFilename())
+ .build();
+ }
+
+ @GET
+ @Path("{bucketName}/{groupId}/{artifactId}/{version}/sha256")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.TEXT_PLAIN)
+ @ApiOperation(
+ value = "Gets the hex representation of the SHA-256 digest for the binary content of the version of the extension bundle",
+ response = String.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionBundleVersionSha256(
+ @PathParam("bucketName")
+ @ApiParam("The bucket name")
+ final String bucketName,
+ @PathParam("groupId")
+ @ApiParam("The group identifier")
+ final String groupId,
+ @PathParam("artifactId")
+ @ApiParam("The artifact identifier")
+ final String artifactId,
+ @PathParam("version")
+ @ApiParam("The version")
+ final String version
+ ) {
+ final Bucket bucket = registryService.getBucketByName(bucketName);
+ authorizeBucketAccess(RequestAction.READ, bucket.getIdentifier());
+
+ final ExtensionBundleVersionCoordinate versionCoordinate = new ExtensionBundleVersionCoordinate(
+ bucket.getIdentifier(), groupId, artifactId, version);
+
+ final ExtensionBundleVersion bundleVersion = registryService.getExtensionBundleVersion(versionCoordinate);
+ final String sha256Hex = bundleVersion.getVersionMetadata().getSha256();
+
+ return Response.ok(sha256Hex, MediaType.TEXT_PLAIN).build();
+ }
+
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java
new file mode 100644
index 0000000..62c4c08
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ExtensionResource.java
@@ -0,0 +1,366 @@
+/*
+ * 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.web.api;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
+import io.swagger.annotations.Extension;
+import io.swagger.annotations.ExtensionProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.event.EventFactory;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.apache.nifi.registry.service.RegistryService;
+import org.apache.nifi.registry.service.extension.ExtensionBundleVersionCoordinate;
+import org.apache.nifi.registry.web.link.LinkService;
+import org.apache.nifi.registry.web.security.PermissionsService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.StreamingOutput;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+
+@Component
+@Path("/extensions")
+@Api(
+ value = "extensions",
+ description = "Gets metadata about extension bundles and extensions.",
+ authorizations = { @Authorization("Authorization") }
+)
+public class ExtensionResource extends AuthorizableApplicationResource {
+
+ public static final String CONTENT_DISPOSITION_HEADER = "content-disposition";
+ private final RegistryService registryService;
+ private final LinkService linkService;
+ private final PermissionsService permissionsService;
+
+ @Autowired
+ public ExtensionResource(final RegistryService registryService,
+ final LinkService linkService,
+ final PermissionsService permissionsService,
+ final AuthorizationService authorizationService,
+ final EventService eventService) {
+ super(authorizationService, eventService);
+ this.registryService = registryService;
+ this.linkService = linkService;
+ this.permissionsService = permissionsService;
+ }
+
+ @GET
+ @Path("bundles")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Get extension bundles across all authorized buckets",
+ notes = "The returned items will include only items from buckets for which the user is authorized. " +
+ "If the user is not authorized to any buckets, an empty list will be returned.",
+ response = ExtensionBundle.class,
+ responseContainer = "List"
+ )
+ @ApiResponses({ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401) })
+ public Response getExtensionBundles() {
+
+ final Set<String> authorizedBucketIds = getAuthorizedBucketIds(RequestAction.READ);
+ if (authorizedBucketIds == null || authorizedBucketIds.isEmpty()) {
+ // not authorized for any bucket, return empty list of items
+ return Response.status(Response.Status.OK).entity(new ArrayList<BucketItem>()).build();
+ }
+
+ List<ExtensionBundle> bundles = registryService.getExtensionBundles(authorizedBucketIds);
+ if (bundles == null) {
+ bundles = Collections.emptyList();
+ }
+ permissionsService.populateItemPermissions(bundles);
+ linkService.populateLinks(bundles);
+
+ return Response.status(Response.Status.OK).entity(bundles).build();
+ }
+
+ @GET
+ @Path("bundles/{bundleId}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets the metadata about an extension bundle",
+ nickname = "globalGetExtensionBundle",
+ response = ExtensionBundle.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionBundle(
+ @PathParam("bundleId")
+ @ApiParam("The extension bundle identifier")
+ final String bundleId) {
+
+ final ExtensionBundle extensionBundle = getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+ permissionsService.populateItemPermissions(extensionBundle);
+ linkService.populateLinks(extensionBundle);
+
+ return Response.status(Response.Status.OK).entity(extensionBundle).build();
+ }
+
+ @DELETE
+ @Path("bundles/{bundleId}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Deletes the given extension bundle and all of it's versions",
+ nickname = "globalDeleteExtensionBundle",
+ response = ExtensionBundle.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "write"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response deleteExtensionBundle(
+ @PathParam("bundleId")
+ @ApiParam("The extension bundle identifier")
+ final String bundleId) {
+
+ final ExtensionBundle extensionBundle = getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+ final ExtensionBundle deletedExtensionBundle = registryService.deleteExtensionBundle(extensionBundle);
+ publish(EventFactory.extensionBundleDeleted(deletedExtensionBundle));
+
+ permissionsService.populateItemPermissions(deletedExtensionBundle);
+ linkService.populateLinks(deletedExtensionBundle);
+
+ return Response.status(Response.Status.OK).entity(deletedExtensionBundle).build();
+ }
+
+ @GET
+ @Path("bundles/{bundleId}/versions")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets the metadata about the versions of an extension bundle",
+ nickname = "globalGetExtensionBundleVersions",
+ response = ExtensionBundleVersionMetadata.class,
+ responseContainer = "List",
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionBundleVersions(
+ @PathParam("bundleId")
+ @ApiParam("The extension bundle identifier")
+ final String bundleId) {
+
+ final ExtensionBundle extensionBundle = getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+ final SortedSet<ExtensionBundleVersionMetadata> bundleVersions = registryService.getExtensionBundleVersions(extensionBundle.getIdentifier());
+ linkService.populateLinks(bundleVersions);
+
+ return Response.status(Response.Status.OK).entity(bundleVersions).build();
+ }
+
+ @GET
+ @Path("bundles/{bundleId}/versions/{version}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Gets the descriptor for the specified version of the extension bundle",
+ nickname = "globalGetExtensionBundleVersionDescriptor",
+ response = ExtensionBundleVersion.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionBundleVersion(
+ @PathParam("bundleId")
+ @ApiParam("The extension bundle identifier")
+ final String bundleId,
+ @PathParam("version")
+ @ApiParam("The version of the bundle")
+ final String version) {
+
+ final ExtensionBundle extensionBundle = getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+ final ExtensionBundleVersionCoordinate versionCoordinate = new ExtensionBundleVersionCoordinate(
+ extensionBundle.getBucketIdentifier(),
+ extensionBundle.getGroupId(),
+ extensionBundle.getArtifactId(),
+ version);
+
+ final ExtensionBundleVersion bundleVersion = registryService.getExtensionBundleVersion(versionCoordinate);
+ linkService.populateLinks(bundleVersion);
+
+ return Response.ok(bundleVersion).build();
+ }
+
+ @GET
+ @Path("bundles/{bundleId}/versions/{version}/content")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_OCTET_STREAM)
+ @ApiOperation(
+ value = "Gets the binary content for the specified version of the extension bundle",
+ nickname = "globalGetExtensionBundleVersion",
+ response = byte[].class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "read"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response getExtensionBundleVersionContent(
+ @PathParam("bundleId")
+ @ApiParam("The extension bundle identifier")
+ final String bundleId,
+ @PathParam("version")
+ @ApiParam("The version of the bundle")
+ final String version) {
+
+ final ExtensionBundle extensionBundle = getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+ final ExtensionBundleVersionCoordinate versionCoordinate = new ExtensionBundleVersionCoordinate(
+ extensionBundle.getBucketIdentifier(),
+ extensionBundle.getGroupId(),
+ extensionBundle.getArtifactId(),
+ version);
+
+ final ExtensionBundleVersion bundleVersion = registryService.getExtensionBundleVersion(versionCoordinate);
+ final StreamingOutput streamingOutput = (output) -> registryService.writeExtensionBundleVersionContent(bundleVersion, output);
+
+ return Response.ok(streamingOutput)
+ .header(CONTENT_DISPOSITION_HEADER,"attachment; filename = " + bundleVersion.getFilename())
+ .build();
+ }
+
+ @DELETE
+ @Path("bundles/{bundleId}/versions/{version}")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @ApiOperation(
+ value = "Deletes the given extension bundle version",
+ nickname = "globalDeleteExtensionBundleVersion",
+ response = ExtensionBundleVersion.class,
+ extensions = {
+ @Extension(name = "access-policy", properties = {
+ @ExtensionProperty(name = "action", value = "write"),
+ @ExtensionProperty(name = "resource", value = "/buckets/{bucketId}") })
+ }
+ )
+ @ApiResponses({
+ @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+ @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+ @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+ @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+ @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+ public Response deleteExtensionBundleVersion(
+ @PathParam("bundleId")
+ @ApiParam("The extension bundle identifier")
+ final String bundleId,
+ @PathParam("version")
+ @ApiParam("The version of the bundle")
+ final String version) {
+
+ final ExtensionBundle extensionBundle = getExtensionBundleWithBucketReadAuthorization(bundleId);
+
+ final ExtensionBundleVersionCoordinate versionCoordinate = new ExtensionBundleVersionCoordinate(
+ extensionBundle.getBucketIdentifier(),
+ extensionBundle.getGroupId(),
+ extensionBundle.getArtifactId(),
+ version);
+
+ final ExtensionBundleVersion bundleVersion = registryService.getExtensionBundleVersion(versionCoordinate);
+
+ final ExtensionBundleVersion deletedBundleVersion = registryService.deleteExtensionBundleVersion(bundleVersion);
+ publish(EventFactory.extensionBundleVersionDeleted(deletedBundleVersion));
+ linkService.populateLinks(deletedBundleVersion);
+
+ return Response.status(Response.Status.OK).entity(deletedBundleVersion).build();
+ }
+
+ /**
+ * Retrieves the extension bundle with the given id and ensures the current user has authorization to read the bucket it belongs to.
+ *
+ * @param bundleId the bundle id
+ * @return the extension bundle
+ */
+ private ExtensionBundle getExtensionBundleWithBucketReadAuthorization(final String bundleId) {
+ final ExtensionBundle extensionBundle = registryService.getExtensionBundle(bundleId);
+
+ // this should never happen, but if somehow the back-end didn't populate the bucket id let's make sure the flow isn't returned
+ if (StringUtils.isBlank(extensionBundle.getBucketIdentifier())) {
+ throw new IllegalStateException("Unable to authorize access because bucket identifier is null or blank");
+ }
+
+ authorizeBucketAccess(RequestAction.READ, extensionBundle.getBucketIdentifier());
+ return extensionBundle;
+ }
+
+}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
index afb8e11..9a1fe55 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/FlowResource.java
@@ -123,7 +123,7 @@
authorizeBucketAccess(RequestAction.READ, flow.getBucketIdentifier());
permissionsService.populateItemPermissions(flow);
- linkService.populateFlowLinks(flow);
+ linkService.populateLinks(flow);
return Response.status(Response.Status.OK).entity(flow).build();
}
@@ -164,7 +164,7 @@
final SortedSet<VersionedFlowSnapshotMetadata> snapshots = registryService.getFlowSnapshots(bucketId, flowId);
if (snapshots != null ) {
- linkService.populateSnapshotLinks(snapshots);
+ linkService.populateLinks(snapshots);
}
return Response.status(Response.Status.OK).entity(snapshots).build();
@@ -284,7 +284,7 @@
authorizeBucketAccess(RequestAction.READ, bucketId);
- linkService.populateSnapshotLinks(latestMetadata);
+ linkService.populateLinks(latestMetadata);
return Response.status(Response.Status.OK).entity(latestMetadata).build();
}
@@ -299,16 +299,16 @@
private void populateLinksAndPermissions(VersionedFlowSnapshot snapshot) {
if (snapshot.getSnapshotMetadata() != null) {
- linkService.populateSnapshotLinks(snapshot.getSnapshotMetadata());
+ linkService.populateLinks(snapshot.getSnapshotMetadata());
}
if (snapshot.getFlow() != null) {
- linkService.populateFlowLinks(snapshot.getFlow());
+ linkService.populateLinks(snapshot.getFlow());
}
if (snapshot.getBucket() != null) {
permissionsService.populateBucketPermissions(snapshot.getBucket());
- linkService.populateBucketLinks(snapshot.getBucket());
+ linkService.populateLinks(snapshot.getBucket());
}
}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
index 02b63d2..b137569 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/ItemResource.java
@@ -115,7 +115,7 @@
items = Collections.emptyList();
}
permissionsService.populateItemPermissions(items);
- linkService.populateItemLinks(items);
+ linkService.populateLinks(items);
return Response.status(Response.Status.OK).entity(items).build();
}
@@ -149,7 +149,7 @@
final List<BucketItem> items = registryService.getBucketItems(bucketId);
permissionsService.populateItemPermissions(items);
- linkService.populateItemLinks(items);
+ linkService.populateLinks(items);
return Response.status(Response.Status.OK).entity(items).build();
}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkBuilder.java
similarity index 94%
rename from nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
rename to nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkBuilder.java
index ec356fd..1e2bc29 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/LinkBuilder.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkBuilder.java
@@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.apache.nifi.registry.web.link.builder;
+package org.apache.nifi.registry.web.link;
import javax.ws.rs.core.Link;
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java
index 19e2168..9b2e818 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/LinkService.java
@@ -17,94 +17,239 @@
package org.apache.nifi.registry.web.link;
import org.apache.nifi.registry.bucket.Bucket;
-import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
import org.apache.nifi.registry.flow.VersionedFlow;
import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
-import org.apache.nifi.registry.web.link.builder.BucketLinkBuilder;
-import org.apache.nifi.registry.web.link.builder.LinkBuilder;
-import org.apache.nifi.registry.web.link.builder.VersionedFlowLinkBuilder;
-import org.apache.nifi.registry.web.link.builder.VersionedFlowSnapshotLinkBuilder;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.nifi.registry.link.LinkableEntity;
import org.springframework.stereotype.Service;
import javax.ws.rs.core.Link;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
@Service
public class LinkService {
- private static final Logger LOGGER = LoggerFactory.getLogger(LinkService.class);
+ private static final String BUCKET_PATH = "buckets/{id}";
- private final LinkBuilder<Bucket> bucketLinkBuilder = new BucketLinkBuilder();
+ private static final String FLOW_PATH = "buckets/{bucketId}/flows/{flowId}";
+ private static final String FLOW_SNAPSHOT_PATH = "buckets/{bucketId}/flows/{flowId}/versions/{versionNumber}";
- private final LinkBuilder<VersionedFlow> versionedFlowLinkBuilder = new VersionedFlowLinkBuilder();
+ private static final String EXTENSION_BUNDLE_PATH = "extensions/bundles/{bundleId}";
+ private static final String EXTENSION_BUNDLE_VERSION_PATH = "extensions/bundles/{bundleId}/versions/{version}";
+ private static final String EXTENSION_BUNDLE_VERSION_CONTENT_PATH = "extensions/bundles/{bundleId}/versions/{version}/content";
- private final LinkBuilder<VersionedFlowSnapshotMetadata> snapshotMetadataLinkBuilder = new VersionedFlowSnapshotLinkBuilder();
+ private static final String EXTENSION_REPO_BUCKET_PATH = "extensions/repo/{bucketName}";
+ private static final String EXTENSION_REPO_GROUP_PATH = "extensions/repo/{bucketName}/{groupId}";
+ private static final String EXTENSION_REPO_ARTIFACT_PATH = "extensions/repo/{bucketName}/{groupId}/{artifactId}";
+ private static final String EXTENSION_REPO_VERSION_PATH = "extensions/repo/{bucketName}/{groupId}/{artifactId}/{version}";
- // ---- Bucket Links
- public void populateBucketLinks(final Iterable<Bucket> buckets) {
- if (buckets == null) {
+ private static final LinkBuilder<Bucket> BUCKET_LINK_BUILDER = (bucket) -> {
+ if (bucket == null) {
+ return null;
+ }
+
+ final URI uri = UriBuilder.fromPath(BUCKET_PATH)
+ .resolveTemplate("id", bucket.getIdentifier())
+ .build();
+
+ return Link.fromUri(uri).rel("self").build();
+ };
+
+ private static final LinkBuilder<VersionedFlow> FLOW_LINK_BUILDER = (versionedFlow -> {
+ if (versionedFlow == null) {
+ return null;
+ }
+
+ final URI uri = UriBuilder.fromPath(FLOW_PATH)
+ .resolveTemplate("bucketId", versionedFlow.getBucketIdentifier())
+ .resolveTemplate("flowId", versionedFlow.getIdentifier())
+ .build();
+
+ return Link.fromUri(uri).rel("self").build();
+ });
+
+ private static final LinkBuilder<VersionedFlowSnapshotMetadata> FLOW_SNAPSHOT_LINK_BUILDER = (snapshotMetadata) -> {
+ if (snapshotMetadata == null) {
+ return null;
+ }
+
+ final URI uri = UriBuilder.fromPath(FLOW_SNAPSHOT_PATH)
+ .resolveTemplate("bucketId", snapshotMetadata.getBucketIdentifier())
+ .resolveTemplate("flowId", snapshotMetadata.getFlowIdentifier())
+ .resolveTemplate("versionNumber", snapshotMetadata.getVersion())
+ .build();
+
+ return Link.fromUri(uri).rel("content").build();
+ };
+
+ private static final LinkBuilder<ExtensionBundle> EXTENSION_BUNDLE_LINK_BUILDER = (extensionBundle -> {
+ if (extensionBundle == null) {
+ return null;
+ }
+
+ final URI uri = UriBuilder.fromPath(EXTENSION_BUNDLE_PATH)
+ .resolveTemplate("bundleId", extensionBundle.getIdentifier())
+ .build();
+
+ return Link.fromUri(uri).rel("self").build();
+ });
+
+ private static final LinkBuilder<ExtensionBundleVersionMetadata> EXTENSION_BUNDLE_VERSION_LINK_BUILDER = (bundleVersion -> {
+ if (bundleVersion == null) {
+ return null;
+ }
+
+ final URI uri = UriBuilder.fromPath(EXTENSION_BUNDLE_VERSION_PATH)
+ .resolveTemplate("bundleId", bundleVersion.getExtensionBundleId())
+ .resolveTemplate("version", bundleVersion.getVersion())
+ .build();
+
+ return Link.fromUri(uri).rel("self").build();
+ });
+
+ private static final LinkBuilder<ExtensionBundleVersion> EXTENSION_BUNDLE_VERSION_CONTENT_LINK_BUILDER = (bundleVersion -> {
+ if (bundleVersion == null) {
+ return null;
+ }
+
+ final URI uri = UriBuilder.fromPath(EXTENSION_BUNDLE_VERSION_CONTENT_PATH)
+ .resolveTemplate("bundleId", bundleVersion.getExtensionBundle().getIdentifier())
+ .resolveTemplate("version", bundleVersion.getVersionMetadata().getVersion())
+ .build();
+
+ return Link.fromUri(uri).rel("self").build();
+ });
+
+ private static final LinkBuilder<ExtensionRepoBucket> EXTENSION_REPO_BUCKET_LINK_BUILDER = (extensionRepoBucket -> {
+ if (extensionRepoBucket == null) {
+ return null;
+ }
+
+ final URI uri = UriBuilder.fromPath(EXTENSION_REPO_BUCKET_PATH)
+ .resolveTemplate("bucketName", extensionRepoBucket.getBucketName())
+ .build();
+
+ return Link.fromUri(uri).rel("self").build();
+ });
+
+ private static final LinkBuilder<ExtensionRepoGroup> EXTENSION_REPO_GROUP_LINK_BUILDER = (extensionRepoGroup -> {
+ if (extensionRepoGroup == null) {
+ return null;
+ }
+
+ final URI uri = UriBuilder.fromPath(EXTENSION_REPO_GROUP_PATH)
+ .resolveTemplate("bucketName", extensionRepoGroup.getBucketName())
+ .resolveTemplate("groupId", extensionRepoGroup.getGroupId())
+ .build();
+
+ return Link.fromUri(uri).rel("self").build();
+ });
+
+ private static final LinkBuilder<ExtensionRepoArtifact> EXTENSION_REPO_ARTIFACT_LINK_BUILDER = (extensionRepoArtifact -> {
+ if (extensionRepoArtifact == null) {
+ return null;
+ }
+
+ final URI uri = UriBuilder.fromPath(EXTENSION_REPO_ARTIFACT_PATH)
+ .resolveTemplate("bucketName", extensionRepoArtifact.getBucketName())
+ .resolveTemplate("groupId", extensionRepoArtifact.getGroupId())
+ .resolveTemplate("artifactId", extensionRepoArtifact.getArtifactId())
+ .build();
+
+ return Link.fromUri(uri).rel("self").build();
+ });
+
+ private static final LinkBuilder<ExtensionRepoVersionSummary> EXTENSION_REPO_VERSION_LINK_BUILDER = (extensionRepoVersion -> {
+ if (extensionRepoVersion == null) {
+ return null;
+ }
+
+ final URI uri = UriBuilder.fromPath(EXTENSION_REPO_VERSION_PATH)
+ .resolveTemplate("bucketName", extensionRepoVersion.getBucketName())
+ .resolveTemplate("groupId", extensionRepoVersion.getGroupId())
+ .resolveTemplate("artifactId", extensionRepoVersion.getArtifactId())
+ .resolveTemplate("version", extensionRepoVersion.getVersion())
+ .build();
+
+ return Link.fromUri(uri).rel("self").build();
+ });
+
+
+ private static final Map<Class,LinkBuilder> LINK_BUILDERS;
+ static {
+ final Map<Class,LinkBuilder> builderMap = new HashMap<>();
+ builderMap.put(Bucket.class, BUCKET_LINK_BUILDER);
+ builderMap.put(VersionedFlow.class, FLOW_LINK_BUILDER);
+ builderMap.put(VersionedFlowSnapshotMetadata.class, FLOW_SNAPSHOT_LINK_BUILDER);
+ builderMap.put(ExtensionBundle.class, EXTENSION_BUNDLE_LINK_BUILDER);
+ builderMap.put(ExtensionBundleVersionMetadata.class, EXTENSION_BUNDLE_VERSION_LINK_BUILDER);
+ builderMap.put(ExtensionBundleVersion.class, EXTENSION_BUNDLE_VERSION_CONTENT_LINK_BUILDER);
+ builderMap.put(ExtensionRepoBucket.class, EXTENSION_REPO_BUCKET_LINK_BUILDER);
+ builderMap.put(ExtensionRepoGroup.class, EXTENSION_REPO_GROUP_LINK_BUILDER);
+ builderMap.put(ExtensionRepoArtifact.class, EXTENSION_REPO_ARTIFACT_LINK_BUILDER);
+ builderMap.put(ExtensionRepoVersionSummary.class, EXTENSION_REPO_VERSION_LINK_BUILDER);
+ LINK_BUILDERS = Collections.unmodifiableMap(builderMap);
+ }
+
+ public <E extends LinkableEntity> void populateLinks(final E entity) {
+ final LinkBuilder linkBuilder = LINK_BUILDERS.get(entity.getClass());
+ if (linkBuilder == null) {
+ throw new IllegalArgumentException("No LinkBuilder found for " + entity.getClass().getCanonicalName());
+ }
+
+ final Link link = linkBuilder.createLink(entity);
+ entity.setLink(link);
+ }
+
+ public <E extends LinkableEntity> void populateLinks(final Iterable<E> entities) {
+ if (entities == null) {
return;
}
- buckets.forEach(b -> populateBucketLinks(b));
+ entities.forEach(e -> populateLinks(e));
}
- public void populateBucketLinks(final Bucket bucket) {
- final Link bucketLink = bucketLinkBuilder.createLink(bucket);
- bucket.setLink(bucketLink);
+ public <E extends LinkableEntity> void populateFullLinks(final E entity, final URI baseUri) {
+ final LinkBuilder linkBuilder = LINK_BUILDERS.get(entity.getClass());
+ if (linkBuilder == null) {
+ throw new IllegalArgumentException("No LinkBuilder found for " + entity.getClass().getCanonicalName());
+ }
+
+ if (baseUri == null) {
+ throw new IllegalArgumentException("Base URI cannot be null");
+ }
+
+ final Link relativeLink = linkBuilder.createLink(entity);
+ final URI relativeUri = relativeLink.getUri();
+
+ final URI fullUri = UriBuilder.fromUri(baseUri)
+ .path(relativeUri.getPath())
+ .build();
+
+ final Link fullLink = Link.fromUri(fullUri)
+ .rel(relativeLink.getRel())
+ .build();
+
+ entity.setLink(fullLink);
}
- // ---- Flow Links
-
- public void populateFlowLinks(final Iterable<VersionedFlow> versionedFlows) {
- if (versionedFlows == null) {
+ public <E extends LinkableEntity> void populateFullLinks(final Iterable<E> entities, final URI baseUri) {
+ if (entities == null) {
return;
}
- versionedFlows.forEach(f -> populateFlowLinks(f));
+ entities.forEach(e -> populateFullLinks(e, baseUri));
}
- public void populateFlowLinks(final VersionedFlow versionedFlow) {
- final Link flowLink = versionedFlowLinkBuilder.createLink(versionedFlow);
- versionedFlow.setLink(flowLink);
- }
-
- // ---- Flow Snapshot Links
-
- public void populateSnapshotLinks(final Iterable<VersionedFlowSnapshotMetadata> snapshotMetadatas) {
- if (snapshotMetadatas == null) {
- return;
- }
-
- snapshotMetadatas.forEach(s -> populateSnapshotLinks(s));
- }
-
- public void populateSnapshotLinks(final VersionedFlowSnapshotMetadata snapshotMetadata) {
- final Link snapshotLink = snapshotMetadataLinkBuilder.createLink(snapshotMetadata);
- snapshotMetadata.setLink(snapshotLink);
- }
-
- // ---- BucketItem Links
-
- public void populateItemLinks(final Iterable<BucketItem> items) {
- if (items == null) {
- return;
- }
-
- items.forEach(i -> populateItemLinks(i));
- }
-
- public void populateItemLinks(final BucketItem bucketItem) {
- if (bucketItem == null) {
- return;
- }
-
- if (bucketItem instanceof VersionedFlow) {
- populateFlowLinks((VersionedFlow)bucketItem);
- } else {
- LOGGER.error("Unable to create link for BucketItem with type: " + bucketItem.getClass().getCanonicalName());
- }
- }
}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/BucketLinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/BucketLinkBuilder.java
deleted file mode 100644
index f0409c7..0000000
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/BucketLinkBuilder.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * 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.web.link.builder;
-
-import org.apache.nifi.registry.bucket.Bucket;
-
-import javax.ws.rs.core.Link;
-import javax.ws.rs.core.UriBuilder;
-import java.net.URI;
-
-/**
- * LinkBuilder that builds "self" links for Buckets.
- */
-public class BucketLinkBuilder implements LinkBuilder<Bucket> {
-
- private static final String PATH = "buckets/{id}";
-
- @Override
- public Link createLink(final Bucket bucket) {
- if (bucket == null) {
- return null;
- }
-
- final URI uri = UriBuilder.fromPath(PATH)
- .resolveTemplate("id", bucket.getIdentifier())
- .build();
-
- return Link.fromUri(uri).rel("self").build();
- }
-
-}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowLinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowLinkBuilder.java
deleted file mode 100644
index 38d3d0e..0000000
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowLinkBuilder.java
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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.web.link.builder;
-
-import org.apache.nifi.registry.flow.VersionedFlow;
-
-import javax.ws.rs.core.Link;
-import javax.ws.rs.core.UriBuilder;
-import java.net.URI;
-
-/**
- * LinkBuilder that builds "self" links for VersionedFlows.
- */
-public class VersionedFlowLinkBuilder implements LinkBuilder<VersionedFlow> {
-
- private static final String PATH = "buckets/{bucketId}/flows/{flowId}";
-
- @Override
- public Link createLink(final VersionedFlow versionedFlow) {
- if (versionedFlow == null) {
- return null;
- }
-
- final URI uri = UriBuilder.fromPath(PATH)
- .resolveTemplate("bucketId", versionedFlow.getBucketIdentifier())
- .resolveTemplate("flowId", versionedFlow.getIdentifier())
- .build();
-
- return Link.fromUri(uri).rel("self").build();
- }
-
-}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowSnapshotLinkBuilder.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowSnapshotLinkBuilder.java
deleted file mode 100644
index 4085c6d..0000000
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/link/builder/VersionedFlowSnapshotLinkBuilder.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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.web.link.builder;
-
-import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
-
-import javax.ws.rs.core.Link;
-import javax.ws.rs.core.UriBuilder;
-import java.net.URI;
-
-/**
- * LinkBuilder that builds "self" links for VersionedFlowSnapshotMetadata.
- */
-public class VersionedFlowSnapshotLinkBuilder implements LinkBuilder<VersionedFlowSnapshotMetadata> {
-
- private static final String PATH = "buckets/{bucketId}/flows/{flowId}/versions/{versionNumber}";
-
- @Override
- public Link createLink(final VersionedFlowSnapshotMetadata snapshotMetadata) {
- if (snapshotMetadata == null) {
- return null;
- }
-
- final URI uri = UriBuilder.fromPath(PATH)
- .resolveTemplate("bucketId", snapshotMetadata.getBucketIdentifier())
- .resolveTemplate("flowId", snapshotMetadata.getFlowIdentifier())
- .resolveTemplate("versionNumber", snapshotMetadata.getVersion())
- .build();
-
- return Link.fromUri(uri).rel("content").build();
- }
-
-}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java
index e611b53..f44d766 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/security/authentication/kerberos/KerberosSpnegoIdentityProvider.java
@@ -39,13 +39,11 @@
import org.springframework.security.kerberos.authentication.KerberosServiceAuthenticationProvider;
import org.springframework.security.kerberos.authentication.KerberosServiceRequestToken;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
-import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
-@Component
public class KerberosSpnegoIdentityProvider implements IdentityProvider {
private static final Logger logger = LoggerFactory.getLogger(KerberosSpnegoIdentityProvider.class);
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
index 2410234..2fb2f4a 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/api/UnsecuredNiFiRegistryClientIT.java
@@ -16,11 +16,17 @@
*/
package org.apache.nifi.registry.web.api;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.registry.authorization.CurrentUser;
import org.apache.nifi.registry.authorization.Permissions;
import org.apache.nifi.registry.bucket.Bucket;
import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.bucket.BucketItemType;
import org.apache.nifi.registry.client.BucketClient;
+import org.apache.nifi.registry.client.ExtensionBundleClient;
+import org.apache.nifi.registry.client.ExtensionBundleVersionClient;
+import org.apache.nifi.registry.client.ExtensionRepoClient;
import org.apache.nifi.registry.client.FlowClient;
import org.apache.nifi.registry.client.FlowSnapshotClient;
import org.apache.nifi.registry.client.ItemsClient;
@@ -30,6 +36,16 @@
import org.apache.nifi.registry.client.UserClient;
import org.apache.nifi.registry.client.impl.JerseyNiFiRegistryClient;
import org.apache.nifi.registry.diff.VersionedFlowDifference;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleType;
+import org.apache.nifi.registry.extension.ExtensionBundleVersion;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionDependency;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersion;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
import org.apache.nifi.registry.field.Fields;
import org.apache.nifi.registry.flow.VersionedFlow;
import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
@@ -37,6 +53,9 @@
import org.apache.nifi.registry.flow.VersionedProcessGroup;
import org.apache.nifi.registry.flow.VersionedProcessor;
import org.apache.nifi.registry.flow.VersionedPropertyDescriptor;
+import org.apache.nifi.registry.util.FileUtils;
+import org.bouncycastle.util.encoders.Hex;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -44,11 +63,21 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import java.io.File;
+import java.io.FileInputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
/**
* Test all basic functionality of JerseyNiFiRegistryClient.
@@ -60,7 +89,7 @@
private NiFiRegistryClient client;
@Before
- public void setup() {
+ public void setup() throws IOException {
final String baseUrl = createBaseURL();
LOGGER.info("Using base url = " + baseUrl);
@@ -76,6 +105,16 @@
Assert.assertNotNull(client);
this.client = client;
+
+ // Clear the extension bundles storage directory in case previous tests left data
+ final File extensionsStorageDir = new File("./target/test-classes/extension_bundles");
+ if (extensionsStorageDir.exists()) {
+ try {
+ FileUtils.deleteFile(extensionsStorageDir, true);
+ } catch (Exception e) {
+ LOGGER.warn("Unable to delete extensions storage dir due to: " + e.getMessage(), e);
+ }
+ }
}
@After
@@ -103,7 +142,7 @@
}
@Test
- public void testNiFiRegistryClient() throws IOException, NiFiRegistryException {
+ public void testNiFiRegistryClient() throws IOException, NiFiRegistryException, NoSuchAlgorithmException {
// ---------------------- TEST BUCKETS --------------------------//
final BucketClient bucketClient = client.getBucketClient();
@@ -281,7 +320,190 @@
Assert.assertEquals(snapshotFlow.getIdentifier(), latestMetadataWithoutBucket.getFlowIdentifier());
Assert.assertEquals(2, latestMetadataWithoutBucket.getVersion());
- // ---------------------- TEST ITEMS --------------------------//
+ // ---------------------- TEST EXTENSIONS ----------------------//
+
+ // verify we have no bundles yet
+ final ExtensionBundleClient bundleClient = client.getExtensionBundleClient();
+ final List<ExtensionBundle> allBundles = bundleClient.getAll();
+ Assert.assertEquals(0, allBundles.size());
+
+ final Bucket bundlesBucket = createdBuckets.get(1);
+ final ExtensionBundleVersionClient bundleVersionClient = client.getExtensionBundleVersionClient();
+
+ // create version 1.0.0 of nifi-test-nar
+ final String testNar1 = "src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar";
+ final ExtensionBundleVersion createdTestNarV1 = createExtensionBundleVersionWithStream(bundlesBucket, bundleVersionClient, testNar1, null);
+
+ final ExtensionBundle testNarV1Bundle = createdTestNarV1.getExtensionBundle();
+ LOGGER.info("Created bundle with id {}", new Object[]{testNarV1Bundle.getIdentifier()});
+
+ Assert.assertEquals("org.apache.nifi", testNarV1Bundle.getGroupId());
+ Assert.assertEquals("nifi-test-nar", testNarV1Bundle.getArtifactId());
+ Assert.assertEquals(ExtensionBundleType.NIFI_NAR, testNarV1Bundle.getBundleType());
+ Assert.assertEquals(1, testNarV1Bundle.getVersionCount());
+
+ Assert.assertEquals("org.apache.nifi:nifi-test-nar", testNarV1Bundle.getName());
+ Assert.assertEquals(bundlesBucket.getIdentifier(), testNarV1Bundle.getBucketIdentifier());
+ Assert.assertEquals(bundlesBucket.getName(), testNarV1Bundle.getBucketName());
+ Assert.assertNotNull(testNarV1Bundle.getPermissions());
+ Assert.assertTrue(testNarV1Bundle.getCreatedTimestamp() > 0);
+ Assert.assertTrue(testNarV1Bundle.getModifiedTimestamp() > 0);
+
+ final ExtensionBundleVersionMetadata testNarV1Metadata = createdTestNarV1.getVersionMetadata();
+ Assert.assertEquals("1.0.0", testNarV1Metadata.getVersion());
+ Assert.assertNotNull(testNarV1Metadata.getId());
+ Assert.assertNotNull(testNarV1Metadata.getSha256());
+ Assert.assertNotNull(testNarV1Metadata.getAuthor());
+ Assert.assertEquals(testNarV1Bundle.getIdentifier(), testNarV1Metadata.getExtensionBundleId());
+ Assert.assertEquals(bundlesBucket.getIdentifier(), testNarV1Metadata.getBucketId());
+ Assert.assertTrue(testNarV1Metadata.getTimestamp() > 0);
+ Assert.assertFalse(testNarV1Metadata.getSha256Supplied());
+
+ final Set<ExtensionBundleVersionDependency> dependencies = createdTestNarV1.getDependencies();
+ Assert.assertNotNull(dependencies);
+ Assert.assertEquals(1, dependencies.size());
+
+ final ExtensionBundleVersionDependency testNarV1Dependency = dependencies.stream().findFirst().get();
+ Assert.assertEquals("org.apache.nifi", testNarV1Dependency.getGroupId());
+ Assert.assertEquals("nifi-test-api-nar", testNarV1Dependency.getArtifactId());
+ Assert.assertEquals("1.0.0", testNarV1Dependency.getVersion());
+
+ final String testNar2 = "src/test/resources/extensions/nars/nifi-test-nar-2.0.0.nar";
+
+ // try to create version 2.0.0 of nifi-test-nar when the supplied SHA-256 does not match server's
+ final String madeUpSha256 = "MADE-UP-SHA-256";
+ try {
+ createExtensionBundleVersionWithStream(bundlesBucket, bundleVersionClient, testNar2, madeUpSha256);
+ Assert.fail("Should have thrown exception");
+ } catch (Exception e) {
+ // should have thrown exception from mismatched SHA-256
+ }
+
+ // create version 2.0.0 of nifi-test-nar using correct supplied SHA-256
+ final String testNar2Sha256 = calculateSha256Hex(testNar2);
+ final ExtensionBundleVersion createdTestNarV2 = createExtensionBundleVersionWithStream(bundlesBucket, bundleVersionClient, testNar2, testNar2Sha256);
+ Assert.assertTrue(createdTestNarV2.getVersionMetadata().getSha256Supplied());
+
+ final ExtensionBundle testNarV2Bundle = createdTestNarV2.getExtensionBundle();
+ LOGGER.info("Created bundle with id {}", new Object[]{testNarV2Bundle.getIdentifier()});
+
+ // create version 1.0.0 of nifi-foo-nar, use the file variant
+ final String fooNar = "src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar";
+ final ExtensionBundleVersion createdFooNarV1 = createExtensionBundleVersionWithFile(bundlesBucket, bundleVersionClient, fooNar, null);
+ Assert.assertFalse(createdFooNarV1.getVersionMetadata().getSha256Supplied());
+
+ final ExtensionBundle fooNarV1Bundle = createdFooNarV1.getExtensionBundle();
+ LOGGER.info("Created bundle with id {}", new Object[]{fooNarV1Bundle.getIdentifier()});
+
+ // verify there are 2 bundles now
+ final List<ExtensionBundle> allBundlesAfterCreate = bundleClient.getAll();
+ Assert.assertEquals(2, allBundlesAfterCreate.size());
+
+ // verify getting bundles by bucket
+ Assert.assertEquals(2, bundleClient.getByBucket(bundlesBucket.getIdentifier()).size());
+ Assert.assertEquals(0, bundleClient.getByBucket(flowsBucket.getIdentifier()).size());
+
+ // verify getting bundles by id
+ final ExtensionBundle retrievedBundle = bundleClient.get(testNarV1Bundle.getIdentifier());
+ Assert.assertNotNull(retrievedBundle);
+ Assert.assertEquals(testNarV1Bundle.getIdentifier(), retrievedBundle.getIdentifier());
+ Assert.assertEquals(testNarV1Bundle.getGroupId(), retrievedBundle.getGroupId());
+ Assert.assertEquals(testNarV1Bundle.getArtifactId(), retrievedBundle.getArtifactId());
+
+ // verify getting list of version metadata for a bundle
+ final List<ExtensionBundleVersionMetadata> bundleVersions = bundleVersionClient.getBundleVersions(testNarV1Bundle.getIdentifier());
+ Assert.assertNotNull(bundleVersions);
+ Assert.assertEquals(2, bundleVersions.size());
+
+ // verify getting a bundle version by the bundle id + version string
+ final ExtensionBundleVersion bundleVersion1 = bundleVersionClient.getBundleVersion(testNarV1Bundle.getIdentifier(), "1.0.0");
+ Assert.assertNotNull(bundleVersion1);
+ Assert.assertEquals("1.0.0", bundleVersion1.getVersionMetadata().getVersion());
+ Assert.assertNotNull(bundleVersion1.getDependencies());
+ Assert.assertEquals(1, bundleVersion1.getDependencies().size());
+
+ final ExtensionBundleVersion bundleVersion2 = bundleVersionClient.getBundleVersion(testNarV1Bundle.getIdentifier(), "2.0.0");
+ Assert.assertNotNull(bundleVersion2);
+ Assert.assertEquals("2.0.0", bundleVersion2.getVersionMetadata().getVersion());
+
+ // verify getting the input stream for a bundle version
+ try (final InputStream bundleVersion1InputStream = bundleVersionClient.getBundleVersionContent(testNarV1Bundle.getIdentifier(), "1.0.0")) {
+ final String sha256Hex = DigestUtils.sha256Hex(bundleVersion1InputStream);
+ Assert.assertEquals(testNarV1Metadata.getSha256(), sha256Hex);
+ }
+
+ // verify writing a bundle version to an output stream
+ final File targetDir = new File("./target");
+ final File bundleFile = bundleVersionClient.writeBundleVersionContent(testNarV1Bundle.getIdentifier(), "1.0.0", targetDir);
+ Assert.assertNotNull(bundleFile);
+
+ try (final InputStream bundleInputStream = new FileInputStream(bundleFile)) {
+ final String sha256Hex = DigestUtils.sha256Hex(bundleInputStream);
+ Assert.assertEquals(testNarV1Metadata.getSha256(), sha256Hex);
+ }
+
+ // Verify deleting a bundle version
+ final ExtensionBundleVersion deletedBundleVersion2 = bundleVersionClient.delete(testNarV1Bundle.getIdentifier(), "2.0.0");
+ Assert.assertNotNull(deletedBundleVersion2);
+ Assert.assertEquals(testNarV1Bundle.getIdentifier(), deletedBundleVersion2.getExtensionBundle().getIdentifier());
+ Assert.assertEquals("2.0.0", deletedBundleVersion2.getVersionMetadata().getVersion());
+
+ try {
+ bundleVersionClient.getBundleVersion(testNarV1Bundle.getIdentifier(), "2.0.0");
+ Assert.fail("Should have thrown exception");
+ } catch (Exception e) {
+ // should catch exception
+ }
+
+ // ---------------------- TEST EXTENSION REPO ----------------------//
+
+ final ExtensionRepoClient extensionRepoClient = client.getExtensionRepoClient();
+
+ final List<ExtensionRepoBucket> repoBuckets = extensionRepoClient.getBuckets();
+ Assert.assertEquals(createdBuckets.size(), repoBuckets.size());
+
+ final String bundlesBucketName = bundlesBucket.getName();
+ final List<ExtensionRepoGroup> repoGroups = extensionRepoClient.getGroups(bundlesBucketName);
+ Assert.assertEquals(1, repoGroups.size());
+
+ final String repoGroupId = "org.apache.nifi";
+ final ExtensionRepoGroup repoGroup = repoGroups.get(0);
+ Assert.assertEquals(repoGroupId, repoGroup.getGroupId());
+
+ final List<ExtensionRepoArtifact> repoArtifacts = extensionRepoClient.getArtifacts(bundlesBucketName, repoGroupId);
+ Assert.assertEquals(2, repoArtifacts.size());
+
+ final String repoArtifactId = "nifi-test-nar";
+ final List<ExtensionRepoVersionSummary> repoVersions = extensionRepoClient.getVersions(bundlesBucketName, repoGroupId, repoArtifactId);
+ Assert.assertEquals(1, repoVersions.size());
+
+ final String repoVersionString = "1.0.0";
+ final ExtensionRepoVersion repoVersion = extensionRepoClient.getVersion(bundlesBucketName, repoGroupId, repoArtifactId, repoVersionString);
+ Assert.assertNotNull(repoVersion);
+ Assert.assertNotNull(repoVersion.getDownloadLink());
+ Assert.assertNotNull(repoVersion.getSha256Link());
+
+ // verify the version links for content and sha256
+ final Client jerseyClient = ClientBuilder.newBuilder().register(MultiPartFeature.class).build();
+
+ final WebTarget downloadLinkTarget = jerseyClient.target(repoVersion.getDownloadLink().getUri());
+ try (final InputStream downloadLinkInputStream = downloadLinkTarget.request()
+ .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE).get().readEntity(InputStream.class)) {
+ final String sha256DownloadResult = DigestUtils.sha256Hex(downloadLinkInputStream);
+
+ final WebTarget sha256LinkTarget = jerseyClient.target(repoVersion.getSha256Link().getUri());
+ final String sha256LinkResult = sha256LinkTarget.request().get(String.class);
+ Assert.assertEquals(sha256DownloadResult, sha256LinkResult);
+ }
+
+ // verify the client methods for content input stream and content sha256
+ try (final InputStream repoVersionInputStream = extensionRepoClient.getVersionContent(bundlesBucketName, repoGroupId, repoArtifactId, repoVersionString)) {
+ final String sha256Hex = DigestUtils.sha256Hex(repoVersionInputStream);
+ final String repoSha256Hex = extensionRepoClient.getVersionSha256(bundlesBucketName, repoGroupId, repoArtifactId, repoVersionString);
+ Assert.assertEquals(sha256Hex, repoSha256Hex);
+ }
+
+ // ---------------------- TEST ITEMS -------------------------- //
final ItemsClient itemsClient = client.getItemsClient();
@@ -292,10 +514,25 @@
// get all items
final List<BucketItem> allItems = itemsClient.getAll();
- Assert.assertEquals(2, allItems.size());
- allItems.stream().forEach(i -> Assert.assertNotNull(i.getBucketName()));
+ Assert.assertEquals(4, allItems.size());
+ allItems.stream().forEach(i -> {
+ Assert.assertNotNull(i.getBucketName());
+ Assert.assertNotNull(i.getLink());
+ });
allItems.stream().forEach(i -> LOGGER.info("All items, item " + i.getIdentifier()));
+ // verify 2 flow items
+ final List<BucketItem> flowItems = allItems.stream()
+ .filter(i -> i.getType() == BucketItemType.Flow)
+ .collect(Collectors.toList());
+ Assert.assertEquals(2, flowItems.size());
+
+ // verify 2 bundle items
+ final List<BucketItem> extensionBundleItems = allItems.stream()
+ .filter(i -> i.getType() == BucketItemType.Extension_Bundle)
+ .collect(Collectors.toList());
+ Assert.assertEquals(2, extensionBundleItems.size());
+
// get items for bucket
final List<BucketItem> bucketItems = itemsClient.getByBucket(flowsBucket.getIdentifier());
Assert.assertEquals(2, bucketItems.size());
@@ -325,6 +562,14 @@
Assert.assertNotNull(deletedFlow2);
LOGGER.info("Deleted flow " + deletedFlow2.getIdentifier());
+ final ExtensionBundle deletedBundle1 = bundleClient.delete(testNarV1Bundle.getIdentifier());
+ Assert.assertNotNull(deletedBundle1);
+ LOGGER.info("Deleted extension bundle " + deletedBundle1.getIdentifier());
+
+ final ExtensionBundle deletedBundle2 = bundleClient.delete(fooNarV1Bundle.getIdentifier());
+ Assert.assertNotNull(deletedBundle2);
+ LOGGER.info("Deleted extension bundle " + deletedBundle2.getIdentifier());
+
// delete each bucket
for (final Bucket bucket : createdBuckets) {
final Bucket deletedBucket = bucketClient.delete(bucket.getIdentifier());
@@ -337,6 +582,58 @@
}
+ private ExtensionBundleVersion createExtensionBundleVersionWithStream(final Bucket bundlesBucket,
+ final ExtensionBundleVersionClient bundleVersionClient,
+ final String narFile, final String sha256)
+ throws IOException, NiFiRegistryException {
+
+ final ExtensionBundleVersion createdBundleVersion;
+ try (final InputStream bundleInputStream = new FileInputStream(narFile)) {
+ if (StringUtils.isBlank(sha256)) {
+ createdBundleVersion = bundleVersionClient.create(
+ bundlesBucket.getIdentifier(), ExtensionBundleType.NIFI_NAR, bundleInputStream);
+ } else {
+ createdBundleVersion = bundleVersionClient.create(
+ bundlesBucket.getIdentifier(), ExtensionBundleType.NIFI_NAR, bundleInputStream, sha256);
+ }
+ }
+
+ Assert.assertNotNull(createdBundleVersion);
+ Assert.assertNotNull(createdBundleVersion.getBucket());
+ Assert.assertNotNull(createdBundleVersion.getExtensionBundle());
+ Assert.assertNotNull(createdBundleVersion.getVersionMetadata());
+
+ return createdBundleVersion;
+ }
+
+ private ExtensionBundleVersion createExtensionBundleVersionWithFile(final Bucket bundlesBucket,
+ final ExtensionBundleVersionClient bundleVersionClient,
+ final String narFile, final String sha256)
+ throws IOException, NiFiRegistryException {
+
+ final ExtensionBundleVersion createdBundleVersion;
+ if (StringUtils.isBlank(sha256)) {
+ createdBundleVersion = bundleVersionClient.create(
+ bundlesBucket.getIdentifier(), ExtensionBundleType.NIFI_NAR, new File(narFile));
+ } else {
+ createdBundleVersion = bundleVersionClient.create(
+ bundlesBucket.getIdentifier(), ExtensionBundleType.NIFI_NAR, new File(narFile), sha256);
+ }
+
+ Assert.assertNotNull(createdBundleVersion);
+ Assert.assertNotNull(createdBundleVersion.getBucket());
+ Assert.assertNotNull(createdBundleVersion.getExtensionBundle());
+ Assert.assertNotNull(createdBundleVersion.getVersionMetadata());
+
+ return createdBundleVersion;
+ }
+
+ private String calculateSha256Hex(final String narFile) throws IOException {
+ try (final InputStream bundleInputStream = new FileInputStream(narFile)) {
+ return Hex.toHexString(DigestUtils.sha256(bundleInputStream));
+ }
+ }
+
private static Bucket createBucket(BucketClient bucketClient, int num) throws IOException, NiFiRegistryException {
final Bucket bucket = new Bucket();
bucket.setName("Bucket #" + num);
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
index bfc9a46..4e1f239 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/java/org/apache/nifi/registry/web/link/TestLinkService.java
@@ -18,17 +18,29 @@
import org.apache.nifi.registry.bucket.Bucket;
import org.apache.nifi.registry.bucket.BucketItem;
+import org.apache.nifi.registry.bucket.BucketItemType;
+import org.apache.nifi.registry.extension.ExtensionBundle;
+import org.apache.nifi.registry.extension.ExtensionBundleVersionMetadata;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoArtifact;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoBucket;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoGroup;
+import org.apache.nifi.registry.extension.repo.ExtensionRepoVersionSummary;
import org.apache.nifi.registry.flow.VersionedFlow;
import org.apache.nifi.registry.flow.VersionedFlowSnapshotMetadata;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
+import javax.ws.rs.core.UriBuilder;
+import java.net.URI;
import java.util.ArrayList;
import java.util.List;
public class TestLinkService {
+ private static final String BASE_URI = "http://localhost:18080/nifi-registry-api";
+ private URI baseUri = UriBuilder.fromUri(BASE_URI).build();
+
private LinkService linkService;
private List<Bucket> buckets;
@@ -36,6 +48,14 @@
private List<VersionedFlowSnapshotMetadata> snapshots;
private List<BucketItem> items;
+ private List<ExtensionBundle> extensionBundles;
+ private List<ExtensionBundleVersionMetadata> extensionBundleVersionMetadata;
+
+ private List<ExtensionRepoBucket> extensionRepoBuckets;
+ private List<ExtensionRepoGroup> extensionRepoGroups;
+ private List<ExtensionRepoArtifact> extensionRepoArtifacts;
+ private List<ExtensionRepoVersionSummary> extensionRepoVersions;
+
@Before
public void setup() {
linkService = new LinkService();
@@ -43,11 +63,11 @@
// setup buckets
final Bucket bucket1 = new Bucket();
bucket1.setIdentifier("b1");
- bucket1.setName("Bucket 1");
+ bucket1.setName("Bucket_1");
final Bucket bucket2 = new Bucket();
bucket2.setIdentifier("b2");
- bucket2.setName("Bucket 2");
+ bucket2.setName("Bucket_2");
buckets = new ArrayList<>();
buckets.add(bucket1);
@@ -56,12 +76,12 @@
// setup flows
final VersionedFlow flow1 = new VersionedFlow();
flow1.setIdentifier("f1");
- flow1.setName("Flow 1");
+ flow1.setName("Flow_1");
flow1.setBucketIdentifier(bucket1.getIdentifier());
final VersionedFlow flow2 = new VersionedFlow();
flow2.setIdentifier("f2");
- flow2.setName("Flow 2");
+ flow2.setName("Flow_2");
flow2.setBucketIdentifier(bucket1.getIdentifier());
flows = new ArrayList<>();
@@ -83,42 +103,197 @@
snapshots.add(snapshotMetadata1);
snapshots.add(snapshotMetadata2);
+ // setup extension bundles
+ final ExtensionBundle bundle1 = new ExtensionBundle();
+ bundle1.setIdentifier("eb1");
+
+ final ExtensionBundle bundle2 = new ExtensionBundle();
+ bundle2.setIdentifier("eb2");
+
+ extensionBundles = new ArrayList<>();
+ extensionBundles.add(bundle1);
+ extensionBundles.add(bundle2);
+
+ // setup extension bundle versions
+ final ExtensionBundleVersionMetadata bundleVersion1 = new ExtensionBundleVersionMetadata();
+ bundleVersion1.setExtensionBundleId(bundle1.getIdentifier());
+ bundleVersion1.setVersion("1.0.0");
+
+ final ExtensionBundleVersionMetadata bundleVersion2 = new ExtensionBundleVersionMetadata();
+ bundleVersion2.setExtensionBundleId(bundle1.getIdentifier());
+ bundleVersion2.setVersion("2.0.0");
+
+ extensionBundleVersionMetadata = new ArrayList<>();
+ extensionBundleVersionMetadata.add(bundleVersion1);
+ extensionBundleVersionMetadata.add(bundleVersion2);
+
+ // setup extension repo buckets
+ final ExtensionRepoBucket rb1 = new ExtensionRepoBucket();
+ rb1.setBucketName(bucket1.getName());
+
+ final ExtensionRepoBucket rb2 = new ExtensionRepoBucket();
+ rb2.setBucketName(bucket2.getName());
+
+ extensionRepoBuckets = new ArrayList<>();
+ extensionRepoBuckets.add(rb1);
+ extensionRepoBuckets.add(rb2);
+
+ // setup extension repo groups
+ final ExtensionRepoGroup rg1 = new ExtensionRepoGroup();
+ rg1.setBucketName(rb1.getBucketName());
+ rg1.setGroupId("g1");
+
+ final ExtensionRepoGroup rg2 = new ExtensionRepoGroup();
+ rg2.setBucketName(rb1.getBucketName());
+ rg2.setGroupId("g2");
+
+ extensionRepoGroups = new ArrayList<>();
+ extensionRepoGroups.add(rg1);
+ extensionRepoGroups.add(rg2);
+
+ // setup extension repo artifacts
+ final ExtensionRepoArtifact ra1 = new ExtensionRepoArtifact();
+ ra1.setBucketName(rb1.getBucketName());
+ ra1.setGroupId(rg1.getGroupId());
+ ra1.setArtifactId("a1");
+
+ final ExtensionRepoArtifact ra2 = new ExtensionRepoArtifact();
+ ra2.setBucketName(rb1.getBucketName());
+ ra2.setGroupId(rg1.getGroupId());
+ ra2.setArtifactId("a2");
+
+ extensionRepoArtifacts = new ArrayList<>();
+ extensionRepoArtifacts.add(ra1);
+ extensionRepoArtifacts.add(ra2);
+
+ // setup extension repo versions
+ final ExtensionRepoVersionSummary rv1 = new ExtensionRepoVersionSummary();
+ rv1.setBucketName(rb1.getBucketName());
+ rv1.setGroupId(rg1.getGroupId());
+ rv1.setArtifactId(ra1.getArtifactId());
+ rv1.setVersion("1.0.0");
+
+ final ExtensionRepoVersionSummary rv2 = new ExtensionRepoVersionSummary();
+ rv2.setBucketName(rb1.getBucketName());
+ rv2.setGroupId(rg1.getGroupId());
+ rv2.setArtifactId(ra1.getArtifactId());
+ rv2.setVersion("2.0.0");
+
+ extensionRepoVersions = new ArrayList<>();
+ extensionRepoVersions.add(rv1);
+ extensionRepoVersions.add(rv2);
+
// setup items
items = new ArrayList<>();
items.add(flow1);
items.add(flow2);
+ items.add(bundle1);
+ items.add(bundle2);
}
@Test
public void testPopulateBucketLinks() {
- buckets.stream().forEach(b -> Assert.assertNull(b.getLink()));
- linkService.populateBucketLinks(buckets);
- buckets.stream().forEach(b -> Assert.assertEquals(
+ buckets.forEach(b -> Assert.assertNull(b.getLink()));
+ linkService.populateLinks(buckets);
+ buckets.forEach(b -> Assert.assertEquals(
"buckets/" + b.getIdentifier(), b.getLink().getUri().toString()));
}
@Test
public void testPopulateFlowLinks() {
- flows.stream().forEach(f -> Assert.assertNull(f.getLink()));
- linkService.populateFlowLinks(flows);
- flows.stream().forEach(f -> Assert.assertEquals(
+ flows.forEach(f -> Assert.assertNull(f.getLink()));
+ linkService.populateLinks(flows);
+ flows.forEach(f -> Assert.assertEquals(
"buckets/" + f.getBucketIdentifier() + "/flows/" + f.getIdentifier(), f.getLink().getUri().toString()));
}
@Test
public void testPopulateSnapshotLinks() {
- snapshots.stream().forEach(s -> Assert.assertNull(s.getLink()));
- linkService.populateSnapshotLinks(snapshots);
- snapshots.stream().forEach(s -> Assert.assertEquals(
+ snapshots.forEach(s -> Assert.assertNull(s.getLink()));
+ linkService.populateLinks(snapshots);
+ snapshots.forEach(s -> Assert.assertEquals(
"buckets/" + s.getBucketIdentifier() + "/flows/" + s.getFlowIdentifier() + "/versions/" + s.getVersion(), s.getLink().getUri().toString()));
}
@Test
public void testPopulateItemLinks() {
- items.stream().forEach(i -> Assert.assertNull(i.getLink()));
- linkService.populateItemLinks(items);
- items.stream().forEach(i -> Assert.assertEquals(
- "buckets/" + i.getBucketIdentifier() + "/flows/" + i.getIdentifier(), i.getLink().getUri().toString()));
+ items.forEach(i -> Assert.assertNull(i.getLink()));
+ linkService.populateLinks(items);
+ items.forEach(i -> {
+ if (i.getType() == BucketItemType.Flow) {
+ Assert.assertEquals("buckets/" + i.getBucketIdentifier() + "/flows/" + i.getIdentifier(), i.getLink().getUri().toString());
+ } else {
+ Assert.assertEquals("extensions/bundles/" + i.getIdentifier(), i.getLink().getUri().toString());
+ }
+ });
}
+ @Test
+ public void testPopulateExtensionBundleLinks() {
+ extensionBundles.forEach(i -> Assert.assertNull(i.getLink()));
+ linkService.populateLinks(extensionBundles);
+ extensionBundles.forEach(eb -> Assert.assertEquals("extensions/bundles/" + eb.getIdentifier(), eb.getLink().getUri().toString()));
+ }
+
+ @Test
+ public void testPopulateExtensionBundleVersionLinks() {
+ extensionBundleVersionMetadata.forEach(i -> Assert.assertNull(i.getLink()));
+ linkService.populateLinks(extensionBundleVersionMetadata);
+ extensionBundleVersionMetadata.forEach(eb -> Assert.assertEquals(
+ "extensions/bundles/" + eb.getExtensionBundleId() + "/versions/" + eb.getVersion(), eb.getLink().getUri().toString()));
+ }
+
+ @Test
+ public void testPopulateExtensionRepoBucketLinks() {
+ extensionRepoBuckets.forEach(i -> Assert.assertNull(i.getLink()));
+ linkService.populateLinks(extensionRepoBuckets);
+ extensionRepoBuckets.forEach(i -> Assert.assertEquals(
+ "extensions/repo/" + i.getBucketName(),
+ i.getLink().getUri().toString())
+ );
+ }
+
+ @Test
+ public void testPopulateExtensionRepoGroupLinks() {
+ extensionRepoGroups.forEach(i -> Assert.assertNull(i.getLink()));
+ linkService.populateLinks(extensionRepoGroups);
+ extensionRepoGroups.forEach(i -> {
+ Assert.assertEquals(
+ "extensions/repo/" + i.getBucketName() + "/" + i.getGroupId(),
+ i.getLink().getUri().toString()); }
+ );
+ }
+
+ @Test
+ public void testPopulateExtensionRepoArtifactLinks() {
+ extensionRepoArtifacts.forEach(i -> Assert.assertNull(i.getLink()));
+ linkService.populateLinks(extensionRepoArtifacts);
+ extensionRepoArtifacts.forEach(i -> {
+ Assert.assertEquals(
+ "extensions/repo/" + i.getBucketName() + "/" + i.getGroupId() + "/" + i.getArtifactId(),
+ i.getLink().getUri().toString()); }
+ );
+ }
+
+ @Test
+ public void testPopulateExtensionRepoVersionLinks() {
+ extensionRepoVersions.forEach(i -> Assert.assertNull(i.getLink()));
+ linkService.populateLinks(extensionRepoVersions);
+ extensionRepoVersions.forEach(i -> {
+ Assert.assertEquals(
+ "extensions/repo/" + i.getBucketName() + "/" + i.getGroupId() + "/" + i.getArtifactId() + "/" + i.getVersion(),
+ i.getLink().getUri().toString()); }
+ );
+ }
+
+ @Test
+ public void testPopulateExtensionRepoVersionFullLinks() {
+ extensionRepoVersions.forEach(i -> Assert.assertNull(i.getLink()));
+ linkService.populateFullLinks(extensionRepoVersions, baseUri);
+ extensionRepoVersions.forEach(i -> {
+ Assert.assertEquals(
+ BASE_URI + "/extensions/repo/" + i.getBucketName() + "/" + i.getGroupId() + "/" + i.getArtifactId() + "/" + i.getVersion(),
+ i.getLink().getUri().toString()); }
+ );
+ }
}
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties
index efa0290..721b949 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/application.properties
@@ -23,3 +23,6 @@
#logging.level.org.springframework.core.io.support: DEBUG
#logging.level.org.springframework.context.annotation: DEBUG
#logging.level.org.springframework.web: DEBUG
+
+# Need to allow overriding of beans in integration tests
+spring.main.allow-bean-definition-overriding=true
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml
index fd002be..c5609ec 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/providers.xml
@@ -22,4 +22,9 @@
<property name="Flow Storage Directory">./target/test-classes/flow_storage</property>
</flowPersistenceProvider>
+ <extensionBundlePersistenceProvider>
+ <class>org.apache.nifi.registry.provider.extension.FileSystemExtensionBundlePersistenceProvider</class>
+ <property name="Extension Bundle Storage Directory">./target/test-classes/extension_bundles</property>
+ </extensionBundlePersistenceProvider>
+
</providers>
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties
index 113773c..70ca5e3 100644
--- a/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/conf/unsecured/nifi-registry.properties
@@ -21,5 +21,8 @@
# providers properties #
nifi.registry.providers.configuration.file=./target/test-classes/conf/providers.xml
+# extensions working dir #
+nifi.registry.extensions.working.directory=./target/work/extensions
+
# database properties
nifi.registry.db.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE
\ No newline at end of file
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar
new file mode 100644
index 0000000..4a91f31
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-foo-nar-1.0.0.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar
new file mode 100644
index 0000000..c3fccf3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-1.0.0.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-bad-manifest.nar b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-bad-manifest.nar
new file mode 100644
index 0000000..4400589
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-bad-manifest.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-diff-checksum.nar b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-diff-checksum.nar
new file mode 100644
index 0000000..7055b64
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-diff-checksum.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-missing-manifest.nar b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-missing-manifest.nar
new file mode 100644
index 0000000..cbee3ab
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-missing-manifest.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-no-dependency.nar b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-no-dependency.nar
new file mode 100644
index 0000000..68e78d3
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0-no-dependency.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0.nar b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0.nar
new file mode 100644
index 0000000..8af4909
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/test/resources/extensions/nars/nifi-test-nar-2.0.0.nar
Binary files differ
diff --git a/nifi-registry-core/nifi-registry-web-ui/pom.xml b/nifi-registry-core/nifi-registry-web-ui/pom.xml
index e8c7548..4a0f4e7 100644
--- a/nifi-registry-core/nifi-registry-web-ui/pom.xml
+++ b/nifi-registry-core/nifi-registry-web-ui/pom.xml
@@ -216,6 +216,53 @@
</resources>
</configuration>
</execution>
+
+ <!--
+ Stage the final bundle of JS to be included in the .war
+ -->
+ <execution>
+ <id>copy-web-ui-bundle</id>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>copy-resources</goal>
+ </goals>
+ <configuration>
+ <outputDirectory>${project.build.directory}/${project.build.finalName}
+ </outputDirectory>
+ <resources>
+ <resource>
+ <directory>${frontend.working.dir}/webapp</directory>
+ <filtering>false</filtering>
+ <includes>
+ <include>nf-registry.bundle.*</include>
+ </includes>
+ </resource>
+ </resources>
+ </configuration>
+ </execution>
+ <!--
+ Stage the localization files to be included in the .war
+ -->
+ <execution>
+ <id>copy-localization</id>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>copy-resources</goal>
+ </goals>
+ <configuration>
+ <outputDirectory>${project.build.directory}/${project.build.finalName}
+ </outputDirectory>
+ <resources>
+ <resource>
+ <directory>${frontend.working.dir}/locale</directory>
+ <filtering>false</filtering>
+ <includes>
+ <include>*</include>
+ </includes>
+ </resource>
+ </resources>
+ </configuration>
+ </execution>
</executions>
</plugin>
<!--
@@ -307,58 +354,6 @@
</executions>
</plugin>
<plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-resources-plugin</artifactId>
- <executions>
- <!--
- Stage the final bundle of JS to be included in the .war
- -->
- <execution>
- <id>copy-web-ui-bundle</id>
- <phase>prepare-package</phase>
- <goals>
- <goal>copy-resources</goal>
- </goals>
- <configuration>
- <outputDirectory>${project.build.directory}/${project.build.finalName}
- </outputDirectory>
- <resources>
- <resource>
- <directory>${frontend.working.dir}/webapp</directory>
- <filtering>false</filtering>
- <includes>
- <include>nf-registry.bundle.*</include>
- </includes>
- </resource>
- </resources>
- </configuration>
- </execution>
- <!--
- Stage the localization files to be included in the .war
- -->
- <execution>
- <id>copy-localization</id>
- <phase>prepare-package</phase>
- <goals>
- <goal>copy-resources</goal>
- </goals>
- <configuration>
- <outputDirectory>${project.build.directory}/${project.build.finalName}
- </outputDirectory>
- <resources>
- <resource>
- <directory>${frontend.working.dir}/locale</directory>
- <filtering>false</filtering>
- <includes>
- <include>*</include>
- </includes>
- </resource>
- </resources>
- </configuration>
- </execution>
- </executions>
- </plugin>
- <plugin>
<groupId>org.apache.rat</groupId>
<artifactId>apache-rat-plugin</artifactId>
<configuration>
diff --git a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html
index 8fbd6d4..fa41287 100644
--- a/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html
+++ b/nifi-registry-core/nifi-registry-web-ui/src/main/webapp/components/explorer/grid-list/registry/nf-registry-grid-list-viewer.html
@@ -44,7 +44,7 @@
[disabled]="disabled" (expanded)="nfRegistryService.getDropletSnapshotMetadata(droplet)">
<ng-template td-expansion-panel-label>
<div fxLayout="column" fxLayoutAlign="space-between start">
- <span class="md-title capitalize">{{droplet.name}} - {{droplet.bucketName}}</span>
+ <span class="md-title">{{droplet.name}} - {{droplet.bucketName}}</span>
<span class="md-subhead">{{droplet.type}}</span>
</div>
</ng-template>
diff --git a/nifi-registry-core/pom.xml b/nifi-registry-core/pom.xml
index 13c7bd2..e09a698 100644
--- a/nifi-registry-core/pom.xml
+++ b/nifi-registry-core/pom.xml
@@ -45,6 +45,7 @@
<module>nifi-registry-docs</module>
<module>nifi-registry-client</module>
<module>nifi-registry-docker</module>
+ <module>nifi-registry-bundle-utils</module>
</modules>
<dependencyManagement>
@@ -97,12 +98,7 @@
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
- <version>2.0.0.Final</version>
- </dependency>
- <dependency>
- <groupId>org.hibernate</groupId>
- <artifactId>hibernate-validator</artifactId>
- <version>6.0.2.Final</version>
+ <version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
@@ -140,6 +136,11 @@
<artifactId>swagger-annotations</artifactId>
<version>1.5.16</version>
</dependency>
+ <dependency>
+ <groupId>org.glassfish.jersey.media</groupId>
+ <artifactId>jersey-media-multipart</artifactId>
+ <version>${jersey.version}</version>
+ </dependency>
</dependencies>
</dependencyManagement>
diff --git a/pom.xml b/pom.xml
index 57d8f22..323c703 100644
--- a/pom.xml
+++ b/pom.xml
@@ -92,11 +92,12 @@
<org.slf4j.version>1.7.12</org.slf4j.version>
<jetty.version>9.4.11.v20180605</jetty.version>
<jax.rs.api.version>2.1</jax.rs.api.version>
- <jersey.version>2.26</jersey.version>
- <jackson.version>2.9.6</jackson.version>
- <spring.boot.version>2.0.4.RELEASE</spring.boot.version>
- <spring.security.version>5.0.7.RELEASE</spring.security.version>
- <flyway.version>4.2.0</flyway.version>
+ <jersey.version>2.27</jersey.version>
+ <jackson.version>2.9.7</jackson.version>
+ <spring.boot.version>2.1.0.RELEASE</spring.boot.version>
+ <spring.security.version>5.1.1.RELEASE</spring.security.version>
+ <flyway.version>5.2.1</flyway.version>
+ <flyway.tests.version>5.1.0</flyway.tests.version>
<swagger.ui.version>3.12.0</swagger.ui.version>
</properties>