org.apache.nifi.registry.service.extension.StandardExtensionService.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.nifi.registry.service.extension.StandardExtensionService.java

Source

/*
 * 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.filter.ExtensionBundleFilterParams;
import org.apache.nifi.registry.extension.filter.ExtensionBundleVersionFilterParams;
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.Comparator;
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);

    static final String SNAPSHOT_VERSION_SUFFIX = "SNAPSHOT";

    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();

            final boolean isSnapshotVersion = version.endsWith(SNAPSHOT_VERSION_SUFFIX);
            final boolean overwriteBundleVersion = isSnapshotVersion
                    || existingBucket.isAllowExtensionBundleRedeploy();

            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, or if its a snapshot version
            // 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) && !isSnapshotVersion) {
                    throw new IllegalStateException(
                            "Found existing extension bundle with same group, artifact, and version, but different SHA-256 checksums");
                }
            }

            // 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);

            // check if the version of incoming bundle already exists in the bucket
            // if it exists and it is a snapshot version or the bucket allows redeploying, then first delete the row in the extension_bundle_version table so we can create a new one
            // otherwise we throw an exception because we don't allow the same version in the same bucket
            final ExtensionBundleVersionEntity existingVersion = metadataService
                    .getExtensionBundleVersion(bucketIdentifier, groupId, artifactId, version);
            if (existingVersion != null) {
                if (overwriteBundleVersion) {
                    metadataService.deleteExtensionBundleVersion(existingVersion);
                } else {
                    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);
            versionMetadata.setContentSize(extensionWorkingFile.length());

            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, overwriteBundleVersion);
                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(final Set<String> bucketIdentifiers,
            final ExtensionBundleFilterParams filterParams) {
        if (bucketIdentifiers == null) {
            throw new IllegalArgumentException("Bucket identifiers cannot be null");
        }

        final List<ExtensionBundleEntity> bundleEntities = metadataService.getExtensionBundles(bucketIdentifiers,
                filterParams == null ? ExtensionBundleFilterParams.empty() : filterParams);
        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 Set<String> bucketIdentifiers,
            final ExtensionBundleVersionFilterParams filterParams) {
        if (bucketIdentifiers == null) {
            throw new IllegalArgumentException("Bucket identifiers cannot be null");
        }

        final SortedSet<ExtensionBundleVersionMetadata> sortedVersions = new TreeSet<>(
                Comparator.comparing(ExtensionBundleVersionMetadata::getExtensionBundleId)
                        .thenComparing(ExtensionBundleVersionMetadata::getVersion));

        final List<ExtensionBundleVersionEntity> bundleVersionEntities = metadataService.getExtensionBundleVersions(
                bucketIdentifiers,
                filterParams == null ? ExtensionBundleVersionFilterParams.empty() : filterParams);
        if (bundleVersionEntities != null) {
            bundleVersionEntities.forEach(bv -> sortedVersions.add(DataModelMapper.map(bv)));
        }
        return sortedVersions;
    }

    @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(
            final ExtensionBundleEntity existingBundle) {
        final SortedSet<ExtensionBundleVersionMetadata> sortedVersions = new TreeSet<>(Collections.reverseOrder());

        final List<ExtensionBundleVersionEntity> existingVersions = metadataService
                .getExtensionBundleVersions(existingBundle.getId());
        if (existingVersions != null) {
            existingVersions.stream().forEach(s -> sortedVersions.add(DataModelMapper.map(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(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();
    }
}