blob: 45003820c0e0a4368eaef108fe0c864853d9576e [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.sling.feature.cpconverter;
import static java.util.Objects.requireNonNull;
import static org.apache.sling.feature.cpconverter.vltpkg.VaultPackageUtils.detectPackageType;
import static org.apache.sling.feature.cpconverter.vltpkg.VaultPackageUtils.getDependencies;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.vault.fs.io.Archive;
import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
import org.apache.jackrabbit.vault.packaging.Dependency;
import org.apache.jackrabbit.vault.packaging.PackageId;
import org.apache.jackrabbit.vault.packaging.PackageProperties;
import org.apache.jackrabbit.vault.packaging.PackageType;
import org.apache.jackrabbit.vault.packaging.VaultPackage;
import org.apache.sling.feature.ArtifactId;
import org.apache.sling.feature.cpconverter.accesscontrol.AclManager;
import org.apache.sling.feature.cpconverter.accesscontrol.DefaultAclManager;
import org.apache.sling.feature.cpconverter.artifacts.ArtifactsDeployer;
import org.apache.sling.feature.cpconverter.artifacts.FileArtifactWriter;
import org.apache.sling.feature.cpconverter.features.FeaturesManager;
import org.apache.sling.feature.cpconverter.filtering.ResourceFilter;
import org.apache.sling.feature.cpconverter.handlers.DefaultHandler;
import org.apache.sling.feature.cpconverter.handlers.EntryHandler;
import org.apache.sling.feature.cpconverter.handlers.EntryHandlersManager;
import org.apache.sling.feature.cpconverter.handlers.NodeTypesEntryHandler;
import org.apache.sling.feature.cpconverter.handlers.slinginitialcontent.BundleSlingInitialContentExtractor;
import org.apache.sling.feature.cpconverter.index.DefaultIndexManager;
import org.apache.sling.feature.cpconverter.index.IndexManager;
import org.apache.sling.feature.cpconverter.vltpkg.BaseVaultPackageScanner;
import org.apache.sling.feature.cpconverter.vltpkg.PackagesEventsEmitter;
import org.apache.sling.feature.cpconverter.vltpkg.RecollectorVaultPackageScanner;
import org.apache.sling.feature.cpconverter.vltpkg.VaultPackageAssembler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanner implements Closeable {
public static final String ZIP_TYPE = "zip";
public static final String PACKAGE_CLASSIFIER = "cp2fm-converted";
private static final String DEFAULT_VERSION = "0.0.0";
private final Map<PackageId, String> subContentPackages = new HashMap<>();
private final List<VaultPackageAssembler> assemblers = new LinkedList<>();
private final Map<PackageId, Set<Dependency>> mutableContentsIds = new LinkedHashMap<>();
private EntryHandlersManager handlersManager;
private AclManager aclManager = new DefaultAclManager();
private FeaturesManager featuresManager;
private ResourceFilter resourceFilter;
private ArtifactsDeployer artifactsDeployer;
private ArtifactsDeployer unreferencedArtifactsDeployer;
private VaultPackageAssembler mainPackageAssembler;
private final RecollectorVaultPackageScanner recollectorVaultPackageScanner;
private final List<PackagesEventsEmitter> emitters = new ArrayList<>();
private final List<Runnable> deployTasks = new ArrayList<>();
private final File tmpDirectory;
private boolean failOnMixedPackages = false;
private PackagePolicy contentTypePackagePolicy = PackagePolicy.REFERENCE;
private boolean removeInstallHooks = false;
private boolean disablePackageTypeRecalculation = false;
private BundleSlingInitialContentExtractor bundleSlingInitialContentExtractor = new BundleSlingInitialContentExtractor();
private IndexManager indexManager = new DefaultIndexManager();
private RunModePolicy runModePolicy = RunModePolicy.DIRECT_ONLY;
public enum RunModePolicy {
/**
* Only path within containing package is considered for run mode evaluation
*/
DIRECT_ONLY,
/**
* For nesting of multiple levels, run mode constraints are inherited down and prepended
*/
PREPEND_INHERITED
}
public enum PackagePolicy {
/**
* References the content package in the feature model and deploys via the {@link ContentPackage2FeatureModelConverter#artifactsDeployer}
*/
REFERENCE,
/**
* Drops the content package completely (i.e. neither reference it in the feature model nor deploy anywhere)
*
* @deprecated
*/
DROP,
/**
* Deploys the content package via the {@link ContentPackage2FeatureModelConverter#unreferencedArtifactsDeployer}
*/
PUT_IN_DEDICATED_FOLDER
}
public enum SlingInitialContentPolicy {
/**
* Keep in bundle and don't extract
*/
KEEP,
/**
* Extract from bundle into content-packages and feature model
*/
EXTRACT_AND_REMOVE,
/**
* Extract from bundle into content-packages and feature model but keep in bundle as well
*/
EXTRACT_AND_KEEP
}
public ContentPackage2FeatureModelConverter() throws IOException {
this(false, SlingInitialContentPolicy.KEEP, false);
}
public ContentPackage2FeatureModelConverter(boolean strictValidation, @NotNull SlingInitialContentPolicy slingInitialContentPolicy) throws IOException {
this(strictValidation, slingInitialContentPolicy, false);
}
public ContentPackage2FeatureModelConverter(boolean strictValidation, @NotNull SlingInitialContentPolicy slingInitialContentPolicy, boolean disablePackageTypeRecalculation) throws IOException {
this(strictValidation, slingInitialContentPolicy, disablePackageTypeRecalculation, RunModePolicy.DIRECT_ONLY);
}
public ContentPackage2FeatureModelConverter(boolean strictValidation, @NotNull SlingInitialContentPolicy slingInitialContentPolicy, boolean disablePackageTypeRecalculation, @NotNull RunModePolicy runModePolicy) throws IOException {
super(strictValidation);
this.disablePackageTypeRecalculation = disablePackageTypeRecalculation;
this.recollectorVaultPackageScanner = new RecollectorVaultPackageScanner(this, this.packageManager, strictValidation, subContentPackages, slingInitialContentPolicy);
this.tmpDirectory = Files.createTempDirectory("cp2fm-converter").toFile();
this.runModePolicy = runModePolicy;
}
public @NotNull ContentPackage2FeatureModelConverter setEntryHandlersManager(@Nullable EntryHandlersManager handlersManager) {
this.handlersManager = handlersManager;
return this;
}
public @Nullable FeaturesManager getFeaturesManager() {
return featuresManager;
}
public @NotNull ContentPackage2FeatureModelConverter setFeaturesManager(@NotNull FeaturesManager featuresManager) {
this.featuresManager = featuresManager;
if (featuresManager instanceof PackagesEventsEmitter) {
this.emitters.add((PackagesEventsEmitter) featuresManager);
}
return this;
}
public @NotNull ContentPackage2FeatureModelConverter setResourceFilter(@Nullable ResourceFilter resourceFilter) {
this.resourceFilter = resourceFilter;
return this;
}
public @Nullable ArtifactsDeployer getArtifactsDeployer() {
return artifactsDeployer;
}
public @NotNull ContentPackage2FeatureModelConverter setBundlesDeployer(@NotNull ArtifactsDeployer bundlesDeployer) {
this.artifactsDeployer = bundlesDeployer;
return this;
}
public @NotNull ContentPackage2FeatureModelConverter setUnreferencedArtifactsDeployer(@NotNull ArtifactsDeployer unreferencedArtifactsDeployer) {
this.unreferencedArtifactsDeployer = unreferencedArtifactsDeployer;
return this;
}
public @NotNull AclManager getAclManager() {
return aclManager;
}
public @NotNull ContentPackage2FeatureModelConverter setAclManager(@NotNull AclManager aclManager) {
this.aclManager = aclManager;
return this;
}
public boolean hasMainPackageAssembler() {
return mainPackageAssembler != null;
}
public @NotNull VaultPackageAssembler getMainPackageAssembler() {
// verify that mainPackageAssembler has been set before retrieving it
return Objects.requireNonNull(mainPackageAssembler);
}
public @NotNull ContentPackage2FeatureModelConverter setMainPackageAssembler(@NotNull VaultPackageAssembler assembler) {
this.mainPackageAssembler = assembler;
return this;
}
public @NotNull ContentPackage2FeatureModelConverter setEmitter(@NotNull PackagesEventsEmitter emitter) {
this.emitters.add(emitter);
return this;
}
public @NotNull ContentPackage2FeatureModelConverter setContentTypePackagePolicy(@NotNull PackagePolicy contentTypePackagePolicy) {
this.contentTypePackagePolicy = contentTypePackagePolicy;
return this;
}
public @NotNull ContentPackage2FeatureModelConverter setFailOnMixedPackages(boolean failOnMixedPackages) {
this.failOnMixedPackages = failOnMixedPackages;
return this;
}
public @NotNull ContentPackage2FeatureModelConverter setRemoveInstallHooks(boolean removeInstallHook) {
this.removeInstallHooks = removeInstallHook;
return this;
}
public @NotNull ContentPackage2FeatureModelConverter setRunModePolicy(@NotNull RunModePolicy runModePolicy) {
this.runModePolicy = runModePolicy;
return this;
}
public RunModePolicy getRunModePolicy() {
return this.runModePolicy;
}
public @Nullable IndexManager getIndexManager() {
return indexManager;
}
public @NotNull ContentPackage2FeatureModelConverter setIndexManager(IndexManager indexManager) {
this.indexManager = indexManager;
return this;
}
public @NotNull File getTempDirectory() {
return this.tmpDirectory;
}
public void cleanup() throws IOException {
if (this.tmpDirectory.exists()) {
logger.info("Cleaning up tmp directory {}", this.tmpDirectory);
FileUtils.deleteDirectory(this.tmpDirectory);
}
}
public void convert(@NotNull File... contentPackages) throws IOException, ConverterException {
requireNonNull(contentPackages, "Null content-package(s) can not be converted.");
secondPass(firstPass(contentPackages));
}
protected @NotNull Collection<VaultPackage> firstPass(@NotNull File... contentPackages) throws IOException, ConverterException {
Map<PackageId, VaultPackage> idFileMap = new LinkedHashMap<>();
Map<PackageId, VaultPackage> idPackageMapping = new ConcurrentHashMap<>();
for (File contentPackage : contentPackages) {
requireNonNull(contentPackage, "Null content-package can not be converted.");
if (!contentPackage.exists() || !contentPackage.isFile()) {
throw new IllegalArgumentException("File " + contentPackage + " does not exist or it is a directory");
}
logger.info("Reading content-package '{}'...", contentPackage);
VaultPackage pack = open(contentPackage);
idPackageMapping.put(pack.getId(), pack);
// analyze sub-content packages in order to filter out
// possible outdated conflicting packages
recollectorVaultPackageScanner.traverse(pack, null);
logger.info("content-package '{}' successfully read!", contentPackage);
aclManager.reset();
bundleSlingInitialContentExtractor.reset();
}
logger.info("Ordering input content-package(s) {}...", idPackageMapping.keySet());
for (VaultPackage pack : idPackageMapping.values()) {
orderDependencies(idFileMap, idPackageMapping, pack, new HashSet<>());
}
logger.info("New content-package(s) order: {}", idFileMap.keySet());
return idFileMap.values();
}
private void secondPass(@NotNull Collection<VaultPackage> orderedContentPackages) throws IOException, ConverterException {
emitters.stream().forEach(PackagesEventsEmitter::start);
for (VaultPackage vaultPackage : orderedContentPackages) {
try {
emitters.stream().forEach(e -> e.startPackage(vaultPackage));
setMainPackageAssembler(VaultPackageAssembler.create(this.getTempDirectory(), vaultPackage, removeInstallHooks, disablePackageTypeRecalculation));
assemblers.add(getMainPackageAssembler());
ArtifactId mvnPackageId = toArtifactId(vaultPackage.getId(), vaultPackage.getFile());
featuresManager.init(mvnPackageId);
logger.info("Converting content-package '{}'...", vaultPackage.getId());
traverse(vaultPackage, null);
// retrieve the resulting zip-content-package and deploy it to the local mvn bundles dir.
try (VaultPackage result = processContentPackageArchive(getMainPackageAssembler(), null)) {
// finally serialize the Feature Model(s) file(s)
aclManager.addRepoinitExtension(assemblers, featuresManager);
bundleSlingInitialContentExtractor.addRepoInitExtension(assemblers, featuresManager);
indexManager.addRepoinitExtension(featuresManager);
logger.info("Conversion complete!");
featuresManager.serialize();
emitters.stream().forEach(e -> e.endPackage(vaultPackage.getId(), result));
}
} finally {
aclManager.reset();
bundleSlingInitialContentExtractor.reset();
indexManager.reset();
assemblers.clear();
try {
vaultPackage.close();
} catch (Exception e) {
// close quietly
}
}
}
deployPackages();
mutableContentsIds.clear();
emitters.stream().forEach(PackagesEventsEmitter::end);
}
private static void orderDependencies(@NotNull Map<PackageId, VaultPackage> idFileMap,
@NotNull Map<PackageId, VaultPackage> idPackageMapping,
@NotNull VaultPackage pack,
@NotNull Set<PackageId> visited) throws IOException, ConverterException {
if (!visited.add(pack.getId())) {
throw new ConverterException("Cyclic dependency detected, " + pack.getId() + " was previously visited already");
}
for (Dependency dep : pack.getDependencies()) {
for (java.util.Map.Entry<PackageId, VaultPackage> entry : idPackageMapping.entrySet()) {
if (dep.matches(entry.getKey())) {
orderDependencies(idFileMap, idPackageMapping, entry.getValue(), visited);
break;
}
}
}
idFileMap.put(pack.getId(), pack);
idPackageMapping.remove(pack.getId());
}
public void processSubPackage(@NotNull String path, @Nullable String runMode, @NotNull VaultPackage vaultPackage, boolean isEmbeddedPackage) throws IOException, ConverterException {
requireNonNull(path, "Impossible to process a null vault package");
requireNonNull(vaultPackage, "Impossible to process a null vault package");
if (!isSubContentPackageIncluded(path)) {
logger.info("Sub content-package {} is filtered out, so it won't be processed.", path);
return;
}
emitters.stream().forEach(e -> e.startSubPackage(path, vaultPackage));
VaultPackageAssembler clonedPackage = VaultPackageAssembler.create(this.getTempDirectory(), vaultPackage, removeInstallHooks,disablePackageTypeRecalculation);
// Please note: THIS IS A HACK to meet the new requirement without drastically change the original design
// temporary swap the main handler to collect stuff
VaultPackageAssembler handler = getMainPackageAssembler();
Properties parentProps = handler.getPackageProperties();
String parentTypeStr = (String)parentProps.get(PackageProperties.NAME_PACKAGE_TYPE);
boolean isContainerPackage = StringUtils.isNotBlank(parentTypeStr) ? PackageType.CONTAINER.equals(PackageType.valueOf(parentTypeStr.toUpperCase(Locale.ENGLISH))) : false;
setMainPackageAssembler(clonedPackage);
assemblers.add(clonedPackage);
// scan the detected package, first
traverse(vaultPackage, runMode);
//set dependency to parent package if the parent package is an application package & subpackage is embedded
if (isEmbeddedPackage && !isContainerPackage) {
PackageId parentId = new PackageId((String) parentProps.get(PackageProperties.NAME_GROUP),
(String) parentProps.get(PackageProperties.NAME_NAME),
(String) parentProps.get(PackageProperties.NAME_VERSION));
clonedPackage.addDependency(new Dependency(parentId));
}
// deploy the new content-package to the local mvn bundles dir and attach it to the feature
try (VaultPackage result = processContentPackageArchive(clonedPackage, runMode)) {
emitters.stream().forEach(e -> e.endSubPackage(path, vaultPackage.getId(), result));
}
// restore the previous assembler
setMainPackageAssembler(handler);
}
private @NotNull VaultPackage processContentPackageArchive(@NotNull VaultPackageAssembler assembler,
@Nullable String runMode) throws IOException, ConverterException {
File contentPackageArchive = assembler.createPackage();
VaultPackage vaultPackage = open(contentPackageArchive);
PackageType packageType = detectPackageType(vaultPackage);
// SLING-8608 - Fail the conversion if the resulting attached content-package is MIXED type
if (PackageType.MIXED == packageType && failOnMixedPackages) {
throw new ConverterException("Generated content-package '"
+ vaultPackage.getId()
+ "' located in file "
+ contentPackageArchive
+ " is of MIXED type");
}
ArtifactId mvnPackageId = toArtifactId(vaultPackage.getId(), contentPackageArchive);
// special handling for converted packages of type content
if (PackageType.CONTENT == packageType) {
switch (contentTypePackagePolicy) {
case DROP:
mutableContentsIds.put(vaultPackage.getId(), getDependencies(vaultPackage));
logger.info("Dropping package of PackageType.CONTENT {} (content-package id: {})",
mvnPackageId.getArtifactId(), vaultPackage.getId());
break;
case PUT_IN_DEDICATED_FOLDER:
mutableContentsIds.put(vaultPackage.getId(), getDependencies(vaultPackage));
// deploy the new content-package to the unreferenced artifacts deployer
if (unreferencedArtifactsDeployer == null) {
throw new IllegalStateException("ContentTypePackagePolicy PUT_IN_DEDICATED_FOLDER requires a valid deployer ");
}
final String location = unreferencedArtifactsDeployer.deploy(new FileArtifactWriter(contentPackageArchive), runMode, mvnPackageId);
logger.info("Put converted package of PackageType.CONTENT {} (content-package id: {}) at {} (not referenced in feature model)",
mvnPackageId.getArtifactId(), vaultPackage.getId(), location);
break;
case REFERENCE:
deploy(assembler, mvnPackageId, runMode);
}
} else {
deploy(assembler, mvnPackageId, runMode);
}
return vaultPackage;
}
public void deployPackages() {
try {
mutableContentsIds.values().forEach(
value -> value.removeIf(dep -> mutableContentsIds.keySet().stream().anyMatch(dep::matches)));
deployTasks.forEach(Runnable::run);
} catch (RuntimeException ex) {
if (ex.getCause() instanceof Exception) {
throw ex;
}
throw ex;
}
deployTasks.clear();
}
private void deploy(@NotNull VaultPackageAssembler assembler, @NotNull ArtifactId mvnPackageId, @Nullable String runMode) {
Objects.requireNonNull(getFeaturesManager()).addArtifact(runMode, mvnPackageId);
ArtifactsDeployer deployer = Objects.requireNonNull(getArtifactsDeployer());
deployTasks.add(() -> {
assembler.updateDependencies(mutableContentsIds);
try {
File finalContentPackageArchive = assembler.createPackage();
// deploy the new content-package to the local mvn bundles dir
deployer.deploy(new FileArtifactWriter(finalContentPackageArchive), runMode, mvnPackageId);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
});
}
public boolean isSubContentPackageIncluded(@NotNull String path) {
return subContentPackages.containsValue(path);
}
private void process(@NotNull String entryPath, @NotNull Archive archive, @Nullable Entry entry, String runMode) throws IOException, ConverterException {
if (resourceFilter != null && resourceFilter.isFilteredOut(entryPath)) {
throw new ConverterException("Path '"
+ entryPath
+ "' in archive "
+ archive.getMetaInf().getPackageProperties().getId()
+ " not allowed by user configuration, please check configured filtering patterns");
}
EntryHandler entryHandler = handlersManager.getEntryHandlerByEntryPath(entryPath);
if (entryHandler == null) {
entryHandler = new DefaultHandler(getMainPackageAssembler(), removeInstallHooks);
}
if (entry == null) {
entry = archive.getEntry(entryPath);
if (entry == null) {
throw new IllegalArgumentException("Archive '" + archive.getMetaInf().getPackageProperties().getId() + "' does not contain entry with path '" + entryPath + "'");
}
}
entryHandler.handle(entryPath, archive, entry, this, runMode);
if (!getMainPackageAssembler().recordEntryPath(entryPath)) {
logger.warn("Duplicate entry path {}", entryPath);
}
}
public ContentPackage2FeatureModelConverter setBundleSlingInitialContentExtractor(BundleSlingInitialContentExtractor bundleSlingInitialContentExtractor) {
this.bundleSlingInitialContentExtractor = bundleSlingInitialContentExtractor;
return this;
}
@Override
protected void onFile(@NotNull String entryPath, @NotNull Archive archive, @NotNull Entry entry, String runMode) throws IOException, ConverterException {
try {
process(entryPath, archive, entry, runMode);
} catch (ConverterException ex) {
throw new ConverterException("ConverterException occured on path " + entryPath + " with message: " + ex.getMessage(), ex);
} catch (IOException ex) {
throw new IOException("IOException occured on path " + entryPath + " with message: " + ex.getMessage(), ex);
}
}
public static @NotNull ArtifactId toArtifactId(@NotNull PackageId packageId, @NotNull File file) {
String groupId = requireNonNull(packageId.getGroup(),
PackageProperties.NAME_GROUP
+ " property not found in content-package "
+ file
+ ", please check META-INF/vault/properties.xml").replace('/', '.');
// Replace any space with an underscore to adhere to Maven Group Id specification
groupId = groupId.replace(" ", "_");
String artifactId = requireNonNull(packageId.getName(),
PackageProperties.NAME_NAME
+ " property not found in content-package "
+ file
+ ", please check META-INF/vault/properties.xml");
// Replace any space with an underscore to adhere to Maven Artifact Id specification
artifactId = artifactId.replace(" ", "_");
// package versions may use suffix "-cp2fm-converted" which is redundant as for artifactIds this is set as dedicated classifier
String version = packageId.getVersionString();
if (version.endsWith(VaultPackageAssembler.VERSION_SUFFIX)) {
version = version.substring(0, version.length() - VaultPackageAssembler.VERSION_SUFFIX.length());
}
if (version.isEmpty()) {
version = DEFAULT_VERSION;
}
return new ArtifactId(groupId, artifactId, version, PACKAGE_CLASSIFIER, ZIP_TYPE);
}
@Override
protected void addCdnPattern(@NotNull Pattern cndPattern) {
handlersManager.addEntryHandler(NodeTypesEntryHandler.forCndPattern(cndPattern));
}
@Override
public void close() throws IOException {
cleanup();
}
public List<VaultPackageAssembler> getAssemblers() {
return new ArrayList<>(assemblers);
}
}