You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by kd...@apache.org on 2018/11/30 18:56:33 UTC

[6/7] nifi-registry git commit: NIFIREG-211 Initial work for adding extenion bundles to NiFi Registry

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-client/src/main/java/org/apache/nifi/registry/client/impl/JerseyNiFiRegistryClient.java
----------------------------------------------------------------------
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 com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
 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.client.UserClient;
 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 @@ public class JerseyNiFiRegistryClient implements NiFiRegistryClient {
         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 @@ public class JerseyNiFiRegistryClient implements NiFiRegistryClient {
         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);
 

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/bucket/BucketItemType.java
----------------------------------------------------------------------
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 @@ public enum BucketItemType {
 
     // 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;
 }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundle.java
----------------------------------------------------------------------
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;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleType.java
----------------------------------------------------------------------
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;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleTypeAdapter.java
----------------------------------------------------------------------
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();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersion.java
----------------------------------------------------------------------
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());
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionDependency.java
----------------------------------------------------------------------
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);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/ExtensionBundleVersionMetadata.java
----------------------------------------------------------------------
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);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoArtifact.java
----------------------------------------------------------------------
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());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoBucket.java
----------------------------------------------------------------------
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());
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoGroup.java
----------------------------------------------------------------------
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());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersion.java
----------------------------------------------------------------------
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;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-data-model/src/main/java/org/apache/nifi/registry/extension/repo/ExtensionRepoVersionSummary.java
----------------------------------------------------------------------
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());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-framework/pom.xml
----------------------------------------------------------------------
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>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/CustomFlywayMigrationStrategy.java
----------------------------------------------------------------------
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 @@ public class CustomFlywayMigrationStrategy implements FlywayMigrationStrategy {
 
     @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 @@ public class CustomFlywayMigrationStrategy implements FlywayMigrationStrategy {
         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);
         }
     }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/DatabaseMetadataService.java
----------------------------------------------------------------------
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 @@ package org.apache.nifi.registry.db;
 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 @@ public class DatabaseMetadataService implements MetadataService {
 
     @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 @@ public class DatabaseMetadataService implements MetadataService {
 
     //----------------- 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 @@ public class DatabaseMetadataService implements MetadataService {
             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 @@ public class DatabaseMetadataService implements MetadataService {
 
     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 @@ public class DatabaseMetadataService implements MetadataService {
                     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 @@ public class DatabaseMetadataService implements MetadataService {
         });
     }
 
+    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 @@ public class DatabaseMetadataService implements MetadataService {
 
     @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 @@ public class DatabaseMetadataService implements MetadataService {
         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() {

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/BucketItemEntityType.java
----------------------------------------------------------------------
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 @@ package org.apache.nifi.registry.db.entity;
  */
 public enum BucketItemEntityType {
 
-    FLOW(Values.FLOW);
+    FLOW(Values.FLOW),
+
+    EXTENSION_BUNDLE(Values.EXTENSION_BUNDLE);
+
 
     private final String value;
 
@@ -37,6 +40,7 @@ public enum BucketItemEntityType {
     // 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";
     }
 
 }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleEntity.java
----------------------------------------------------------------------
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);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleEntityType.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleEntityType.java b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleEntityType.java
new file mode 100644
index 0000000..0f4950c
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleEntityType.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+/**
+ * The possible types of extension bundles.
+ */
+public enum ExtensionBundleEntityType {
+
+    NIFI_NAR,
+
+    MINIFI_CPP;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionDependencyEntity.java
----------------------------------------------------------------------
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;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/f1e5aef7/nifi-registry-core/nifi-registry-framework/src/main/java/org/apache/nifi/registry/db/entity/ExtensionBundleVersionEntity.java
----------------------------------------------------------------------
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;
+    }
+}