AllPackageBuilder.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2020 wcm.io
* %%
* Licensed 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.
* #L%
*/
package io.wcm.devops.conga.plugins.aem.maven.allpackage;
import static io.wcm.devops.conga.generator.util.FileUtil.getCanonicalPath;
import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.RUNMODE_AUTHOR;
import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.RUNMODE_PUBLISH;
import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.eliminateAuthorPublishDuplicates;
import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.isAuthorAndPublish;
import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.isOnlyAuthor;
import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.isOnlyPublish;
import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_DEPENDENCIES;
import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_NAME;
import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_PACKAGE_TYPE;
import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_VERSION;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.vault.packaging.Dependency;
import org.apache.jackrabbit.vault.packaging.DependencyUtil;
import org.apache.jackrabbit.vault.packaging.PackageType;
import org.apache.jackrabbit.vault.packaging.VersionRange;
import org.apache.maven.artifact.ArtifactUtils;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugin.logging.SystemStreamLog;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import io.wcm.devops.conga.plugins.aem.maven.AutoDependenciesMode;
import io.wcm.devops.conga.plugins.aem.maven.BuildOutputTimestamp;
import io.wcm.devops.conga.plugins.aem.maven.PackageTypeValidation;
import io.wcm.devops.conga.plugins.aem.maven.PackageVersionMode;
import io.wcm.devops.conga.plugins.aem.maven.RunModeOptimization;
import io.wcm.devops.conga.plugins.aem.maven.model.BundleFile;
import io.wcm.devops.conga.plugins.aem.maven.model.ContentPackageFile;
import io.wcm.devops.conga.plugins.aem.maven.model.InstallableFile;
import io.wcm.tooling.commons.contentpackagebuilder.ContentPackage;
import io.wcm.tooling.commons.contentpackagebuilder.ContentPackageBuilder;
import io.wcm.tooling.commons.contentpackagebuilder.PackageFilter;
/**
* Builds "all" package based on given set of content packages.
* <p>
* General concept:
* </p>
* <ul>
* <li>Iterates through all content packages that are generated or collected by CONGA and contained in the
* model.json</li>
* <li>Enforces the order defined in CONGA by automatically adding dependencies to all packages reflecting the file
* order in model.json</li>
* <li>Because the dependency chain may be different for each runmode (author/publish), each package is added once for
* each runmode. Internally this separate dependency change for author and publish is optimized to have each package
* included only once for author+publish, unless it has a different chain of dependencies for both runmodes, in which
* case it is included separately for each run mode.</li>
* <li>To avoid conflicts with duplicate packages with different dependency chains the names of packages that are
* included in different versions for author/publish are changed and a runmode suffix (.author or .publish) is added,
* and it is put in a corresponding install folder.</li>
* <li>To avoid problems with nested sub packages, the sub packages are extracted from the packages and treated in the
* same way as other packages.</li>
* </ul>
*/
public final class AllPackageBuilder {
private final File targetFile;
private final String groupName;
private final String packageName;
private String version;
private AutoDependenciesMode autoDependenciesMode = AutoDependenciesMode.OFF;
private RunModeOptimization runModeOptimization = RunModeOptimization.OFF;
private PackageTypeValidation packageTypeValidation = PackageTypeValidation.STRICT;
private PackageVersionMode packageVersionMode = PackageVersionMode.DEFAULT;
private Log log;
private BuildOutputTimestamp buildOutputTimestamp;
private static final String RUNMODE_DEFAULT = "$default$";
private static final Set<String> ALLOWED_PACKAGE_TYPES = Set.of(
PackageType.APPLICATION.name().toLowerCase(),
PackageType.CONTAINER.name().toLowerCase(),
PackageType.CONTENT.name().toLowerCase());
private static final String VERSION_SUFFIX_SEPARATOR = "-";
private final List<ContentPackageFileSet> contentPackageFileSets = new ArrayList<>();
private final List<BundleFileSet> bundleFileSets = new ArrayList<>();
/**
* @param targetFile Target file
* @param groupName Group name
* @param packageName Package name
*/
public AllPackageBuilder(File targetFile, String groupName, String packageName) {
this.targetFile = targetFile;
this.groupName = groupName;
this.packageName = packageName;
}
/**
* @param value Automatically generate dependencies between content packages based on file order in CONGA
* configuration.
* @return this
*/
public AllPackageBuilder autoDependenciesMode(AutoDependenciesMode value) {
this.autoDependenciesMode = value;
return this;
}
/**
* @param value Configure run mode optimization.
* @return this
*/
public AllPackageBuilder runModeOptimization(RunModeOptimization value) {
this.runModeOptimization = value;
return this;
}
/**
* @param value How to validate package types to be included in "all" package.
* @return this
*/
public AllPackageBuilder packageTypeValidation(PackageTypeValidation value) {
this.packageTypeValidation = value;
return this;
}
/**
* @param value How to handle versions of packages and sub-packages inside "all" package.
* @return this
*/
public AllPackageBuilder packageVersionMode(PackageVersionMode value) {
this.packageVersionMode = value;
return this;
}
/**
* @param value Maven logger
* @return this
*/
public AllPackageBuilder logger(Log value) {
this.log = value;
return this;
}
/**
* @param value Package version
* @return this
*/
public AllPackageBuilder version(String value) {
this.version = value;
return this;
}
/**
* @param value Build output timestamp
* @return this
*/
public AllPackageBuilder buildOutputTimestamp(BuildOutputTimestamp value) {
this.buildOutputTimestamp = value;
return this;
}
private Log getLog() {
if (this.log == null) {
this.log = new SystemStreamLog();
}
return this.log;
}
/**
* Add content packages and OSGi bundles to be contained in "all" content package.
* @param files Content packages (invalid will be filtered out) and OSGi bundles
* @param cloudManagerTarget Target environments/run modes the packages should be attached to
* @throws IllegalArgumentException If and invalid package type is detected
*/
public void add(List<InstallableFile> files, Set<String> cloudManagerTarget) {
List<ContentPackageFile> contentPackages = filterFiles(files, ContentPackageFile.class);
// collect list of cloud manager environment run modes
List<String> environmentRunModes = new ArrayList<>();
if (cloudManagerTarget.isEmpty()) {
environmentRunModes.add(RUNMODE_DEFAULT);
}
else {
environmentRunModes.addAll(cloudManagerTarget);
}
List<ContentPackageFile> validContentPackages;
switch (packageTypeValidation) {
case STRICT:
validContentPackages = getValidContentPackagesStrictValidation(contentPackages);
break;
case WARN:
validContentPackages = getValidContentPackagesWarnValidation(contentPackages);
break;
default:
throw new IllegalArgumentException("Unsupported package type validation: " + packageTypeValidation);
}
if (!validContentPackages.isEmpty()) {
contentPackageFileSets.add(new ContentPackageFileSet(validContentPackages, environmentRunModes));
}
// add OSGi bundles
List<BundleFile> bundles = filterFiles(files, BundleFile.class);
if (!bundles.isEmpty()) {
bundleFileSets.add(new BundleFileSet(bundles, environmentRunModes));
}
}
/**
* Get valid content packages in strict mode: Ignore content packages without package type (with warning),
* fail build if Content package with "mixed" mode is found.
* @param contentPackages Content packages
* @return Valid content packages
*/
private List<ContentPackageFile> getValidContentPackagesStrictValidation(List<? extends ContentPackageFile> contentPackages) {
// generate warning for each content packages without package type that is skipped
contentPackages.stream()
.filter(pkg -> !hasPackageType(pkg))
.forEach(pkg -> getLog().warn("Skipping content package without package type: " + getCanonicalPath(pkg.getFile())));
// fail build if content packages with non-allowed package types exist
List<ContentPackageFile> invalidPackageTypeContentPackages = contentPackages.stream()
.filter(AllPackageBuilder::hasPackageType)
.filter(pkg -> !isValidPackageType(pkg))
.collect(Collectors.toList());
if (!invalidPackageTypeContentPackages.isEmpty()) {
throw new IllegalArgumentException("Content packages found with unsupported package types: " +
invalidPackageTypeContentPackages.stream()
.map(pkg -> pkg.getName() + " -> " + pkg.getPackageType())
.collect(Collectors.joining(", ")));
}
// collect AEM content packages with package type
return contentPackages.stream()
.filter(AllPackageBuilder::hasPackageType)
.collect(Collectors.toList());
}
/**
* Get all content packages, generate warnings if package type is missing or "mixed" mode package type is used.
* @param contentPackages Content packages
* @return Valid content packages
*/
private List<ContentPackageFile> getValidContentPackagesWarnValidation(List<? extends ContentPackageFile> contentPackages) {
// generate warning for each content packages without package type
contentPackages.stream()
.filter(pkg -> !hasPackageType(pkg))
.forEach(pkg -> getLog().warn("Found content package without package type: " + getCanonicalPath(pkg.getFile())));
// generate warning for each content packages with invalid package type
contentPackages.stream()
.filter(AllPackageBuilder::hasPackageType)
.filter(pkg -> !isValidPackageType(pkg))
.forEach(pkg -> getLog().warn("Found content package with invalid package type: "
+ getCanonicalPath(pkg.getFile()) + " -> " + pkg.getPackageType()));
// return all content packages
return contentPackages.stream().collect(Collectors.toList());
}
private static <T> List<T> filterFiles(List<? extends InstallableFile> files, Class<T> fileClass) {
return files.stream()
.filter(fileClass::isInstance)
.map(fileClass::cast)
.collect(Collectors.toList());
}
/**
* Build "all" content package.
* @param properties Specifies additional properties to be set in the properties.xml file.
* @return true if "all" package was generated, false if no valid package was found.
* @throws IOException I/O exception
*/
public boolean build(Map<String, String> properties) throws IOException {
if (contentPackageFileSets.isEmpty()) {
return false;
}
// prepare content package metadata
ContentPackageBuilder builder = new ContentPackageBuilder()
.group(groupName)
.name(packageName)
.packageType("container");
if (version != null) {
builder.version(version);
}
// define root path for "all" package
String rootPath = buildRootPath(groupName, packageName);
builder.filter(new PackageFilter(rootPath));
// additional package properties
if (properties != null) {
properties.entrySet().forEach(entry -> builder.property(entry.getKey(), entry.getValue()));
}
// build content package
try (ContentPackage contentPackage = builder.build(targetFile)) {
buildAddContentPackages(contentPackage, rootPath);
buildAddBundles(contentPackage, rootPath);
}
return true;
}
@SuppressWarnings("java:S3776") // ignore complexity
private void buildAddContentPackages(ContentPackage contentPackage, String rootPath) throws IOException {
// build set with dependencies instances for each package contained in all filesets
Set<Dependency> allPackagesFromFileSets = new HashSet<>();
for (ContentPackageFileSet fileSet : contentPackageFileSets) {
for (ContentPackageFile pkg : fileSet.getFiles()) {
addDependencyInformation(allPackagesFromFileSets, pkg);
}
}
Collection<ContentPackageFileSet> processedFileSets;
if (runModeOptimization == RunModeOptimization.ELIMINATE_DUPLICATES) {
// eliminate duplicates which are same for author and publish
processedFileSets = eliminateAuthorPublishDuplicates(contentPackageFileSets,
environmentRunMode -> new ContentPackageFileSet(new ArrayList<>(), Collections.singletonList(environmentRunMode)));
}
else {
processedFileSets = contentPackageFileSets;
}
for (ContentPackageFileSet fileSet : processedFileSets) {
for (String environmentRunMode : fileSet.getEnvironmentRunModes()) {
List<ContentPackageFile> previousPackages = new ArrayList<>();
for (ContentPackageFile pkg : fileSet.getFiles()) {
ContentPackageFile previousPkg = getDependencyChainPreviousPackage(pkg, previousPackages);
// set package name, wire previous package in package dependency
List<TemporaryContentPackageFile> processedFiles = processContentPackage(pkg, previousPkg, environmentRunMode, allPackagesFromFileSets);
// add processed content packages to "all" content package - and delete the temporary files
try {
for (TemporaryContentPackageFile processedFile : processedFiles) {
String path = buildPackagePath(processedFile, rootPath, environmentRunMode);
contentPackage.addFile(path, processedFile.getFile());
if (log.isDebugEnabled()) {
log.debug(" Add " + processedFile.getPackageInfoWithDependencies());
}
}
}
finally {
processedFiles.stream()
.map(TemporaryContentPackageFile::getFile)
.forEach(FileUtils::deleteQuietly);
}
previousPackages.add(pkg);
}
}
}
}
/**
* Gets the previous package in the order defined by CONGA to define as package dependency in current package.
* @param currentPackage Current package
* @param previousPackages List of previous packages
* @return Package to define as dependency, or null if no dependency should be defined
*/
private @Nullable ContentPackageFile getDependencyChainPreviousPackage(@NotNull ContentPackageFile currentPackage,
@NotNull List<ContentPackageFile> previousPackages) {
if ((autoDependenciesMode == AutoDependenciesMode.OFF)
|| (autoDependenciesMode == AutoDependenciesMode.IMMUTABLE_ONLY && isMutable(currentPackage))) {
return null;
}
// get last previous package
return previousPackages.stream()
// if not IMMUTABLE_MUTABLE_COMBINED active only that of the same mutability type
.filter(item -> (autoDependenciesMode == AutoDependenciesMode.IMMUTABLE_MUTABLE_COMBINED) || mutableMatches(item, currentPackage))
// make sure author-only or publish-only packages are only taken into account if the current package has same restriction
.filter(item -> isAuthorAndPublish(item)
|| (isOnlyAuthor(item) && isOnlyAuthor(currentPackage))
|| (isOnlyPublish(item) && isOnlyPublish(currentPackage)))
// get last in list
.reduce((first, second) -> second).orElse(null);
}
private void buildAddBundles(ContentPackage contentPackage, String rootPath) throws IOException {
Collection<BundleFileSet> processedFileSets;
if (runModeOptimization == RunModeOptimization.ELIMINATE_DUPLICATES) {
// eliminate duplicates which are same for author and publish
processedFileSets = eliminateAuthorPublishDuplicates(bundleFileSets,
environmentRunMode -> new BundleFileSet(new ArrayList<>(), Collections.singletonList(environmentRunMode)));
}
else {
processedFileSets = bundleFileSets;
}
for (BundleFileSet bundleFileSet : processedFileSets) {
for (String environmentRunMode : bundleFileSet.getEnvironmentRunModes()) {
for (BundleFile bundleFile : bundleFileSet.getFiles()) {
String path = buildBundlePath(bundleFile, rootPath, environmentRunMode);
contentPackage.addFile(path, bundleFile.getFile());
}
}
}
}
private static boolean hasPackageType(ContentPackageFile pkg) {
// accept only content packages with a package type set
return pkg.getPackageType() != null;
}
private static boolean isValidPackageType(ContentPackageFile pkg) {
// check if the package type is an allowed package type
return ALLOWED_PACKAGE_TYPES.contains(pkg.getPackageType());
}
private static boolean isMutable(ContentPackageFile pkg) {
return StringUtils.equals("content", pkg.getPackageType());
}
private static boolean mutableMatches(ContentPackageFile pkg1, ContentPackageFile pkg2) {
if (pkg1 == null || pkg2 == null) {
return false;
}
return isMutable(pkg1) == isMutable(pkg2);
}
/**
* Build root path to be used for embedded package.
* @param groupName Group name
* @param packageName Package name
* @return Package path
*/
private static String buildRootPath(String groupName, String packageName) {
return "/apps/" + groupName + "-" + packageName + "-packages";
}
/**
* Generate suffix for instance and environment run modes.
* @param file File
* @return Suffix string
*/
private static String buildRunModeSuffix(InstallableFile file, String environmentRunMode) {
StringBuilder runModeSuffix = new StringBuilder();
if (isOnlyAuthor(file)) {
runModeSuffix.append(".").append(RUNMODE_AUTHOR);
}
else if (isOnlyPublish(file)) {
runModeSuffix.append(".").append(RUNMODE_PUBLISH);
}
if (!StringUtils.equals(environmentRunMode, RUNMODE_DEFAULT)) {
runModeSuffix.append(".").append(environmentRunMode);
}
return runModeSuffix.toString();
}
/**
* Generate suffix for versions of content packages.
* @param pkg Content package
* @param ignoreSnapshot Do not build version suffix for SNAPSHOT versions
* @return Suffix string
*/
private String buildVersionSuffix(ContentPackageFile pkg, boolean ignoreSnapshot) {
StringBuilder versionSuffix = new StringBuilder();
if (this.packageVersionMode == PackageVersionMode.RELEASE_SUFFIX_VERSION
&& (!ArtifactUtils.isSnapshot(pkg.getVersion()) || !ignoreSnapshot)
&& !StringUtils.equals(pkg.getVersion(), this.version)
&& this.version != null) {
versionSuffix.append(VERSION_SUFFIX_SEPARATOR)
// replace dots with underlines in version suffix to avoid confusion with main version number
.append(StringUtils.replace(this.version, ".", "_"));
}
return versionSuffix.toString();
}
/**
* Build path to be used for embedded package.
* @param pkg Package
* @param rootPath Root path
* @return Package path
*/
@SuppressWarnings("java:S1075") // no filesystem path
private String buildPackagePath(ContentPackageFile pkg, String rootPath, String environmentRunMode) {
if (packageTypeValidation == PackageTypeValidation.STRICT && !isValidPackageType(pkg)) {
throw new IllegalArgumentException("Package " + pkg.getPackageInfo() + " has invalid package type: '" + pkg.getPackageType() + "'.");
}
String runModeSuffix = buildRunModeSuffix(pkg, environmentRunMode);
// add run mode suffix to both install folder path and package file name
String path = rootPath + "/" + Objects.toString(pkg.getPackageType(), "misc") + "/install" + runModeSuffix;
String versionSuffix = "";
String packageVersion = pkg.getVersion();
String packageVersionWithoutSuffix = packageVersion;
if (this.packageVersionMode == PackageVersionMode.RELEASE_SUFFIX_VERSION && this.version != null) {
packageVersionWithoutSuffix = StringUtils.removeEnd(packageVersion, buildVersionSuffix(pkg, false));
}
if (packageVersion != null && pkg.getFile().getName().contains(packageVersionWithoutSuffix)) {
versionSuffix = "-" + packageVersion;
}
String fileName = pkg.getName() + versionSuffix
+ "." + FilenameUtils.getExtension(pkg.getFile().getName());
return path + "/" + fileName;
}
/**
* Build path to be used for embedded bundle.
* @param bundleFile Bundle
* @param rootPath Root path
* @return Package path
*/
private static String buildBundlePath(BundleFile bundleFile, String rootPath, String environmentRunMode) {
String runModeSuffix = buildRunModeSuffix(bundleFile, environmentRunMode);
// add run mode suffix to both install folder path and package file name
String path = rootPath + "/application/install" + runModeSuffix;
return path + "/" + bundleFile.getFile().getName();
}
/**
* Rewrite content package ZIP file while adding to "all" package:
* Add dependency to previous package in CONGA configuration file oder.
* @param pkg Package to process (can be parent packe of the actual file)
* @param previousPkg Previous package to get dependency information from.
* Is null if no previous package exists or auto dependency mode is switched off.
* @param environmentRunMode Environment run mode
* @param allPackagesFromFileSets Set with all packages from all file sets as dependency instances
* @return Returns a list of content package *temporary* files - have to be deleted when processing is completed.
* @throws IOException I/O error
*/
@SuppressWarnings("java:S3776") // ignore complexity
private List<TemporaryContentPackageFile> processContentPackage(ContentPackageFile pkg,
ContentPackageFile previousPkg, String environmentRunMode,
Set<Dependency> allPackagesFromFileSets) throws IOException {
List<TemporaryContentPackageFile> result = new ArrayList<>();
List<TemporaryContentPackageFile> subPackages = new ArrayList<>();
// create temp zip file to create rewritten copy of package
File tempFile = File.createTempFile(FilenameUtils.getBaseName(pkg.getFile().getName()), ".zip");
// open original content package
try (ZipFile zipFileIn = new ZipFile.Builder().setFile(pkg.getFile()).get()) {
// iterate through entries and write them to the temp. zip file
try (FileOutputStream fos = new FileOutputStream(tempFile);
ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(fos)) {
Enumeration<? extends ZipArchiveEntry> zipInEntries = zipFileIn.getEntries();
while (zipInEntries.hasMoreElements()) {
ZipArchiveEntry zipInEntry = zipInEntries.nextElement();
if (!zipInEntry.isDirectory()) {
try (InputStream is = zipFileIn.getInputStream(zipInEntry)) {
boolean processedEntry = false;
// if entry is properties.xml, update dependency information
if (StringUtils.equals(zipInEntry.getName(), "META-INF/vault/properties.xml")) {
FileVaultProperties fileVaultProps = new FileVaultProperties(is);
Properties props = fileVaultProps.getProperties();
addSuffixToPackageName(props, pkg, environmentRunMode);
addSuffixToVersion(props, pkg);
// update package dependencies
ContentPackageFile dependencyFile = previousPkg;
if (autoDependenciesMode == AutoDependenciesMode.OFF) {
dependencyFile = null;
}
updateDependencies(pkg, props, dependencyFile, environmentRunMode, allPackagesFromFileSets);
// if package type is missing in package properties, put in the type defined in model
String packageType = pkg.getPackageType();
if (props.get(NAME_PACKAGE_TYPE) == null && packageType != null) {
props.put(NAME_PACKAGE_TYPE, packageType);
}
ZipArchiveEntry zipOutEntry = newZipEntry(zipInEntry);
zipOut.putArchiveEntry(zipOutEntry);
fileVaultProps.storeToXml(zipOut);
zipOut.closeArchiveEntry();
processedEntry = true;
}
// process sub-packages as well: add runmode suffix and update dependencies
else if (StringUtils.equals(FilenameUtils.getExtension(zipInEntry.getName()), "zip")) {
File tempSubPackageFile = File.createTempFile(FilenameUtils.getBaseName(zipInEntry.getName()), ".zip");
try (FileOutputStream subPackageFos = new FileOutputStream(tempSubPackageFile)) {
IOUtils.copy(is, subPackageFos);
}
// check if contained ZIP file is really a content package
// then process it as well, remove if from the content package is was contained in
// and add it as "1st level package" to the all package
TemporaryContentPackageFile tempSubPackage = new TemporaryContentPackageFile(tempSubPackageFile, pkg.getVariants());
if (packageTypeValidation == PackageTypeValidation.STRICT && !isValidPackageType(tempSubPackage)) {
throw new IllegalArgumentException("Package " + pkg.getPackageInfo() + " contains sub package " + tempSubPackage.getPackageInfo()
+ " with invalid package type: '" + StringUtils.defaultString(tempSubPackage.getPackageType()) + "'");
}
if (StringUtils.isNoneBlank(tempSubPackage.getGroup(), tempSubPackage.getName())) {
subPackages.add(tempSubPackage);
processedEntry = true;
}
else {
FileUtils.deleteQuietly(tempSubPackageFile);
}
}
// otherwise transfer the binary data 1:1
if (!processedEntry) {
ZipArchiveEntry zipOutEntry = newZipEntry(zipInEntry);
zipOut.putArchiveEntry(zipOutEntry);
IOUtils.copy(is, zipOut);
zipOut.closeArchiveEntry();
}
}
}
}
}
// add sub package metadata to set with dependency information
for (TemporaryContentPackageFile tempSubPackage : subPackages) {
addDependencyInformation(allPackagesFromFileSets, tempSubPackage);
}
// process sub packages and add to result
for (TemporaryContentPackageFile tempSubPackage : subPackages) {
result.addAll(processContentPackage(tempSubPackage, previousPkg, environmentRunMode, allPackagesFromFileSets));
}
result.add(new TemporaryContentPackageFile(tempFile, pkg.getVariants()));
}
return result;
}
private ZipArchiveEntry newZipEntry(ZipArchiveEntry in) {
ZipArchiveEntry out = new ZipArchiveEntry(in.getName());
if (buildOutputTimestamp != null && buildOutputTimestamp.isValid()) {
out.setLastModifiedTime(buildOutputTimestamp.toFileTime());
}
else if (in.getLastModifiedTime() != null) {
out.setLastModifiedTime(in.getLastModifiedTime());
}
return out;
}
/**
* Add dependency information to dependencies string in properties (if it does not exist already).
* @param pkg Current content package
* @param props Properties
* @param dependencyFile Dependency package
* @param allPackagesFromFileSets Set with all packages from all file sets as dependency instances
*/
private void updateDependencies(ContentPackageFile pkg, Properties props, ContentPackageFile dependencyFile,
String environmentRunMode, Set<Dependency> allPackagesFromFileSets) {
String[] existingDepsStrings = StringUtils.split(props.getProperty(NAME_DEPENDENCIES), ",");
Dependency[] existingDeps = null;
if (existingDepsStrings != null && existingDepsStrings.length > 0) {
existingDeps = Dependency.fromString(existingDepsStrings);
}
if (existingDeps != null) {
existingDeps = autoDependenciesMode == AutoDependenciesMode.OFF
? rewriteReferencesToManagedPackages(pkg, environmentRunMode, allPackagesFromFileSets, existingDeps)
: removeReferencesToManagedPackages(existingDeps, allPackagesFromFileSets);
}
Dependency[] deps;
if (dependencyFile != null) {
Dependency newDependency = createDependencyFromContentPackageFile(dependencyFile, environmentRunMode);
deps = addDependency(existingDeps, newDependency);
}
else {
deps = existingDeps;
}
if (deps != null) {
String dependenciesString = Dependency.toString(deps);
props.put(NAME_DEPENDENCIES, dependenciesString);
}
}
private @NotNull Dependency createDependencyFromContentPackageFile(@NotNull ContentPackageFile dependencyFile,
@NotNull String environmentRunMode) {
String runModeSuffix = buildRunModeSuffix(dependencyFile, environmentRunMode);
String dependencyVersion = dependencyFile.getVersion() + buildVersionSuffix(dependencyFile, true);
return new Dependency(dependencyFile.getGroup(),
dependencyFile.getName() + runModeSuffix,
VersionRange.fromString(dependencyVersion));
}
private static Dependency[] addDependency(Dependency[] existingDeps, Dependency newDependency) {
if (existingDeps != null) {
return DependencyUtil.add(existingDeps, newDependency);
}
else {
return new Dependency[] { newDependency };
}
}
private static void addSuffixToPackageName(Properties props, ContentPackageFile pkg, String environmentRunMode) {
String runModeSuffix = buildRunModeSuffix(pkg, environmentRunMode);
String packageName = props.getProperty(NAME_NAME) + runModeSuffix;
props.put(NAME_NAME, packageName);
}
private void addSuffixToVersion(Properties props, ContentPackageFile pkg) {
// package version
if (StringUtils.isEmpty(pkg.getVersion())) {
return;
}
String suffixedVersion = pkg.getVersion() + buildVersionSuffix(pkg, true);
props.put(NAME_VERSION, suffixedVersion);
}
private @NotNull Dependency[] rewriteReferencesToManagedPackages(@NotNull ContentPackageFile pkg,
@NotNull String environmentRunMode, @NotNull Set<Dependency> allPackagesFromFileSets, @NotNull Dependency[] deps) {
return Arrays.stream(deps)
.map(dep -> rewriteReferenceIfDependencyIsManagedPackage(pkg, environmentRunMode, allPackagesFromFileSets, dep))
.toArray(Dependency[]::new);
}
private @NotNull Dependency rewriteReferenceIfDependencyIsManagedPackage(@NotNull ContentPackageFile pkg,
@NotNull String environmentRunMode, @NotNull Set<Dependency> allPackagesFromFileSets, @NotNull Dependency dep) {
// not a managed package, return as is
if (!allPackagesFromFileSets.contains(dep)) {
return dep;
}
return findContentPackageFileForDependency(pkg, dep)
// found a content package file for the dependency, rewrite the dependency
.map(contentPackageFile -> createDependencyFromContentPackageFile(contentPackageFile, environmentRunMode))
// found no content package file for the dependency, use current run mode suffix
.orElseGet(() -> createDependencyWithCurrentPackageRunModeSuffix(pkg, environmentRunMode, dep));
}
private @NotNull Optional<ContentPackageFile> findContentPackageFileForDependency(@NotNull ContentPackageFile pkg,
@NotNull Dependency dep) {
// look for content package in all file sets
return contentPackageFileSets.stream()
// prefer file set which contains the current package to use current run mode
.sorted((fileSet1, fileSet2) -> sortFileSetsContainingPackageFirst(pkg, fileSet1, fileSet2))
.flatMap(fileSet -> fileSet.getFiles().stream())
.filter(contentPackageFile -> isContentPackageForDependency(contentPackageFile, dep))
.findFirst();
}
private int sortFileSetsContainingPackageFirst(@NotNull ContentPackageFile pkg,
@NotNull ContentPackageFileSet fileSet1, @NotNull ContentPackageFileSet fileSet2) {
int fileSet1ContainsPackage = fileSet1.getFiles().contains(pkg) ? 1 : 0;
int fileSet2ContainsPackage = fileSet2.getFiles().contains(pkg) ? 1 : 0;
return fileSet2ContainsPackage - fileSet1ContainsPackage;
}
private boolean isContentPackageForDependency(@NotNull ContentPackageFile contentPackageFile, @NotNull Dependency dep) {
return contentPackageFile.getGroup().equals(dep.getGroup())
&& contentPackageFile.getName().equals(dep.getName());
}
private @NotNull Dependency createDependencyWithCurrentPackageRunModeSuffix(@NotNull ContentPackageFile pkg,
@NotNull String environmentRunMode, @NotNull Dependency dep) {
String runModeSuffix = buildRunModeSuffix(pkg, environmentRunMode);
return new Dependency(dep.getGroup(), dep.getName() + runModeSuffix, dep.getRange());
}
/**
* Removes existing references to packages contained in the list of packages to manage by this builder because
* they are added new (and probably with a different package name) during processing.
* @param deps Dependencies list
* @param allPackagesFromFileSets Set with all packages from all file sets as dependency instances
* @return Dependencies list
*/
private static Dependency[] removeReferencesToManagedPackages(Dependency[] deps, Set<Dependency> allPackagesFromFileSets) {
return Arrays.stream(deps)
.filter(dep -> !allPackagesFromFileSets.contains(dep))
.toArray(size -> new Dependency[size]);
}
private static void addDependencyInformation(Set<Dependency> allPackagesFromFileSets, ContentPackageFile pkg) {
allPackagesFromFileSets.add(new Dependency(pkg.getGroup(), pkg.getName(), VersionRange.fromString(pkg.getVersion())));
}
public String getGroupName() {
return this.groupName;
}
public String getPackageName() {
return this.packageName;
}
public File getTargetFile() {
return this.targetFile;
}
}