blob: c7cca08cffe32d5c1806060c46ce28bb7b67ef72 [file] [log] [blame]
/*
* 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.bundle.extract.BundleExtractor;
import org.apache.nifi.registry.bundle.model.BundleDetails;
import org.apache.nifi.registry.bundle.model.BundleIdentifier;
import org.apache.nifi.registry.db.entity.BucketEntity;
import org.apache.nifi.registry.db.entity.BundleEntity;
import org.apache.nifi.registry.db.entity.BundleVersionDependencyEntity;
import org.apache.nifi.registry.db.entity.BundleVersionEntity;
import org.apache.nifi.registry.db.entity.ExtensionAdditionalDetailsEntity;
import org.apache.nifi.registry.db.entity.ExtensionEntity;
import org.apache.nifi.registry.exception.ResourceNotFoundException;
import org.apache.nifi.registry.extension.BundleContext;
import org.apache.nifi.registry.extension.BundlePersistenceProvider;
import org.apache.nifi.registry.extension.bundle.BuildInfo;
import org.apache.nifi.registry.extension.bundle.Bundle;
import org.apache.nifi.registry.extension.bundle.BundleFilterParams;
import org.apache.nifi.registry.extension.bundle.BundleType;
import org.apache.nifi.registry.extension.bundle.BundleVersion;
import org.apache.nifi.registry.extension.bundle.BundleVersionDependency;
import org.apache.nifi.registry.extension.bundle.BundleVersionFilterParams;
import org.apache.nifi.registry.extension.bundle.BundleVersionMetadata;
import org.apache.nifi.registry.extension.component.ExtensionFilterParams;
import org.apache.nifi.registry.extension.component.ExtensionMetadata;
import org.apache.nifi.registry.extension.component.TagCount;
import org.apache.nifi.registry.extension.component.manifest.Extension;
import org.apache.nifi.registry.extension.component.manifest.ProvidedServiceAPI;
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.StandardBundleContext;
import org.apache.nifi.registry.security.authorization.user.NiFiUserUtils;
import org.apache.nifi.registry.serialization.Serializer;
import org.apache.nifi.registry.service.MetadataService;
import org.apache.nifi.registry.service.extension.docs.DocumentationConstants;
import org.apache.nifi.registry.service.extension.docs.ExtensionDocWriter;
import org.apache.nifi.registry.service.mapper.BucketMappings;
import org.apache.nifi.registry.service.mapper.ExtensionMappings;
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.nio.charset.StandardCharsets;
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 Serializer<Extension> extensionSerializer;
private final ExtensionDocWriter extensionDocWriter;
private final MetadataService metadataService;
private final Map<BundleType, BundleExtractor> extractors;
private final BundlePersistenceProvider bundlePersistenceProvider;
private final Validator validator;
private final File extensionsWorkingDir;
@Autowired
public StandardExtensionService(final Serializer<Extension> extensionSerializer,
final ExtensionDocWriter extensionDocWriter,
final MetadataService metadataService,
final Map<BundleType, BundleExtractor> extractors,
final BundlePersistenceProvider bundlePersistenceProvider,
final Validator validator,
final NiFiRegistryProperties properties) {
this.extensionSerializer = extensionSerializer;
this.extensionDocWriter = extensionDocWriter;
this.metadataService = metadataService;
this.extractors = extractors;
this.bundlePersistenceProvider = bundlePersistenceProvider;
this.validator = validator;
this.extensionsWorkingDir = properties.getExtensionsWorkingDirectory();
Validate.notNull(this.extensionSerializer);
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 BundleVersion createBundleVersion(final String bucketIdentifier, final BundleType 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 = getBucketEntity(bucketIdentifier);
// 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 BundleIdentifier bundleIdentifier = bundleDetails.getBundleIdentifier();
final BuildInfo buildInfo = bundleDetails.getBuildInfo();
final String groupId = bundleIdentifier.getGroupId();
final String artifactId = bundleIdentifier.getArtifactId();
final String version = bundleIdentifier.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<BundleVersionEntity> allExistingVersions = metadataService.getBundleVersionsGlobal(groupId, artifactId, version);
for (final BundleVersionEntity 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 BundleEntity bundleEntity = 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 BundleVersionEntity existingVersion = metadataService.getBundleVersion(bucketIdentifier, groupId, artifactId, version);
if (existingVersion != null) {
if (overwriteBundleVersion) {
LOGGER.debug("Bundle overwriting allowed, deleting existing version...");
metadataService.deleteBundleVersion(existingVersion);
} else {
LOGGER.warn("The specified version [{}] already exists for extension bundle [{}].", new Object[]{version, bundleEntity.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 BundleVersionMetadata versionMetadata = new BundleVersionMetadata();
versionMetadata.setId(UUID.randomUUID().toString());
versionMetadata.setBundleId(bundleEntity.getId());
versionMetadata.setBucketId(bucketIdentifier);
versionMetadata.setVersion(version);
versionMetadata.setTimestamp(currentTime);
versionMetadata.setAuthor(userIdentity);
versionMetadata.setSha256(sha256Hex);
versionMetadata.setSha256Supplied(sha256Supplied);
versionMetadata.setContentSize(extensionWorkingFile.length());
versionMetadata.setSystemApiVersion(bundleDetails.getSystemApiVersion());
versionMetadata.setBuildInfo(buildInfo);
validate(versionMetadata, "Cannot create extension bundle version");
// create the bundle version in the metadata db
final BundleVersionEntity versionEntity = ExtensionMappings.map(versionMetadata);
metadataService.createBundleVersion(versionEntity);
// create and persist the version dependencies in the metadata db
final Set<BundleVersionDependencyEntity> dependencyEntities = getDependencyEntities(versionEntity, bundleDetails);
dependencyEntities.forEach(d -> metadataService.createDependency(d));
// create and persist extensions in the metadata db
final Set<ExtensionEntity> extensionEntities = getExtensionEntities(versionEntity, bundleDetails);
extensionEntities.forEach(e -> metadataService.createExtension(e));
// persist the content of the bundle to the persistence provider
persistBundleVersionContent(bundleType, existingBucket, bundleEntity, versionEntity, extensionWorkingFile, overwriteBundleVersion);
// get the updated extension bundle so it contains the correct version count
final BundleEntity updatedBundle = metadataService.getBundle(bucketIdentifier, groupId, artifactId);
// create the full BundleVersion instance to return
final BundleVersion bundleVersion = new BundleVersion();
bundleVersion.setVersionMetadata(versionMetadata);
bundleVersion.setBundle(ExtensionMappings.map(existingBucket, updatedBundle));
bundleVersion.setBucket(BucketMappings.map(existingBucket));
final Set<BundleVersionDependency> dependencies = new HashSet<>();
dependencyEntities.forEach(d -> dependencies.add(ExtensionMappings.map(d)));
bundleVersion.setDependencies(dependencies);
LOGGER.debug("Created bundle - '{}:{}:{}'", new Object[]{groupId, artifactId, version});
return bundleVersion;
} finally {
if (extensionWorkingFile.exists()) {
try {
extensionWorkingFile.delete();
} catch (Exception e) {
LOGGER.warn("Error removing temporary extension bundle file at {}",
new Object[]{extensionWorkingFile.getAbsolutePath()});
}
}
}
}
private Set<BundleVersionDependencyEntity> getDependencyEntities(final BundleVersionEntity versionEntity, final BundleDetails bundleDetails) {
final Set<BundleIdentifier> dependencyCoordinates = bundleDetails.getDependencies();
if (dependencyCoordinates == null) {
return Collections.emptySet();
}
final Set<BundleVersionDependencyEntity> versionDependencies = new HashSet<>();
for (final BundleIdentifier dependencyCoordinate : dependencyCoordinates) {
final BundleVersionDependency versionDependency = new BundleVersionDependency();
versionDependency.setGroupId(dependencyCoordinate.getGroupId());
versionDependency.setArtifactId(dependencyCoordinate.getArtifactId());
versionDependency.setVersion(dependencyCoordinate.getVersion());
validate(versionDependency, "Cannot create extension bundle version dependency");
final BundleVersionDependencyEntity versionDependencyEntity = ExtensionMappings.map(versionDependency);
versionDependencyEntity.setId(UUID.randomUUID().toString());
versionDependencyEntity.setExtensionBundleVersionId(versionEntity.getId());
versionDependencies.add(versionDependencyEntity);
}
return versionDependencies;
}
private Set<ExtensionEntity> getExtensionEntities(final BundleVersionEntity versionEntity, final BundleDetails bundleDetails) {
final Set<Extension> extensions = bundleDetails.getExtensions();
if (extensions == null) {
return Collections.emptySet();
}
final Set<ExtensionEntity> extensionEntities = new HashSet<>();
final Map<String,String> additionalDetails = bundleDetails.getAdditionalDetails();
for (final Extension extension : extensions) {
validate(extension, "Invalid extension due to one or more constraint violations");
// Convert Extension to ExtensionEntity and populate ids
final ExtensionEntity extensionEntity = ExtensionMappings.map(extension, extensionSerializer);
extensionEntity.setId(UUID.randomUUID().toString());
extensionEntity.setBundleVersionId(versionEntity.getId());
extensionEntity.getRestrictions().forEach(r -> {
r.setId(UUID.randomUUID().toString());
r.setExtensionId(extensionEntity.getId());
});
extensionEntity.getProvidedServiceApis().forEach(p -> {
p.setId(UUID.randomUUID().toString());
p.setExtensionId(extensionEntity.getId());
});
// Check the additionalDetails map to see if there is an entry, and if so populate it
final String additionalDetailsContent = additionalDetails.get(extensionEntity.getName());
if (!StringUtils.isBlank(additionalDetailsContent)) {
LOGGER.debug("Found additional details documentation for extension '{}'", new Object[]{extensionEntity.getName()});
extensionEntity.setAdditionalDetails(additionalDetailsContent);
}
extensionEntities.add(extensionEntity);
}
return extensionEntities;
}
private BundleEntity getOrCreateExtensionBundle(final String bucketId, final String groupId, final String artifactId,
final BundleType bundleType, final long currentTime) {
BundleEntity existingBundleEntity = metadataService.getBundle(bucketId, groupId, artifactId);
if (existingBundleEntity == null) {
final Bundle bundle = new Bundle();
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.createBundle(ExtensionMappings.map(bundle));
} else {
if (bundleType != 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 void persistBundleVersionContent(final BundleType bundleType, final BucketEntity bucket, final BundleEntity bundle,
final BundleVersionEntity bundleVersion, final File extensionWorkingFile,
final boolean overwriteBundleVersion) throws IOException {
final BundleContext context = new StandardBundleContext.Builder()
.bundleType(getProviderBundleType(bundleType))
.bucketId(bucket.getId())
.bucketName(bucket.getName())
.bundleId(bundle.getId())
.bundleGroupId(bundle.getGroupId())
.bundleArtifactId(bundle.getArtifactId())
.bundleVersion(bundleVersion.getVersion())
.author(bundleVersion.getCreatedBy())
.timestamp(bundleVersion.getCreated().getTime())
.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[]{context.getBundleGroupId(), context.getBundleArtifactId(), context.getBundleVersion()});
}
}
private BundleContext.BundleType getProviderBundleType(final BundleType bundleType) {
switch (bundleType) {
case NIFI_NAR:
return BundleContext.BundleType.NIFI_NAR;
case MINIFI_CPP:
return BundleContext.BundleType.MINIFI_CPP;
default:
throw new IllegalArgumentException("Unknown bundle type: " + bundleType.toString());
}
}
@Override
public List<Bundle> getBundles(final Set<String> bucketIdentifiers, final BundleFilterParams filterParams) {
if (bucketIdentifiers == null) {
throw new IllegalArgumentException("Bucket identifiers cannot be null");
}
final List<BundleEntity> bundleEntities = metadataService.getBundles(bucketIdentifiers,
filterParams == null ? BundleFilterParams.empty() : filterParams);
return bundleEntities.stream().map(b -> ExtensionMappings.map(null, b)).collect(Collectors.toList());
}
@Override
public List<Bundle> getBundlesByBucket(final String bucketIdentifier) {
if (StringUtils.isBlank(bucketIdentifier)) {
throw new IllegalArgumentException("Bucket identifier cannot be null or blank");
}
final BucketEntity existingBucket = getBucketEntity(bucketIdentifier);
final List<BundleEntity> bundleEntities = metadataService.getBundlesByBucket(bucketIdentifier);
return bundleEntities.stream().map(b -> ExtensionMappings.map(existingBucket, b)).collect(Collectors.toList());
}
@Override
public Bundle getBundle(final String bundleIdentifier) {
if (StringUtils.isBlank(bundleIdentifier)) {
throw new IllegalArgumentException("Bundle identifier cannot be null or blank");
}
final BundleEntity existingBundle = getBundleEntity(bundleIdentifier);
final BucketEntity existingBucket = getBucketEntity(existingBundle.getBucketId());
return ExtensionMappings.map(existingBucket, existingBundle);
}
@Override
public Bundle deleteBundle(final Bundle bundle) {
if (bundle == null) {
throw new IllegalArgumentException("Extension bundle cannot be null");
}
// delete the bundle from the database
metadataService.deleteBundle(bundle.getIdentifier());
// delete all content associated with the bundle in the persistence provider
bundlePersistenceProvider.deleteAllBundleVersions(
bundle.getBucketIdentifier(),
bundle.getBucketName(),
bundle.getGroupId(),
bundle.getArtifactId());
return bundle;
}
// ---- BundleVersion methods -----
@Override
public SortedSet<BundleVersionMetadata> getBundleVersions(final Set<String> bucketIdentifiers,
final BundleVersionFilterParams filterParams) {
if (bucketIdentifiers == null) {
throw new IllegalArgumentException("Bucket identifiers cannot be null");
}
final SortedSet<BundleVersionMetadata> sortedVersions = new TreeSet<>(
Comparator.comparing(BundleVersionMetadata::getBundleId)
.thenComparing(BundleVersionMetadata::getVersion)
);
final List<BundleVersionEntity> bundleVersionEntities = metadataService.getBundleVersions(bucketIdentifiers,
filterParams == null ? BundleVersionFilterParams.empty() : filterParams);
if (bundleVersionEntities != null) {
bundleVersionEntities.forEach(bv -> sortedVersions.add(ExtensionMappings.map(bv)));
}
return sortedVersions;
}
@Override
public SortedSet<BundleVersionMetadata> getBundleVersions(final String bundleIdentifier) {
if (StringUtils.isBlank(bundleIdentifier)) {
throw new IllegalArgumentException("Extension bundle identifier cannot be null or blank");
}
// ensure the bundle exists
final BundleEntity existingBundle = getBundleEntity(bundleIdentifier);
return getExtensionBundleVersionsSet(existingBundle);
}
private SortedSet<BundleVersionMetadata> getExtensionBundleVersionsSet(final BundleEntity existingBundle) {
final SortedSet<BundleVersionMetadata> sortedVersions = new TreeSet<>(Collections.reverseOrder());
final List<BundleVersionEntity> existingVersions = metadataService.getBundleVersions(existingBundle.getId());
if (existingVersions != null) {
existingVersions.stream().forEach(s -> sortedVersions.add(ExtensionMappings.map(s)));
}
return sortedVersions;
}
@Override
public BundleVersion getBundleVersion(final String bucketId, final String bundleId, final String version) {
if (StringUtils.isBlank(bucketId)) {
throw new IllegalArgumentException("Bucket id cannot be null or blank");
}
if (StringUtils.isBlank(bundleId)) {
throw new IllegalArgumentException("Bundle id cannot be null or blank");
}
if (StringUtils.isBlank(version)) {
throw new IllegalArgumentException("Version cannot be null or blank");
}
// ensure the bucket exists
final BucketEntity existingBucket = getBucketEntity(bucketId);
// ensure the bundle exists
final BundleEntity existingBundle = getBundleEntity(bundleId);
if (!existingBucket.getId().equals(existingBundle.getBucketId())) {
throw new IllegalStateException("The requested bundle is not located in the given bucket");
}
// retrieve the version of the bundle...
final BundleVersionEntity existingVersion = metadataService.getBundleVersion(bundleId, version);
if (existingVersion == null) {
LOGGER.warn("The specified version [{}] does not exist for extension bundle [{}].", new Object[]{version, bundleId});
throw new ResourceNotFoundException("The specified extension bundle version does not exist.");
}
return getBundleVersion(existingBucket, existingBundle, existingVersion);
}
@Override
public BundleVersion getBundleVersion(final String bucketId, final String groupId, final String artifactId, final String version) {
if (StringUtils.isBlank(bucketId)) {
throw new IllegalArgumentException("Bucket id cannot be null or blank");
}
if (StringUtils.isBlank(groupId)) {
throw new IllegalArgumentException("Group id cannot be null or blank");
}
if (StringUtils.isBlank(artifactId)) {
throw new IllegalArgumentException("Artifact id cannot be null or blank");
}
if (StringUtils.isBlank(version)) {
throw new IllegalArgumentException("Version cannot be null or blank");
}
// ensure the bucket exists
final BucketEntity existingBucket = getBucketEntity(bucketId);
// ensure the bundle exists
final BundleEntity existingBundle = metadataService.getBundle(bucketId, groupId, artifactId);
if (existingBundle == null) {
LOGGER.warn("The specified extension bundle [{}-{}-{}] does not exist.", new Object[]{bucketId, groupId, artifactId});
throw new ResourceNotFoundException("The specified extension bundle does not exist in this bucket.");
}
//ensure the version of the bundle exists
final BundleVersionEntity existingVersion = metadataService.getBundleVersion(bucketId, groupId, artifactId, version);
if (existingVersion == null) {
LOGGER.warn("The specified extension bundle version [{}-{}-{}-{}] does not exist.", new Object[]{bucketId, groupId, artifactId, version});
throw new ResourceNotFoundException("The specified extension bundle version does not exist in this bucket.");
}
// get the dependencies for the bundle version
return getBundleVersion(existingBucket, existingBundle, existingVersion);
}
private BundleVersion getBundleVersion(final BucketEntity existingBucket, final BundleEntity existingBundle, final BundleVersionEntity existingVersion) {
// get the dependencies for the bundle version
final List<BundleVersionDependencyEntity> existingVersionDependencies = metadataService
.getDependenciesForBundleVersion(existingVersion.getId());
// convert the dependency db entities
final Set<BundleVersionDependency> dependencies = existingVersionDependencies.stream()
.map(d -> ExtensionMappings.map(d))
.collect(Collectors.toSet());
// create the full BundleVersion instance to return
final BundleVersion bundleVersion = new BundleVersion();
bundleVersion.setVersionMetadata(ExtensionMappings.map(existingVersion));
bundleVersion.setBundle(ExtensionMappings.map(existingBucket, existingBundle));
bundleVersion.setBucket(BucketMappings.map(existingBucket));
bundleVersion.setDependencies(dependencies);
return bundleVersion;
}
@Override
public void writeBundleVersionContent(final BundleVersion bundleVersion, final OutputStream out) {
// get the content from the persistence provider and write it to the output stream
final BundleContext context = getExtensionBundleContext(bundleVersion);
bundlePersistenceProvider.getBundleVersion(context, out);
}
@Override
public BundleVersion deleteBundleVersion(final BundleVersion 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.deleteBundleVersion(extensionBundleVersionId);
// delete content associated with the bundle version in the persistence provider
final BundleContext context = new StandardBundleContext.Builder()
.bundleType(getProviderBundleType(bundleVersion.getBundle().getBundleType()))
.bucketId(bundleVersion.getBucket().getIdentifier())
.bucketName(bundleVersion.getBucket().getName())
.bundleId(bundleVersion.getBundle().getIdentifier())
.bundleGroupId(bundleVersion.getBundle().getGroupId())
.bundleArtifactId(bundleVersion.getBundle().getArtifactId())
.bundleVersion(bundleVersion.getVersionMetadata().getVersion())
.author(bundleVersion.getVersionMetadata().getAuthor())
.timestamp(bundleVersion.getVersionMetadata().getTimestamp())
.build();
bundlePersistenceProvider.deleteBundleVersion(context);
return bundleVersion;
}
// ------ Extension Methods ----
@Override
public SortedSet<ExtensionMetadata> getExtensionMetadata(final Set<String> bucketIdentifiers, final ExtensionFilterParams filterParams) {
if (bucketIdentifiers == null) {
throw new IllegalArgumentException("Bucket identifiers cannot be null");
}
// retrieve the extension entities
final List<ExtensionEntity> extensionEntities = metadataService.getExtensions(bucketIdentifiers, filterParams);
return getExtensionMetadata(extensionEntities);
}
@Override
public SortedSet<ExtensionMetadata> getExtensionMetadata(final Set<String> bucketIdentifiers, final ProvidedServiceAPI serviceAPI) {
if (bucketIdentifiers == null) {
throw new IllegalArgumentException("Bucket identifiers cannot be null");
}
if (serviceAPI == null
|| StringUtils.isBlank(serviceAPI.getClassName())
|| StringUtils.isBlank(serviceAPI.getGroupId())
|| StringUtils.isBlank(serviceAPI.getArtifactId())
|| StringUtils.isBlank(serviceAPI.getVersion())) {
throw new IllegalArgumentException("Provided service API must be specified with a class, group, artifact, and version");
}
// retrieve the extension entities
final List<ExtensionEntity> extensionEntities = metadataService.getExtensionsByProvidedServiceApi(bucketIdentifiers, serviceAPI);
return getExtensionMetadata(extensionEntities);
}
private SortedSet<ExtensionMetadata> getExtensionMetadata(List<ExtensionEntity> extensionEntities) {
// map to extension metadata and sort by extension name
final SortedSet<ExtensionMetadata> extensions = new TreeSet<>();
extensionEntities.forEach(e -> {
final ExtensionMetadata metadata = ExtensionMappings.mapToMetadata(e, extensionSerializer);
extensions.add(metadata);
});
return extensions;
}
@Override
public SortedSet<ExtensionMetadata> getExtensionMetadata(final BundleVersion bundleVersion) {
if (bundleVersion == null) {
throw new IllegalArgumentException("Extension bundle version cannot be null");
}
// ensure the bundle version exists
final BundleVersionEntity existingBundleVersion = metadataService.getBundleVersion(
bundleVersion.getVersionMetadata().getBucketId(),
bundleVersion.getBundle().getGroupId(),
bundleVersion.getBundle().getArtifactId(),
bundleVersion.getVersionMetadata().getVersion());
if (existingBundleVersion == null) {
LOGGER.warn("The specified extension bundle version does not exist for [{}] - [{}] - [{}] - [{}]",
new Object[]{
bundleVersion.getVersionMetadata().getBucketId(),
bundleVersion.getBundle().getGroupId(),
bundleVersion.getBundle().getArtifactId(),
bundleVersion.getVersionMetadata().getVersion()});
throw new ResourceNotFoundException("The specified extension bundle version does not exist.");
}
// retrieve the extension entities
final List<ExtensionEntity> extensionEntities = metadataService.getExtensionsByBundleVersionId(existingBundleVersion.getId());
// map to extension and sort by extension name
final SortedSet<ExtensionMetadata> extensions = new TreeSet<>();
extensionEntities.forEach(e -> extensions.add(ExtensionMappings.mapToMetadata(e, extensionSerializer)));
return extensions;
}
@Override
public Extension getExtension(final BundleVersion bundleVersion, final String name) {
if (bundleVersion == null) {
throw new IllegalArgumentException("Bundle version cannot be null");
}
if (bundleVersion.getVersionMetadata() == null || StringUtils.isBlank(bundleVersion.getVersionMetadata().getId())) {
throw new IllegalArgumentException("Bundle version must contain a version metadata with a bundle version id");
}
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("Extension name cannot be null or blank");
}
final ExtensionEntity entity = metadataService.getExtensionByName(bundleVersion.getVersionMetadata().getId(), name);
if (entity == null) {
LOGGER.warn("The specified extension [{}] does not exist in the specified bundle version [{}].",
new Object[]{name, bundleVersion.getVersionMetadata().getId()});
throw new ResourceNotFoundException("The specified extension does not exist in this registry.");
}
return ExtensionMappings.map(entity, extensionSerializer);
}
@Override
public void writeExtensionDocs(final BundleVersion bundleVersion, final String name, final OutputStream outputStream)
throws IOException {
if (bundleVersion == null) {
throw new IllegalArgumentException("Bundle version cannot be null");
}
if (bundleVersion.getVersionMetadata() == null || StringUtils.isBlank(bundleVersion.getVersionMetadata().getId())) {
throw new IllegalArgumentException("Bundle version must contain a version metadata with a bundle version id");
}
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("Extension name cannot be null or blank");
}
if (outputStream == null) {
throw new IllegalArgumentException("Output stream cannot be null");
}
final ExtensionEntity entity = metadataService.getExtensionByName(bundleVersion.getVersionMetadata().getId(), name);
if (entity == null) {
LOGGER.warn("The specified extension [{}] does not exist in the specified bundle version [{}].",
new Object[]{name, bundleVersion.getVersionMetadata().getId()});
throw new ResourceNotFoundException("The specified extension does not exist in this registry.");
}
final ExtensionMetadata extensionMetadata = ExtensionMappings.mapToMetadata(entity, extensionSerializer);
final Extension extension = ExtensionMappings.map(entity, extensionSerializer);
extensionDocWriter.write(extensionMetadata, extension, outputStream);
}
@Override
public void writeAdditionalDetailsDocs(final BundleVersion bundleVersion, final String name, final OutputStream outputStream) throws IOException {
if (bundleVersion == null) {
throw new IllegalArgumentException("Bundle version cannot be null");
}
if (bundleVersion.getVersionMetadata() == null || StringUtils.isBlank(bundleVersion.getVersionMetadata().getId())) {
throw new IllegalArgumentException("Bundle version must contain a version metadata with a bundle version id");
}
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("Extension name cannot be null or blank");
}
if (outputStream == null) {
throw new IllegalArgumentException("Output stream cannot be null");
}
final ExtensionAdditionalDetailsEntity additionalDetailsEntity = metadataService.getExtensionAdditionalDetails(
bundleVersion.getVersionMetadata().getId(), name);
if (additionalDetailsEntity == null) {
LOGGER.warn("The specified extension [{}] does not exist in the specified bundle version [{}].",
new Object[]{name, bundleVersion.getVersionMetadata().getId()});
throw new ResourceNotFoundException("The specified extension does not exist in this registry.");
}
if (!additionalDetailsEntity.getAdditionalDetails().isPresent()) {
LOGGER.warn("The specified extension [{}] does not have additional details in the specified bundle version [{}].",
new Object[]{name, bundleVersion.getVersionMetadata().getId()});
throw new IllegalStateException("The specified extension does not have additional details.");
}
final String additionalDetailsContent = additionalDetailsEntity.getAdditionalDetails().get();
// The additional details content may have come from NiFi which has a different path to the css so we need to fix the location
final String componentUsageCssRef = DocumentationConstants.CSS_PATH + "component-usage.css";
final String updatedContent = additionalDetailsContent.replace("../../../../../css/component-usage.css", componentUsageCssRef);
IOUtils.write(updatedContent, outputStream, StandardCharsets.UTF_8);
}
@Override
public SortedSet<TagCount> getExtensionTags() {
final SortedSet<TagCount> tagCounts = new TreeSet<>();
metadataService.getAllExtensionTags().forEach(tc -> tagCounts.add(ExtensionMappings.map(tc)));
return tagCounts;
}
// ------ 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<BundleEntity> bundleEntities = metadataService.getBundlesByBucket(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<BundleEntity> bundleEntities = metadataService.getBundlesByBucketAndGroup(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<BundleVersionEntity> versionEntities = metadataService.getBundleVersions(bucket.getIdentifier(), groupId, artifactId);
if (!versionEntities.isEmpty()) {
final BundleEntity bundleEntity = metadataService.getBundle(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());
repoVersion.setAuthor(v.getCreatedBy());
repoVersion.setTimestamp(v.getCreated().getTime());
repoVersions.add(repoVersion);
});
}
return repoVersions;
}
// ------ Helper Methods -------
private BundleContext getExtensionBundleContext(final BundleVersion bundleVersion) {
return getExtensionBundleContext(bundleVersion.getBucket(), bundleVersion.getBundle(), bundleVersion.getVersionMetadata());
}
private BundleContext getExtensionBundleContext(final Bucket bucket, final Bundle bundle,
final BundleVersionMetadata bundleVersionMetadata) {
return new StandardBundleContext.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();
}
private BucketEntity getBucketEntity(final String bucketIdentifier) {
// 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.");
}
return existingBucket;
}
private BundleEntity getBundleEntity(final String bundleId) {
final BundleEntity existingBundle = metadataService.getBundle(bundleId);
if (existingBundle == null) {
LOGGER.warn("The specified extension bundle id [{}] does not exist.", bundleId);
throw new ResourceNotFoundException("The specified extension bundle ID does not exist.");
}
return existingBundle;
}
}