blob: 8a0fc92fcadc0dff2a046ba91f61fcf948607e0c [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.File;
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.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import org.apache.jackrabbit.vault.fs.io.Archive;
import org.apache.jackrabbit.vault.fs.io.Archive.Entry;
import org.apache.jackrabbit.vault.packaging.CyclicDependencyException;
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.acl.AclManager;
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.EntryHandler;
import org.apache.sling.feature.cpconverter.handlers.EntryHandlersManager;
import org.apache.sling.feature.cpconverter.handlers.NodeTypesEntryHandler;
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;
public class ContentPackage2FeatureModelConverter extends BaseVaultPackageScanner {
public static final String ZIP_TYPE = "zip";
public static final String PACKAGE_CLASSIFIER = "cp2fm-converted";
private static final String DEFEAULT_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;
private FeaturesManager featuresManager;
private ResourceFilter resourceFilter;
private ArtifactsDeployer artifactsDeployer;
private VaultPackageAssembler mainPackageAssembler = null;
private RecollectorVaultPackageScanner recollectorVaultPackageScanner;
private PackagesEventsEmitter emitter;
private boolean failOnMixedPackages = false;
private boolean dropContent = false;
public ContentPackage2FeatureModelConverter() {
this(false);
}
public ContentPackage2FeatureModelConverter(boolean strictValidation) {
super(strictValidation);
this.recollectorVaultPackageScanner = new RecollectorVaultPackageScanner(this, this.packageManager, strictValidation, subContentPackages);
}
public ContentPackage2FeatureModelConverter setEntryHandlersManager(EntryHandlersManager handlersManager) {
this.handlersManager = handlersManager;
return this;
}
public FeaturesManager getFeaturesManager() {
return featuresManager;
}
public ContentPackage2FeatureModelConverter setFeaturesManager(FeaturesManager featuresManager) {
this.featuresManager = featuresManager;
return this;
}
public ContentPackage2FeatureModelConverter setResourceFilter(ResourceFilter resourceFilter) {
this.resourceFilter = resourceFilter;
return this;
}
public ArtifactsDeployer getArtifactsDeployer() {
return artifactsDeployer;
}
public ContentPackage2FeatureModelConverter setBundlesDeployer(ArtifactsDeployer bundlesDeployer) {
this.artifactsDeployer = bundlesDeployer;
return this;
}
public AclManager getAclManager() {
return aclManager;
}
public ContentPackage2FeatureModelConverter setAclManager(AclManager aclManager) {
this.aclManager = aclManager;
return this;
}
public VaultPackageAssembler getMainPackageAssembler() {
return mainPackageAssembler;
}
public ContentPackage2FeatureModelConverter setEmitter(PackagesEventsEmitter emitter) {
this.emitter = emitter;
return this;
}
public ContentPackage2FeatureModelConverter setDropContent(boolean dropContent) {
this.dropContent = dropContent;
return this;
}
public ContentPackage2FeatureModelConverter setFailOnMixedPackages(boolean failOnMixedPackages) {
this.failOnMixedPackages = failOnMixedPackages;
return this;
}
public void convert(File...contentPackages) throws Exception {
requireNonNull(contentPackages , "Null content-package(s) can not be converted.");
secondPass(firstPass(contentPackages));
}
protected Collection<VaultPackage> firstPass(File...contentPackages) throws Exception {
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);
logger.info("content-package '{}' successfully read!", contentPackage);
}
logger.info("Ordering input content-package(s) {}...", idPackageMapping.keySet());
for (VaultPackage pack : idPackageMapping.values()) {
orderDependencies(idFileMap, idPackageMapping, pack, new HashSet<PackageId>());
}
logger.info("New content-package(s) order: {}", idFileMap.keySet());
return idFileMap.values();
}
protected void secondPass(Collection<VaultPackage> orderedContentPackages) throws Exception {
emitter.start();
for (VaultPackage vaultPackage : orderedContentPackages) {
try {
emitter.startPackage(vaultPackage);
mainPackageAssembler = VaultPackageAssembler.create(vaultPackage);
assemblers.add(mainPackageAssembler);
ArtifactId mvnPackageId = toArtifactId(vaultPackage);
featuresManager.init(mvnPackageId.getGroupId(), mvnPackageId.getArtifactId(), mvnPackageId.getVersion());
logger.info("Converting content-package '{}'...", vaultPackage.getId());
traverse(vaultPackage);
// make sure
mainPackageAssembler.updateDependencies(mutableContentsIds);
// attach all unmatched resources as new content-package
File contentPackageArchive = mainPackageAssembler.createPackage();
// deploy the new zip content-package to the local mvn bundles dir
processContentPackageArchive(contentPackageArchive, null, mvnPackageId, vaultPackage.getId());
// finally serialize the Feature Model(s) file(s)
aclManager.addRepoinitExtension(assemblers, featuresManager);
logger.info("Conversion complete!");
featuresManager.serialize();
emitter.endPackage();
} finally {
aclManager.reset();
assemblers.clear();
try {
vaultPackage.close();
} catch (Exception e) {
// close quietly
}
}
}
mutableContentsIds.clear();
emitter.end();
}
private void orderDependencies(Map<PackageId, VaultPackage> idFileMap,
Map<PackageId, VaultPackage> idPackageMapping,
VaultPackage pack,
Set<PackageId> visited) throws CyclicDependencyException {
if (!visited.add(pack.getId())) {
throw new CyclicDependencyException("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(String path, String runMode, VaultPackage vaultPackage) throws Exception {
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;
}
emitter.startSubPackage(path, vaultPackage);
PackageId originalPackageId = vaultPackage.getId();
ArtifactId mvnPackageId = toArtifactId(vaultPackage);
VaultPackageAssembler clonedPackage = VaultPackageAssembler.create(vaultPackage);
// 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 = mainPackageAssembler;
assemblers.add(handler);
mainPackageAssembler = clonedPackage;
// scan the detected package, first
traverse(vaultPackage);
clonedPackage.updateDependencies(mutableContentsIds);
File contentPackageArchive = clonedPackage.createPackage();
// deploy the new content-package to the local mvn bundles dir and attach it to the feature
processContentPackageArchive(contentPackageArchive, runMode, mvnPackageId, originalPackageId);
// restore the previous assembler
mainPackageAssembler = handler;
emitter.endSubPackage();
}
private void processContentPackageArchive(File contentPackageArchive,
String runMode,
ArtifactId mvnPackageId,
PackageId originalPackageId) throws Exception {
try (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 Exception("Generated content-package '"
+ originalPackageId
+ "' located in file "
+ contentPackageArchive
+ " is of MIXED type");
}
// don't deploy & add content-packages of type content to featuremodel if dropContent is set
if (PackageType.CONTENT != packageType || !dropContent) {
// deploy the new content-package to the local mvn bundles dir and attach it to the feature
artifactsDeployer.deploy(new FileArtifactWriter(contentPackageArchive), mvnPackageId);
featuresManager.addArtifact(runMode, mvnPackageId);
} else {
mutableContentsIds.put(originalPackageId, getDependencies(vaultPackage));
logger.info("Dropping package of PackageType.CONTENT {} (content-package id: {})",
mvnPackageId.getArtifactId(), originalPackageId);
}
}
}
protected boolean isSubContentPackageIncluded(String path) {
return subContentPackages.containsValue(path);
}
@Override
protected void onFile(String entryPath, Archive archive, Entry entry) throws Exception {
if (resourceFilter != null && resourceFilter.isFilteredOut(entryPath)) {
throw new IllegalArgumentException("Path '"
+ entryPath
+ "' in archive "
+ archive.getMetaInf().getProperties()
+ " not allowed by user configuration, please check configured filtering patterns");
}
EntryHandler entryHandler = handlersManager.getEntryHandlerByEntryPath(entryPath);
if (entryHandler == null) {
entryHandler = mainPackageAssembler;
}
entryHandler.handle(entryPath, archive, entry, this);
}
private static ArtifactId toArtifactId(VaultPackage vaultPackage) {
PackageId packageId = vaultPackage.getId();
String groupId = requireNonNull(packageId.getGroup(),
PackageProperties.NAME_GROUP
+ " property not found in content-package "
+ vaultPackage
+ ", please check META-INF/vault/properties.xml").replace('/', '.');
// Replace any space with an underscore to adhere to Maven Group Id specification
groupId = groupId.replaceAll(" ", "_");
String artifactid = requireNonNull(packageId.getName(),
PackageProperties.NAME_NAME
+ " property not found in content-package "
+ vaultPackage
+ ", please check META-INF/vault/properties.xml");
// Replace any space with an underscore to adhere to Maven Artifact Id specification
artifactid = artifactid.replaceAll(" ", "_");
String version = packageId.getVersionString();
if (version == null || version.isEmpty()) {
version = DEFEAULT_VERSION;
}
return new ArtifactId(groupId, artifactid, version, PACKAGE_CLASSIFIER, ZIP_TYPE);
}
@Override
protected void addCdnPattern(Pattern cndPattern) {
handlersManager.addEntryHandler(new NodeTypesEntryHandler(cndPattern));
}
}