AllPackageBuilder.java

  1. /*
  2.  * #%L
  3.  * wcm.io
  4.  * %%
  5.  * Copyright (C) 2020 wcm.io
  6.  * %%
  7.  * Licensed under the Apache License, Version 2.0 (the "License");
  8.  * you may not use this file except in compliance with the License.
  9.  * You may obtain a copy of the License at
  10.  *
  11.  *      http://www.apache.org/licenses/LICENSE-2.0
  12.  *
  13.  * Unless required by applicable law or agreed to in writing, software
  14.  * distributed under the License is distributed on an "AS IS" BASIS,
  15.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16.  * See the License for the specific language governing permissions and
  17.  * limitations under the License.
  18.  * #L%
  19.  */
  20. package io.wcm.devops.conga.plugins.aem.maven.allpackage;

  21. import static io.wcm.devops.conga.generator.util.FileUtil.getCanonicalPath;
  22. import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.RUNMODE_AUTHOR;
  23. import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.RUNMODE_PUBLISH;
  24. import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.eliminateAuthorPublishDuplicates;
  25. import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.isAuthorAndPublish;
  26. import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.isOnlyAuthor;
  27. import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.isOnlyPublish;
  28. import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_DEPENDENCIES;
  29. import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_NAME;
  30. import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_PACKAGE_TYPE;
  31. import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_VERSION;

  32. import java.io.File;
  33. import java.io.FileOutputStream;
  34. import java.io.IOException;
  35. import java.io.InputStream;
  36. import java.util.ArrayList;
  37. import java.util.Arrays;
  38. import java.util.Collection;
  39. import java.util.Collections;
  40. import java.util.Enumeration;
  41. import java.util.HashSet;
  42. import java.util.List;
  43. import java.util.Map;
  44. import java.util.Objects;
  45. import java.util.Optional;
  46. import java.util.Properties;
  47. import java.util.Set;
  48. import java.util.stream.Collectors;

  49. import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
  50. import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
  51. import org.apache.commons.compress.archivers.zip.ZipFile;
  52. import org.apache.commons.io.FileUtils;
  53. import org.apache.commons.io.FilenameUtils;
  54. import org.apache.commons.io.IOUtils;
  55. import org.apache.commons.lang3.StringUtils;
  56. import org.apache.jackrabbit.vault.packaging.Dependency;
  57. import org.apache.jackrabbit.vault.packaging.DependencyUtil;
  58. import org.apache.jackrabbit.vault.packaging.PackageType;
  59. import org.apache.jackrabbit.vault.packaging.VersionRange;
  60. import org.apache.maven.artifact.ArtifactUtils;
  61. import org.apache.maven.plugin.logging.Log;
  62. import org.apache.maven.plugin.logging.SystemStreamLog;
  63. import org.jetbrains.annotations.NotNull;
  64. import org.jetbrains.annotations.Nullable;

  65. import io.wcm.devops.conga.plugins.aem.maven.AutoDependenciesMode;
  66. import io.wcm.devops.conga.plugins.aem.maven.BuildOutputTimestamp;
  67. import io.wcm.devops.conga.plugins.aem.maven.PackageTypeValidation;
  68. import io.wcm.devops.conga.plugins.aem.maven.PackageVersionMode;
  69. import io.wcm.devops.conga.plugins.aem.maven.RunModeOptimization;
  70. import io.wcm.devops.conga.plugins.aem.maven.model.BundleFile;
  71. import io.wcm.devops.conga.plugins.aem.maven.model.ContentPackageFile;
  72. import io.wcm.devops.conga.plugins.aem.maven.model.InstallableFile;
  73. import io.wcm.tooling.commons.contentpackagebuilder.ContentPackage;
  74. import io.wcm.tooling.commons.contentpackagebuilder.ContentPackageBuilder;
  75. import io.wcm.tooling.commons.contentpackagebuilder.PackageFilter;

  76. /**
  77.  * Builds "all" package based on given set of content packages.
  78.  * <p>
  79.  * General concept:
  80.  * </p>
  81.  * <ul>
  82.  * <li>Iterates through all content packages that are generated or collected by CONGA and contained in the
  83.  * model.json</li>
  84.  * <li>Enforces the order defined in CONGA by automatically adding dependencies to all packages reflecting the file
  85.  * order in model.json</li>
  86.  * <li>Because the dependency chain may be different for each runmode (author/publish), each package is added once for
  87.  * each runmode. Internally this separate dependency change for author and publish is optimized to have each package
  88.  * included only once for author+publish, unless it has a different chain of dependencies for both runmodes, in which
  89.  * case it is included separately for each run mode.</li>
  90.  * <li>To avoid conflicts with duplicate packages with different dependency chains the names of packages that are
  91.  * included in different versions for author/publish are changed and a runmode suffix (.author or .publish) is added,
  92.  * and it is put in a corresponding install folder.</li>
  93.  * <li>To avoid problems with nested sub packages, the sub packages are extracted from the packages and treated in the
  94.  * same way as other packages.</li>
  95.  * </ul>
  96.  */
  97. public final class AllPackageBuilder {

  98.   private final File targetFile;
  99.   private final String groupName;
  100.   private final String packageName;
  101.   private String version;
  102.   private AutoDependenciesMode autoDependenciesMode = AutoDependenciesMode.OFF;
  103.   private RunModeOptimization runModeOptimization = RunModeOptimization.OFF;
  104.   private PackageTypeValidation packageTypeValidation = PackageTypeValidation.STRICT;
  105.   private PackageVersionMode packageVersionMode = PackageVersionMode.DEFAULT;
  106.   private Log log;
  107.   private BuildOutputTimestamp buildOutputTimestamp;

  108.   private static final String RUNMODE_DEFAULT = "$default$";
  109.   private static final Set<String> ALLOWED_PACKAGE_TYPES = Set.of(
  110.       PackageType.APPLICATION.name().toLowerCase(),
  111.       PackageType.CONTAINER.name().toLowerCase(),
  112.       PackageType.CONTENT.name().toLowerCase());
  113.   private static final String VERSION_SUFFIX_SEPARATOR = "-";

  114.   private final List<ContentPackageFileSet> contentPackageFileSets = new ArrayList<>();
  115.   private final List<BundleFileSet> bundleFileSets = new ArrayList<>();

  116.   /**
  117.    * @param targetFile Target file
  118.    * @param groupName Group name
  119.    * @param packageName Package name
  120.    */
  121.   public AllPackageBuilder(File targetFile, String groupName, String packageName) {
  122.     this.targetFile = targetFile;
  123.     this.groupName = groupName;
  124.     this.packageName = packageName;
  125.   }

  126.   /**
  127.    * @param value Automatically generate dependencies between content packages based on file order in CONGA
  128.    *          configuration.
  129.    * @return this
  130.    */
  131.   public AllPackageBuilder autoDependenciesMode(AutoDependenciesMode value) {
  132.     this.autoDependenciesMode = value;
  133.     return this;
  134.   }

  135.   /**
  136.    * @param value Configure run mode optimization.
  137.    * @return this
  138.    */
  139.   public AllPackageBuilder runModeOptimization(RunModeOptimization value) {
  140.     this.runModeOptimization = value;
  141.     return this;
  142.   }

  143.   /**
  144.    * @param value How to validate package types to be included in "all" package.
  145.    * @return this
  146.    */
  147.   public AllPackageBuilder packageTypeValidation(PackageTypeValidation value) {
  148.     this.packageTypeValidation = value;
  149.     return this;
  150.   }

  151.   /**
  152.    * @param value How to handle versions of packages and sub-packages inside "all" package.
  153.    * @return this
  154.    */
  155.   public AllPackageBuilder packageVersionMode(PackageVersionMode value) {
  156.     this.packageVersionMode = value;
  157.     return this;
  158.   }

  159.   /**
  160.    * @param value Maven logger
  161.    * @return this
  162.    */
  163.   public AllPackageBuilder logger(Log value) {
  164.     this.log = value;
  165.     return this;
  166.   }

  167.   /**
  168.    * @param value Package version
  169.    * @return this
  170.    */
  171.   public AllPackageBuilder version(String value) {
  172.     this.version = value;
  173.     return this;
  174.   }

  175.   /**
  176.    * @param value Build output timestamp
  177.    * @return this
  178.    */
  179.   public AllPackageBuilder buildOutputTimestamp(BuildOutputTimestamp value) {
  180.     this.buildOutputTimestamp = value;
  181.     return this;
  182.   }

  183.   private Log getLog() {
  184.     if (this.log == null) {
  185.       this.log = new SystemStreamLog();
  186.     }
  187.     return this.log;
  188.   }

  189.   /**
  190.    * Add content packages and OSGi bundles to be contained in "all" content package.
  191.    * @param files Content packages (invalid will be filtered out) and OSGi bundles
  192.    * @param cloudManagerTarget Target environments/run modes the packages should be attached to
  193.    * @throws IllegalArgumentException If and invalid package type is detected
  194.    */
  195.   public void add(List<InstallableFile> files, Set<String> cloudManagerTarget) {
  196.     List<ContentPackageFile> contentPackages = filterFiles(files, ContentPackageFile.class);

  197.     // collect list of cloud manager environment run modes
  198.     List<String> environmentRunModes = new ArrayList<>();
  199.     if (cloudManagerTarget.isEmpty()) {
  200.       environmentRunModes.add(RUNMODE_DEFAULT);
  201.     }
  202.     else {
  203.       environmentRunModes.addAll(cloudManagerTarget);
  204.     }

  205.     List<ContentPackageFile> validContentPackages;
  206.     switch (packageTypeValidation) {
  207.       case STRICT:
  208.         validContentPackages = getValidContentPackagesStrictValidation(contentPackages);
  209.         break;
  210.       case WARN:
  211.         validContentPackages = getValidContentPackagesWarnValidation(contentPackages);
  212.         break;
  213.       default:
  214.         throw new IllegalArgumentException("Unsupported package type validation: " + packageTypeValidation);
  215.     }

  216.     if (!validContentPackages.isEmpty()) {
  217.       contentPackageFileSets.add(new ContentPackageFileSet(validContentPackages, environmentRunModes));
  218.     }

  219.     // add OSGi bundles
  220.     List<BundleFile> bundles = filterFiles(files, BundleFile.class);
  221.     if (!bundles.isEmpty()) {
  222.       bundleFileSets.add(new BundleFileSet(bundles, environmentRunModes));
  223.     }
  224.   }

  225.   /**
  226.    * Get valid content packages in strict mode: Ignore content packages without package type (with warning),
  227.    * fail build if Content package with "mixed" mode is found.
  228.    * @param contentPackages Content packages
  229.    * @return Valid content packages
  230.    */
  231.   private List<ContentPackageFile> getValidContentPackagesStrictValidation(List<? extends ContentPackageFile> contentPackages) {
  232.     // generate warning for each content packages without package type that is skipped
  233.     contentPackages.stream()
  234.         .filter(pkg -> !hasPackageType(pkg))
  235.         .forEach(pkg -> getLog().warn("Skipping content package without package type: " + getCanonicalPath(pkg.getFile())));

  236.     // fail build if content packages with non-allowed package types exist
  237.     List<ContentPackageFile> invalidPackageTypeContentPackages = contentPackages.stream()
  238.         .filter(AllPackageBuilder::hasPackageType)
  239.         .filter(pkg -> !isValidPackageType(pkg))
  240.         .collect(Collectors.toList());
  241.     if (!invalidPackageTypeContentPackages.isEmpty()) {
  242.       throw new IllegalArgumentException("Content packages found with unsupported package types: " +
  243.           invalidPackageTypeContentPackages.stream()
  244.               .map(pkg -> pkg.getName() + " -> " + pkg.getPackageType())
  245.               .collect(Collectors.joining(", ")));
  246.     }

  247.     // collect AEM content packages with package type
  248.     return contentPackages.stream()
  249.         .filter(AllPackageBuilder::hasPackageType)
  250.         .collect(Collectors.toList());
  251.   }

  252.   /**
  253.    * Get all content packages, generate warnings if package type is missing or "mixed" mode package type is used.
  254.    * @param contentPackages Content packages
  255.    * @return Valid content packages
  256.    */
  257.   private List<ContentPackageFile> getValidContentPackagesWarnValidation(List<? extends ContentPackageFile> contentPackages) {
  258.     // generate warning for each content packages without package type
  259.     contentPackages.stream()
  260.         .filter(pkg -> !hasPackageType(pkg))
  261.         .forEach(pkg -> getLog().warn("Found content package without package type: " + getCanonicalPath(pkg.getFile())));

  262.     // generate warning for each content packages with invalid package type
  263.     contentPackages.stream()
  264.         .filter(AllPackageBuilder::hasPackageType)
  265.         .filter(pkg -> !isValidPackageType(pkg))
  266.         .forEach(pkg -> getLog().warn("Found content package with invalid package type: "
  267.             + getCanonicalPath(pkg.getFile()) + " -> " + pkg.getPackageType()));

  268.     // return all content packages
  269.     return contentPackages.stream().collect(Collectors.toList());
  270.   }

  271.   private static <T> List<T> filterFiles(List<? extends InstallableFile> files, Class<T> fileClass) {
  272.     return files.stream()
  273.         .filter(fileClass::isInstance)
  274.         .map(fileClass::cast)
  275.         .collect(Collectors.toList());
  276.   }

  277.   /**
  278.    * Build "all" content package.
  279.    * @param properties Specifies additional properties to be set in the properties.xml file.
  280.    * @return true if "all" package was generated, false if no valid package was found.
  281.    * @throws IOException I/O exception
  282.    */
  283.   public boolean build(Map<String, String> properties) throws IOException {

  284.     if (contentPackageFileSets.isEmpty()) {
  285.       return false;
  286.     }

  287.     // prepare content package metadata
  288.     ContentPackageBuilder builder = new ContentPackageBuilder()
  289.         .group(groupName)
  290.         .name(packageName)
  291.         .packageType("container");
  292.     if (version != null) {
  293.       builder.version(version);
  294.     }

  295.     // define root path for "all" package
  296.     String rootPath = buildRootPath(groupName, packageName);
  297.     builder.filter(new PackageFilter(rootPath));

  298.     // additional package properties
  299.     if (properties != null) {
  300.       properties.entrySet().forEach(entry -> builder.property(entry.getKey(), entry.getValue()));
  301.     }

  302.     // build content package
  303.     try (ContentPackage contentPackage = builder.build(targetFile)) {
  304.       buildAddContentPackages(contentPackage, rootPath);
  305.       buildAddBundles(contentPackage, rootPath);
  306.     }

  307.     return true;
  308.   }

  309.   @SuppressWarnings("java:S3776") // ignore complexity
  310.   private void buildAddContentPackages(ContentPackage contentPackage, String rootPath) throws IOException {
  311.     // build set with dependencies instances for each package contained in all filesets
  312.     Set<Dependency> allPackagesFromFileSets = new HashSet<>();
  313.     for (ContentPackageFileSet fileSet : contentPackageFileSets) {
  314.       for (ContentPackageFile pkg : fileSet.getFiles()) {
  315.         addDependencyInformation(allPackagesFromFileSets, pkg);
  316.       }
  317.     }

  318.     Collection<ContentPackageFileSet> processedFileSets;
  319.     if (runModeOptimization == RunModeOptimization.ELIMINATE_DUPLICATES) {
  320.       // eliminate duplicates which are same for author and publish
  321.       processedFileSets = eliminateAuthorPublishDuplicates(contentPackageFileSets,
  322.         environmentRunMode -> new ContentPackageFileSet(new ArrayList<>(), Collections.singletonList(environmentRunMode)));
  323.     }
  324.     else {
  325.       processedFileSets = contentPackageFileSets;
  326.     }

  327.     for (ContentPackageFileSet fileSet : processedFileSets) {
  328.       for (String environmentRunMode : fileSet.getEnvironmentRunModes()) {
  329.         List<ContentPackageFile> previousPackages = new ArrayList<>();
  330.         for (ContentPackageFile pkg : fileSet.getFiles()) {
  331.           ContentPackageFile previousPkg = getDependencyChainPreviousPackage(pkg, previousPackages);

  332.           // set package name, wire previous package in package dependency
  333.           List<TemporaryContentPackageFile> processedFiles = processContentPackage(pkg, previousPkg, environmentRunMode, allPackagesFromFileSets);

  334.           // add processed content packages to "all" content package - and delete the temporary files
  335.           try {
  336.             for (TemporaryContentPackageFile processedFile : processedFiles) {
  337.               String path = buildPackagePath(processedFile, rootPath, environmentRunMode);
  338.               contentPackage.addFile(path, processedFile.getFile());
  339.               if (log.isDebugEnabled()) {
  340.                 log.debug("  Add " + processedFile.getPackageInfoWithDependencies());
  341.               }
  342.             }
  343.           }
  344.           finally {
  345.             processedFiles.stream()
  346.                 .map(TemporaryContentPackageFile::getFile)
  347.                 .forEach(FileUtils::deleteQuietly);
  348.           }

  349.           previousPackages.add(pkg);
  350.         }
  351.       }
  352.     }
  353.   }

  354.   /**
  355.    * Gets the previous package in the order defined by CONGA to define as package dependency in current package.
  356.    * @param currentPackage Current package
  357.    * @param previousPackages List of previous packages
  358.    * @return Package to define as dependency, or null if no dependency should be defined
  359.    */
  360.   private @Nullable ContentPackageFile getDependencyChainPreviousPackage(@NotNull ContentPackageFile currentPackage,
  361.       @NotNull List<ContentPackageFile> previousPackages) {
  362.     if ((autoDependenciesMode == AutoDependenciesMode.OFF)
  363.         || (autoDependenciesMode == AutoDependenciesMode.IMMUTABLE_ONLY && isMutable(currentPackage))) {
  364.       return null;
  365.     }
  366.     // get last previous package
  367.     return previousPackages.stream()
  368.         // if not IMMUTABLE_MUTABLE_COMBINED active only that of the same mutability type
  369.         .filter(item -> (autoDependenciesMode == AutoDependenciesMode.IMMUTABLE_MUTABLE_COMBINED) || mutableMatches(item, currentPackage))
  370.         // make sure author-only or publish-only packages are only taken into account if the current package has same restriction
  371.         .filter(item -> isAuthorAndPublish(item)
  372.             || (isOnlyAuthor(item) && isOnlyAuthor(currentPackage))
  373.             || (isOnlyPublish(item) && isOnlyPublish(currentPackage)))
  374.         // get last in list
  375.         .reduce((first, second) -> second).orElse(null);
  376.   }

  377.   private void buildAddBundles(ContentPackage contentPackage, String rootPath) throws IOException {
  378.     Collection<BundleFileSet> processedFileSets;
  379.     if (runModeOptimization == RunModeOptimization.ELIMINATE_DUPLICATES) {
  380.       // eliminate duplicates which are same for author and publish
  381.       processedFileSets = eliminateAuthorPublishDuplicates(bundleFileSets,
  382.           environmentRunMode -> new BundleFileSet(new ArrayList<>(), Collections.singletonList(environmentRunMode)));
  383.     }
  384.     else {
  385.       processedFileSets = bundleFileSets;
  386.     }

  387.     for (BundleFileSet bundleFileSet : processedFileSets) {
  388.       for (String environmentRunMode : bundleFileSet.getEnvironmentRunModes()) {
  389.         for (BundleFile bundleFile : bundleFileSet.getFiles()) {
  390.           String path = buildBundlePath(bundleFile, rootPath, environmentRunMode);
  391.           contentPackage.addFile(path, bundleFile.getFile());
  392.         }
  393.       }
  394.     }
  395.   }

  396.   private static boolean hasPackageType(ContentPackageFile pkg) {
  397.     // accept only content packages with a package type set
  398.     return pkg.getPackageType() != null;
  399.   }

  400.   private static boolean isValidPackageType(ContentPackageFile pkg) {
  401.     // check if the package type is an allowed package type
  402.     return ALLOWED_PACKAGE_TYPES.contains(pkg.getPackageType());
  403.   }

  404.   private static boolean isMutable(ContentPackageFile pkg) {
  405.     return StringUtils.equals("content", pkg.getPackageType());
  406.   }

  407.   private static boolean mutableMatches(ContentPackageFile pkg1, ContentPackageFile pkg2) {
  408.     if (pkg1 == null || pkg2 == null) {
  409.       return false;
  410.     }
  411.     return isMutable(pkg1) == isMutable(pkg2);
  412.   }

  413.   /**
  414.    * Build root path to be used for embedded package.
  415.    * @param groupName Group name
  416.    * @param packageName Package name
  417.    * @return Package path
  418.    */
  419.   private static String buildRootPath(String groupName, String packageName) {
  420.     return "/apps/" + groupName + "-" + packageName + "-packages";
  421.   }

  422.   /**
  423.    * Generate suffix for instance and environment run modes.
  424.    * @param file File
  425.    * @return Suffix string
  426.    */
  427.   private static String buildRunModeSuffix(InstallableFile file, String environmentRunMode) {
  428.     StringBuilder runModeSuffix = new StringBuilder();
  429.     if (isOnlyAuthor(file)) {
  430.       runModeSuffix.append(".").append(RUNMODE_AUTHOR);
  431.     }
  432.     else if (isOnlyPublish(file)) {
  433.       runModeSuffix.append(".").append(RUNMODE_PUBLISH);
  434.     }
  435.     if (!StringUtils.equals(environmentRunMode, RUNMODE_DEFAULT)) {
  436.       runModeSuffix.append(".").append(environmentRunMode);
  437.     }
  438.     return runModeSuffix.toString();
  439.   }

  440.   /**
  441.    * Generate suffix for versions of content packages.
  442.    * @param pkg Content package
  443.    * @param ignoreSnapshot Do not build version suffix for SNAPSHOT versions
  444.    * @return Suffix string
  445.    */
  446.   private String buildVersionSuffix(ContentPackageFile pkg, boolean ignoreSnapshot) {
  447.     StringBuilder versionSuffix = new StringBuilder();

  448.     if (this.packageVersionMode == PackageVersionMode.RELEASE_SUFFIX_VERSION
  449.         && (!ArtifactUtils.isSnapshot(pkg.getVersion()) || !ignoreSnapshot)
  450.         && !StringUtils.equals(pkg.getVersion(), this.version)
  451.         && this.version != null) {
  452.       versionSuffix.append(VERSION_SUFFIX_SEPARATOR)
  453.           // replace dots with underlines in version suffix to avoid confusion with main version number
  454.           .append(StringUtils.replace(this.version, ".", "_"));
  455.     }

  456.     return versionSuffix.toString();
  457.   }

  458.   /**
  459.    * Build path to be used for embedded package.
  460.    * @param pkg Package
  461.    * @param rootPath Root path
  462.    * @return Package path
  463.    */
  464.   @SuppressWarnings("java:S1075") // no filesystem path
  465.   private String buildPackagePath(ContentPackageFile pkg, String rootPath, String environmentRunMode) {
  466.     if (packageTypeValidation == PackageTypeValidation.STRICT && !isValidPackageType(pkg)) {
  467.       throw new IllegalArgumentException("Package " + pkg.getPackageInfo() + " has invalid package type: '" + pkg.getPackageType() + "'.");
  468.     }

  469.     String runModeSuffix = buildRunModeSuffix(pkg, environmentRunMode);

  470.     // add run mode suffix to both install folder path and package file name
  471.     String path = rootPath + "/" + Objects.toString(pkg.getPackageType(), "misc") + "/install" + runModeSuffix;

  472.     String versionSuffix = "";
  473.     String packageVersion = pkg.getVersion();
  474.     String packageVersionWithoutSuffix = packageVersion;
  475.     if (this.packageVersionMode == PackageVersionMode.RELEASE_SUFFIX_VERSION && this.version != null) {
  476.       packageVersionWithoutSuffix = StringUtils.removeEnd(packageVersion, buildVersionSuffix(pkg, false));
  477.     }
  478.     if (packageVersion != null && pkg.getFile().getName().contains(packageVersionWithoutSuffix)) {
  479.       versionSuffix = "-" + packageVersion;
  480.     }
  481.     String fileName = pkg.getName() + versionSuffix
  482.         + "." + FilenameUtils.getExtension(pkg.getFile().getName());
  483.     return path + "/" + fileName;
  484.   }

  485.   /**
  486.    * Build path to be used for embedded bundle.
  487.    * @param bundleFile Bundle
  488.    * @param rootPath Root path
  489.    * @return Package path
  490.    */
  491.   private static String buildBundlePath(BundleFile bundleFile, String rootPath, String environmentRunMode) {
  492.     String runModeSuffix = buildRunModeSuffix(bundleFile, environmentRunMode);

  493.     // add run mode suffix to both install folder path and package file name
  494.     String path = rootPath + "/application/install" + runModeSuffix;

  495.     return path + "/" + bundleFile.getFile().getName();
  496.   }

  497.   /**
  498.    * Rewrite content package ZIP file while adding to "all" package:
  499.    * Add dependency to previous package in CONGA configuration file oder.
  500.    * @param pkg Package to process (can be parent packe of the actual file)
  501.    * @param previousPkg Previous package to get dependency information from.
  502.    *          Is null if no previous package exists or auto dependency mode is switched off.
  503.    * @param environmentRunMode Environment run mode
  504.    * @param allPackagesFromFileSets Set with all packages from all file sets as dependency instances
  505.    * @return Returns a list of content package *temporary* files - have to be deleted when processing is completed.
  506.    * @throws IOException I/O error
  507.    */
  508.   @SuppressWarnings("java:S3776") // ignore complexity
  509.   private List<TemporaryContentPackageFile> processContentPackage(ContentPackageFile pkg,
  510.       ContentPackageFile previousPkg, String environmentRunMode,
  511.       Set<Dependency> allPackagesFromFileSets) throws IOException {

  512.     List<TemporaryContentPackageFile> result = new ArrayList<>();
  513.     List<TemporaryContentPackageFile> subPackages = new ArrayList<>();

  514.     // create temp zip file to create rewritten copy of package
  515.     File tempFile = File.createTempFile(FilenameUtils.getBaseName(pkg.getFile().getName()), ".zip");

  516.     // open original content package
  517.     try (ZipFile zipFileIn = new ZipFile.Builder().setFile(pkg.getFile()).get()) {

  518.       // iterate through entries and write them to the temp. zip file
  519.       try (FileOutputStream fos = new FileOutputStream(tempFile);
  520.           ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(fos)) {
  521.         Enumeration<? extends ZipArchiveEntry> zipInEntries = zipFileIn.getEntries();
  522.         while (zipInEntries.hasMoreElements()) {
  523.           ZipArchiveEntry zipInEntry = zipInEntries.nextElement();
  524.           if (!zipInEntry.isDirectory()) {
  525.             try (InputStream is = zipFileIn.getInputStream(zipInEntry)) {
  526.               boolean processedEntry = false;

  527.               // if entry is properties.xml, update dependency information
  528.               if (StringUtils.equals(zipInEntry.getName(), "META-INF/vault/properties.xml")) {
  529.                 FileVaultProperties fileVaultProps = new FileVaultProperties(is);
  530.                 Properties props = fileVaultProps.getProperties();
  531.                 addSuffixToPackageName(props, pkg, environmentRunMode);
  532.                 addSuffixToVersion(props, pkg);

  533.                 // update package dependencies
  534.                 ContentPackageFile dependencyFile = previousPkg;
  535.                 if (autoDependenciesMode == AutoDependenciesMode.OFF) {
  536.                   dependencyFile = null;
  537.                 }
  538.                 updateDependencies(pkg, props, dependencyFile, environmentRunMode, allPackagesFromFileSets);

  539.                 // if package type is missing in package properties, put in the type defined in model
  540.                 String packageType = pkg.getPackageType();
  541.                 if (props.get(NAME_PACKAGE_TYPE) == null && packageType != null) {
  542.                   props.put(NAME_PACKAGE_TYPE, packageType);
  543.                 }

  544.                 ZipArchiveEntry zipOutEntry = newZipEntry(zipInEntry);
  545.                 zipOut.putArchiveEntry(zipOutEntry);
  546.                 fileVaultProps.storeToXml(zipOut);
  547.                 zipOut.closeArchiveEntry();
  548.                 processedEntry = true;
  549.               }

  550.               // process sub-packages as well: add runmode suffix and update dependencies
  551.               else if (StringUtils.equals(FilenameUtils.getExtension(zipInEntry.getName()), "zip")) {
  552.                 File tempSubPackageFile = File.createTempFile(FilenameUtils.getBaseName(zipInEntry.getName()), ".zip");
  553.                 try (FileOutputStream subPackageFos = new FileOutputStream(tempSubPackageFile)) {
  554.                   IOUtils.copy(is, subPackageFos);
  555.                 }

  556.                 // check if contained ZIP file is really a content package
  557.                 // then process it as well, remove if from the content package is was contained in
  558.                 // and add it as "1st level package" to the all package
  559.                 TemporaryContentPackageFile tempSubPackage = new TemporaryContentPackageFile(tempSubPackageFile, pkg.getVariants());
  560.                 if (packageTypeValidation == PackageTypeValidation.STRICT && !isValidPackageType(tempSubPackage)) {
  561.                   throw new IllegalArgumentException("Package " + pkg.getPackageInfo() + " contains sub package " + tempSubPackage.getPackageInfo()
  562.                       + " with invalid package type: '" + StringUtils.defaultString(tempSubPackage.getPackageType()) + "'");
  563.                 }
  564.                 if (StringUtils.isNoneBlank(tempSubPackage.getGroup(), tempSubPackage.getName())) {
  565.                   subPackages.add(tempSubPackage);
  566.                   processedEntry = true;
  567.                 }
  568.                 else {
  569.                   FileUtils.deleteQuietly(tempSubPackageFile);
  570.                 }
  571.               }

  572.               // otherwise transfer the binary data 1:1
  573.               if (!processedEntry) {
  574.                 ZipArchiveEntry zipOutEntry = newZipEntry(zipInEntry);
  575.                 zipOut.putArchiveEntry(zipOutEntry);
  576.                 IOUtils.copy(is, zipOut);
  577.                 zipOut.closeArchiveEntry();
  578.               }
  579.             }

  580.           }
  581.         }
  582.       }

  583.       // add sub package metadata to set with dependency information
  584.       for (TemporaryContentPackageFile tempSubPackage : subPackages) {
  585.         addDependencyInformation(allPackagesFromFileSets, tempSubPackage);
  586.       }

  587.       // process sub packages and add to result
  588.       for (TemporaryContentPackageFile tempSubPackage : subPackages) {
  589.         result.addAll(processContentPackage(tempSubPackage, previousPkg, environmentRunMode, allPackagesFromFileSets));
  590.       }

  591.       result.add(new TemporaryContentPackageFile(tempFile, pkg.getVariants()));
  592.     }
  593.     return result;
  594.   }

  595.   private ZipArchiveEntry newZipEntry(ZipArchiveEntry in) {
  596.     ZipArchiveEntry out = new ZipArchiveEntry(in.getName());
  597.     if (buildOutputTimestamp != null && buildOutputTimestamp.isValid()) {
  598.       out.setLastModifiedTime(buildOutputTimestamp.toFileTime());
  599.     }
  600.     else if (in.getLastModifiedTime() != null) {
  601.       out.setLastModifiedTime(in.getLastModifiedTime());
  602.     }
  603.     return out;
  604.   }

  605.   /**
  606.    * Add dependency information to dependencies string in properties (if it does not exist already).
  607.    * @param pkg Current content package
  608.    * @param props Properties
  609.    * @param dependencyFile Dependency package
  610.    * @param allPackagesFromFileSets Set with all packages from all file sets as dependency instances
  611.    */
  612.   private void updateDependencies(ContentPackageFile pkg, Properties props, ContentPackageFile dependencyFile,
  613.       String environmentRunMode, Set<Dependency> allPackagesFromFileSets) {
  614.     String[] existingDepsStrings = StringUtils.split(props.getProperty(NAME_DEPENDENCIES), ",");
  615.     Dependency[] existingDeps = null;
  616.     if (existingDepsStrings != null && existingDepsStrings.length > 0) {
  617.       existingDeps = Dependency.fromString(existingDepsStrings);
  618.     }
  619.     if (existingDeps != null) {
  620.       existingDeps = autoDependenciesMode == AutoDependenciesMode.OFF
  621.           ? rewriteReferencesToManagedPackages(pkg, environmentRunMode, allPackagesFromFileSets, existingDeps)
  622.           : removeReferencesToManagedPackages(existingDeps, allPackagesFromFileSets);
  623.     }

  624.     Dependency[] deps;
  625.     if (dependencyFile != null) {
  626.       Dependency newDependency = createDependencyFromContentPackageFile(dependencyFile, environmentRunMode);
  627.       deps = addDependency(existingDeps, newDependency);
  628.     }
  629.     else {
  630.       deps = existingDeps;
  631.     }

  632.     if (deps != null) {
  633.       String dependenciesString = Dependency.toString(deps);
  634.       props.put(NAME_DEPENDENCIES, dependenciesString);
  635.     }
  636.   }

  637.   private @NotNull Dependency createDependencyFromContentPackageFile(@NotNull ContentPackageFile dependencyFile,
  638.       @NotNull String environmentRunMode) {
  639.     String runModeSuffix = buildRunModeSuffix(dependencyFile, environmentRunMode);
  640.     String dependencyVersion = dependencyFile.getVersion() + buildVersionSuffix(dependencyFile, true);
  641.     return new Dependency(dependencyFile.getGroup(),
  642.         dependencyFile.getName() + runModeSuffix,
  643.         VersionRange.fromString(dependencyVersion));
  644.   }

  645.   private static Dependency[] addDependency(Dependency[] existingDeps, Dependency newDependency) {
  646.     if (existingDeps != null) {
  647.       return DependencyUtil.add(existingDeps, newDependency);
  648.     }
  649.     else {
  650.       return new Dependency[] { newDependency };
  651.     }
  652.   }

  653.   private static void addSuffixToPackageName(Properties props, ContentPackageFile pkg, String environmentRunMode) {
  654.     String runModeSuffix = buildRunModeSuffix(pkg, environmentRunMode);
  655.     String packageName = props.getProperty(NAME_NAME) + runModeSuffix;
  656.     props.put(NAME_NAME, packageName);
  657.   }

  658.   private void addSuffixToVersion(Properties props, ContentPackageFile pkg) {
  659.     // package version
  660.     if (StringUtils.isEmpty(pkg.getVersion())) {
  661.       return;
  662.     }
  663.     String suffixedVersion = pkg.getVersion() + buildVersionSuffix(pkg, true);
  664.     props.put(NAME_VERSION, suffixedVersion);
  665.   }

  666.   private @NotNull Dependency[] rewriteReferencesToManagedPackages(@NotNull ContentPackageFile pkg,
  667.       @NotNull String environmentRunMode, @NotNull Set<Dependency> allPackagesFromFileSets, @NotNull Dependency[] deps) {
  668.     return Arrays.stream(deps)
  669.         .map(dep -> rewriteReferenceIfDependencyIsManagedPackage(pkg, environmentRunMode, allPackagesFromFileSets, dep))
  670.         .toArray(Dependency[]::new);
  671.   }

  672.   private @NotNull Dependency rewriteReferenceIfDependencyIsManagedPackage(@NotNull ContentPackageFile pkg,
  673.       @NotNull String environmentRunMode, @NotNull Set<Dependency> allPackagesFromFileSets, @NotNull Dependency dep) {
  674.     // not a managed package, return as is
  675.     if (!allPackagesFromFileSets.contains(dep)) {
  676.       return dep;
  677.     }
  678.     return findContentPackageFileForDependency(pkg, dep)
  679.         // found a content package file for the dependency, rewrite the dependency
  680.         .map(contentPackageFile -> createDependencyFromContentPackageFile(contentPackageFile, environmentRunMode))
  681.         // found no content package file for the dependency, use current run mode suffix
  682.         .orElseGet(() -> createDependencyWithCurrentPackageRunModeSuffix(pkg, environmentRunMode, dep));
  683.   }

  684.   private @NotNull Optional<ContentPackageFile> findContentPackageFileForDependency(@NotNull ContentPackageFile pkg,
  685.       @NotNull Dependency dep) {
  686.     // look for content package in all file sets
  687.     return contentPackageFileSets.stream()
  688.             // prefer file set which contains the current package to use current run mode
  689.             .sorted((fileSet1, fileSet2) -> sortFileSetsContainingPackageFirst(pkg, fileSet1, fileSet2))
  690.             .flatMap(fileSet -> fileSet.getFiles().stream())
  691.             .filter(contentPackageFile -> isContentPackageForDependency(contentPackageFile, dep))
  692.             .findFirst();
  693.   }

  694.   private int sortFileSetsContainingPackageFirst(@NotNull ContentPackageFile pkg,
  695.       @NotNull ContentPackageFileSet fileSet1, @NotNull ContentPackageFileSet fileSet2) {
  696.     int fileSet1ContainsPackage = fileSet1.getFiles().contains(pkg) ? 1 : 0;
  697.     int fileSet2ContainsPackage = fileSet2.getFiles().contains(pkg) ? 1 : 0;
  698.     return fileSet2ContainsPackage - fileSet1ContainsPackage;
  699.   }

  700.   private boolean isContentPackageForDependency(@NotNull ContentPackageFile contentPackageFile, @NotNull Dependency dep) {
  701.     return contentPackageFile.getGroup().equals(dep.getGroup())
  702.             && contentPackageFile.getName().equals(dep.getName());
  703.   }

  704.   private @NotNull Dependency createDependencyWithCurrentPackageRunModeSuffix(@NotNull ContentPackageFile pkg,
  705.       @NotNull String environmentRunMode, @NotNull Dependency dep) {
  706.     String runModeSuffix = buildRunModeSuffix(pkg, environmentRunMode);
  707.     return new Dependency(dep.getGroup(), dep.getName() + runModeSuffix, dep.getRange());
  708.   }

  709.   /**
  710.    * Removes existing references to packages contained in the list of packages to manage by this builder because
  711.    * they are added new (and probably with a different package name) during processing.
  712.    * @param deps Dependencies list
  713.    * @param allPackagesFromFileSets Set with all packages from all file sets as dependency instances
  714.    * @return Dependencies list
  715.    */
  716.   private static Dependency[] removeReferencesToManagedPackages(Dependency[] deps, Set<Dependency> allPackagesFromFileSets) {
  717.     return Arrays.stream(deps)
  718.         .filter(dep -> !allPackagesFromFileSets.contains(dep))
  719.         .toArray(size -> new Dependency[size]);
  720.   }

  721.   private static void addDependencyInformation(Set<Dependency> allPackagesFromFileSets, ContentPackageFile pkg) {
  722.     allPackagesFromFileSets.add(new Dependency(pkg.getGroup(), pkg.getName(), VersionRange.fromString(pkg.getVersion())));
  723.   }

  724.   public String getGroupName() {
  725.     return this.groupName;
  726.   }

  727.   public String getPackageName() {
  728.     return this.packageName;
  729.   }

  730.   public File getTargetFile() {
  731.     return this.targetFile;
  732.   }

  733. }