View Javadoc
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  
22  import static io.wcm.devops.conga.generator.util.FileUtil.getCanonicalPath;
23  import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.RUNMODE_AUTHOR;
24  import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.RUNMODE_PUBLISH;
25  import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.eliminateAuthorPublishDuplicates;
26  import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.isAuthorAndPublish;
27  import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.isOnlyAuthor;
28  import static io.wcm.devops.conga.plugins.aem.maven.allpackage.RunModeUtil.isOnlyPublish;
29  import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_DEPENDENCIES;
30  import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_NAME;
31  import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_PACKAGE_TYPE;
32  import static org.apache.jackrabbit.vault.packaging.PackageProperties.NAME_VERSION;
33  
34  import java.io.File;
35  import java.io.FileOutputStream;
36  import java.io.IOException;
37  import java.io.InputStream;
38  import java.util.ArrayList;
39  import java.util.Arrays;
40  import java.util.Collection;
41  import java.util.Collections;
42  import java.util.Enumeration;
43  import java.util.HashSet;
44  import java.util.List;
45  import java.util.Map;
46  import java.util.Objects;
47  import java.util.Optional;
48  import java.util.Properties;
49  import java.util.Set;
50  import java.util.stream.Collectors;
51  
52  import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
53  import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
54  import org.apache.commons.compress.archivers.zip.ZipFile;
55  import org.apache.commons.io.FileUtils;
56  import org.apache.commons.io.FilenameUtils;
57  import org.apache.commons.io.IOUtils;
58  import org.apache.commons.lang3.StringUtils;
59  import org.apache.commons.lang3.Strings;
60  import org.apache.jackrabbit.vault.packaging.Dependency;
61  import org.apache.jackrabbit.vault.packaging.DependencyUtil;
62  import org.apache.jackrabbit.vault.packaging.PackageType;
63  import org.apache.jackrabbit.vault.packaging.VersionRange;
64  import org.apache.maven.artifact.ArtifactUtils;
65  import org.apache.maven.plugin.logging.Log;
66  import org.apache.maven.plugin.logging.SystemStreamLog;
67  import org.jetbrains.annotations.NotNull;
68  import org.jetbrains.annotations.Nullable;
69  
70  import io.wcm.devops.conga.plugins.aem.maven.AutoDependenciesMode;
71  import io.wcm.devops.conga.plugins.aem.maven.BuildOutputTimestamp;
72  import io.wcm.devops.conga.plugins.aem.maven.PackageTypeValidation;
73  import io.wcm.devops.conga.plugins.aem.maven.PackageVersionMode;
74  import io.wcm.devops.conga.plugins.aem.maven.RunModeOptimization;
75  import io.wcm.devops.conga.plugins.aem.maven.model.BundleFile;
76  import io.wcm.devops.conga.plugins.aem.maven.model.ContentPackageFile;
77  import io.wcm.devops.conga.plugins.aem.maven.model.InstallableFile;
78  import io.wcm.tooling.commons.contentpackagebuilder.ContentPackage;
79  import io.wcm.tooling.commons.contentpackagebuilder.ContentPackageBuilder;
80  import io.wcm.tooling.commons.contentpackagebuilder.PackageFilter;
81  
82  /**
83   * Builds "all" package based on given set of content packages.
84   *
85   * <p>
86   * General concept:
87   * </p>
88   *
89   * <ul>
90   * <li>Iterates through all content packages that are generated or collected by CONGA and contained in the
91   * model.json</li>
92   * <li>Enforces the order defined in CONGA by automatically adding dependencies to all packages reflecting the file
93   * order in model.json</li>
94   * <li>Because the dependency chain may be different for each runmode (author/publish), each package is added once for
95   * each runmode. Internally this separate dependency change for author and publish is optimized to have each package
96   * included only once for author+publish, unless it has a different chain of dependencies for both runmodes, in which
97   * case it is included separately for each run mode.</li>
98   * <li>To avoid conflicts with duplicate packages with different dependency chains the names of packages that are
99   * included in different versions for author/publish are changed and a runmode suffix (.author or .publish) is added,
100  * and it is put in a corresponding install folder.</li>
101  * <li>To avoid problems with nested sub packages, the sub packages are extracted from the packages and treated in the
102  * same way as other packages.</li>
103  * </ul>
104  */
105 public final class AllPackageBuilder {
106 
107   private final File targetFile;
108   private final String groupName;
109   private final String packageName;
110   private String version;
111   private AutoDependenciesMode autoDependenciesMode = AutoDependenciesMode.OFF;
112   private RunModeOptimization runModeOptimization = RunModeOptimization.OFF;
113   private PackageTypeValidation packageTypeValidation = PackageTypeValidation.STRICT;
114   private PackageVersionMode packageVersionMode = PackageVersionMode.DEFAULT;
115   private Log log;
116   private BuildOutputTimestamp buildOutputTimestamp;
117 
118   private static final String RUNMODE_DEFAULT = "$default$";
119   private static final Set<String> ALLOWED_PACKAGE_TYPES = Set.of(
120       PackageType.APPLICATION.name().toLowerCase(),
121       PackageType.CONTAINER.name().toLowerCase(),
122       PackageType.CONTENT.name().toLowerCase());
123   private static final String VERSION_SUFFIX_SEPARATOR = "-";
124 
125   private final List<ContentPackageFileSet> contentPackageFileSets = new ArrayList<>();
126   private final List<BundleFileSet> bundleFileSets = new ArrayList<>();
127 
128   /**
129    * Constructor.
130    * @param targetFile Target file
131    * @param groupName Group name
132    * @param packageName Package name
133    */
134   public AllPackageBuilder(File targetFile, String groupName, String packageName) {
135     this.targetFile = targetFile;
136     this.groupName = groupName;
137     this.packageName = packageName;
138   }
139 
140   /**
141    * Automatically generate dependencies between content packages based on file order in CONGA configuration.
142    * @param value mode
143    * @return this
144    */
145   public AllPackageBuilder autoDependenciesMode(AutoDependenciesMode value) {
146     this.autoDependenciesMode = value;
147     return this;
148   }
149 
150   /**
151    * Configure run mode optimization.
152    * @param value mode
153    * @return this
154    */
155   public AllPackageBuilder runModeOptimization(RunModeOptimization value) {
156     this.runModeOptimization = value;
157     return this;
158   }
159 
160   /**
161    * How to validate package types to be included in "all" package.
162    * @param value validation
163    * @return this
164    */
165   public AllPackageBuilder packageTypeValidation(PackageTypeValidation value) {
166     this.packageTypeValidation = value;
167     return this;
168   }
169 
170   /**
171    * How to handle versions of packages and sub-packages inside "all" package.
172    * @param value mode
173    * @return this
174    */
175   public AllPackageBuilder packageVersionMode(PackageVersionMode value) {
176     this.packageVersionMode = value;
177     return this;
178   }
179 
180   /**
181    * Maven logger
182    * @param value Maven logger
183    * @return this
184    */
185   public AllPackageBuilder logger(Log value) {
186     this.log = value;
187     return this;
188   }
189 
190   /**
191    * Package version
192    * @param value Package version
193    * @return this
194    */
195   public AllPackageBuilder version(String value) {
196     this.version = value;
197     return this;
198   }
199 
200   /**
201    * Build output timestamp
202    * @param value timestamp
203    * @return this
204    */
205   public AllPackageBuilder buildOutputTimestamp(BuildOutputTimestamp value) {
206     this.buildOutputTimestamp = value;
207     return this;
208   }
209 
210   private Log getLog() {
211     if (this.log == null) {
212       this.log = new SystemStreamLog();
213     }
214     return this.log;
215   }
216 
217   /**
218    * Add content packages and OSGi bundles to be contained in "all" content package.
219    * @param files Content packages (invalid will be filtered out) and OSGi bundles
220    * @param cloudManagerTarget Target environments/run modes the packages should be attached to
221    * @throws IllegalArgumentException If and invalid package type is detected
222    */
223   public void add(List<InstallableFile> files, Set<String> cloudManagerTarget) {
224     List<ContentPackageFile> contentPackages = filterFiles(files, ContentPackageFile.class);
225 
226     // collect list of cloud manager environment run modes
227     List<String> environmentRunModes = new ArrayList<>();
228     if (cloudManagerTarget.isEmpty()) {
229       environmentRunModes.add(RUNMODE_DEFAULT);
230     }
231     else {
232       environmentRunModes.addAll(cloudManagerTarget);
233     }
234 
235     List<ContentPackageFile> validContentPackages;
236     switch (packageTypeValidation) {
237       case STRICT:
238         validContentPackages = getValidContentPackagesStrictValidation(contentPackages);
239         break;
240       case WARN:
241         validContentPackages = getValidContentPackagesWarnValidation(contentPackages);
242         break;
243       default:
244         throw new IllegalArgumentException("Unsupported package type validation: " + packageTypeValidation);
245     }
246 
247     if (!validContentPackages.isEmpty()) {
248       contentPackageFileSets.add(new ContentPackageFileSet(validContentPackages, environmentRunModes));
249     }
250 
251     // add OSGi bundles
252     List<BundleFile> bundles = filterFiles(files, BundleFile.class);
253     if (!bundles.isEmpty()) {
254       bundleFileSets.add(new BundleFileSet(bundles, environmentRunModes));
255     }
256   }
257 
258   /**
259    * Get valid content packages in strict mode: Ignore content packages without package type (with warning),
260    * fail build if Content package with "mixed" mode is found.
261    * @param contentPackages Content packages
262    * @return Valid content packages
263    */
264   private List<ContentPackageFile> getValidContentPackagesStrictValidation(List<? extends ContentPackageFile> contentPackages) {
265     // generate warning for each content packages without package type that is skipped
266     contentPackages.stream()
267         .filter(pkg -> !hasPackageType(pkg))
268         .forEach(pkg -> getLog().warn("Skipping content package without package type: " + getCanonicalPath(pkg.getFile())));
269 
270     // fail build if content packages with non-allowed package types exist
271     List<ContentPackageFile> invalidPackageTypeContentPackages = contentPackages.stream()
272         .filter(AllPackageBuilder::hasPackageType)
273         .filter(pkg -> !isValidPackageType(pkg))
274         .collect(Collectors.toList());
275     if (!invalidPackageTypeContentPackages.isEmpty()) {
276       throw new IllegalArgumentException("Content packages found with unsupported package types: " +
277           invalidPackageTypeContentPackages.stream()
278               .map(pkg -> pkg.getName() + " -> " + pkg.getPackageType())
279               .collect(Collectors.joining(", ")));
280     }
281 
282     // collect AEM content packages with package type
283     return contentPackages.stream()
284         .filter(AllPackageBuilder::hasPackageType)
285         .collect(Collectors.toList());
286   }
287 
288   /**
289    * Get all content packages, generate warnings if package type is missing or "mixed" mode package type is used.
290    * @param contentPackages Content packages
291    * @return Valid content packages
292    */
293   private List<ContentPackageFile> getValidContentPackagesWarnValidation(List<? extends ContentPackageFile> contentPackages) {
294     // generate warning for each content packages without package type
295     contentPackages.stream()
296         .filter(pkg -> !hasPackageType(pkg))
297         .forEach(pkg -> getLog().warn("Found content package without package type: " + getCanonicalPath(pkg.getFile())));
298 
299     // generate warning for each content packages with invalid package type
300     contentPackages.stream()
301         .filter(AllPackageBuilder::hasPackageType)
302         .filter(pkg -> !isValidPackageType(pkg))
303         .forEach(pkg -> getLog().warn("Found content package with invalid package type: "
304             + getCanonicalPath(pkg.getFile()) + " -> " + pkg.getPackageType()));
305 
306     // return all content packages
307     return contentPackages.stream().collect(Collectors.toList());
308   }
309 
310   private static <T> List<T> filterFiles(List<? extends InstallableFile> files, Class<T> fileClass) {
311     return files.stream()
312         .filter(fileClass::isInstance)
313         .map(fileClass::cast)
314         .toList();
315   }
316 
317   /**
318    * Build "all" content package.
319    * @param properties Specifies additional properties to be set in the properties.xml file.
320    * @return true if "all" package was generated, false if no valid package was found.
321    * @throws IOException I/O exception
322    */
323   public boolean build(Map<String, String> properties) throws IOException {
324 
325     if (contentPackageFileSets.isEmpty()) {
326       return false;
327     }
328 
329     // prepare content package metadata
330     ContentPackageBuilder builder = new ContentPackageBuilder()
331         .group(groupName)
332         .name(packageName)
333         .packageType("container");
334     if (version != null) {
335       builder.version(version);
336     }
337 
338     // define root path for "all" package
339     String rootPath = buildRootPath(groupName, packageName);
340     builder.filter(new PackageFilter(rootPath));
341 
342     // additional package properties
343     if (properties != null) {
344       properties.entrySet().forEach(entry -> builder.property(entry.getKey(), entry.getValue()));
345     }
346 
347     // build content package
348     try (ContentPackage contentPackage = builder.build(targetFile)) {
349       buildAddContentPackages(contentPackage, rootPath);
350       buildAddBundles(contentPackage, rootPath);
351     }
352 
353     return true;
354   }
355 
356   @SuppressWarnings("java:S3776") // ignore complexity
357   private void buildAddContentPackages(ContentPackage contentPackage, String rootPath) throws IOException {
358     // build set with dependencies instances for each package contained in all filesets
359     Set<Dependency> allPackagesFromFileSets = new HashSet<>();
360     for (ContentPackageFileSet fileSet : contentPackageFileSets) {
361       for (ContentPackageFile pkg : fileSet.getFiles()) {
362         addDependencyInformation(allPackagesFromFileSets, pkg);
363       }
364     }
365 
366     Collection<ContentPackageFileSet> processedFileSets;
367     if (runModeOptimization == RunModeOptimization.ELIMINATE_DUPLICATES) {
368       // eliminate duplicates which are same for author and publish
369       processedFileSets = eliminateAuthorPublishDuplicates(contentPackageFileSets,
370         environmentRunMode -> new ContentPackageFileSet(new ArrayList<>(), Collections.singletonList(environmentRunMode)));
371     }
372     else {
373       processedFileSets = contentPackageFileSets;
374     }
375 
376     for (ContentPackageFileSet fileSet : processedFileSets) {
377       for (String environmentRunMode : fileSet.getEnvironmentRunModes()) {
378         List<ContentPackageFile> previousPackages = new ArrayList<>();
379         for (ContentPackageFile pkg : fileSet.getFiles()) {
380           ContentPackageFile previousPkg = getDependencyChainPreviousPackage(pkg, previousPackages);
381 
382           // set package name, wire previous package in package dependency
383           List<TemporaryContentPackageFile> processedFiles = processContentPackage(pkg, previousPkg, environmentRunMode, allPackagesFromFileSets);
384 
385           // add processed content packages to "all" content package - and delete the temporary files
386           try {
387             for (TemporaryContentPackageFile processedFile : processedFiles) {
388               String path = buildPackagePath(processedFile, rootPath, environmentRunMode);
389               contentPackage.addFile(path, processedFile.getFile());
390               if (log.isDebugEnabled()) {
391                 log.debug("  Add " + processedFile.getPackageInfoWithDependencies());
392               }
393             }
394           }
395           finally {
396             processedFiles.stream()
397                 .map(TemporaryContentPackageFile::getFile)
398                 .forEach(FileUtils::deleteQuietly);
399           }
400 
401           previousPackages.add(pkg);
402         }
403       }
404     }
405   }
406 
407   /**
408    * Gets the previous package in the order defined by CONGA to define as package dependency in current package.
409    * @param currentPackage Current package
410    * @param previousPackages List of previous packages
411    * @return Package to define as dependency, or null if no dependency should be defined
412    */
413   private @Nullable ContentPackageFile getDependencyChainPreviousPackage(@NotNull ContentPackageFile currentPackage,
414       @NotNull List<ContentPackageFile> previousPackages) {
415     if ((autoDependenciesMode == AutoDependenciesMode.OFF)
416         || (autoDependenciesMode == AutoDependenciesMode.IMMUTABLE_ONLY && isMutable(currentPackage))) {
417       return null;
418     }
419     // get last previous package
420     return previousPackages.stream()
421         // if not IMMUTABLE_MUTABLE_COMBINED active only that of the same mutability type
422         .filter(item -> (autoDependenciesMode == AutoDependenciesMode.IMMUTABLE_MUTABLE_COMBINED) || mutableMatches(item, currentPackage))
423         // make sure author-only or publish-only packages are only taken into account if the current package has same restriction
424         .filter(item -> isAuthorAndPublish(item)
425             || (isOnlyAuthor(item) && isOnlyAuthor(currentPackage))
426             || (isOnlyPublish(item) && isOnlyPublish(currentPackage)))
427         // ignore packages that are marked as dependency chain ignore
428         .filter(item -> !item.isDependencyChainIgnore())
429         // get last in list
430         .reduce((first, second) -> second).orElse(null);
431   }
432 
433   private void buildAddBundles(ContentPackage contentPackage, String rootPath) throws IOException {
434     Collection<BundleFileSet> processedFileSets;
435     if (runModeOptimization == RunModeOptimization.ELIMINATE_DUPLICATES) {
436       // eliminate duplicates which are same for author and publish
437       processedFileSets = eliminateAuthorPublishDuplicates(bundleFileSets,
438           environmentRunMode -> new BundleFileSet(new ArrayList<>(), Collections.singletonList(environmentRunMode)));
439     }
440     else {
441       processedFileSets = bundleFileSets;
442     }
443 
444     for (BundleFileSet bundleFileSet : processedFileSets) {
445       for (String environmentRunMode : bundleFileSet.getEnvironmentRunModes()) {
446         for (BundleFile bundleFile : bundleFileSet.getFiles()) {
447           String path = buildBundlePath(bundleFile, rootPath, environmentRunMode);
448           contentPackage.addFile(path, bundleFile.getFile());
449         }
450       }
451     }
452   }
453 
454   private static boolean hasPackageType(ContentPackageFile pkg) {
455     // accept only content packages with a package type set
456     return pkg.getPackageType() != null;
457   }
458 
459   private static boolean isValidPackageType(ContentPackageFile pkg) {
460     // check if the package type is an allowed package type
461     return ALLOWED_PACKAGE_TYPES.contains(pkg.getPackageType());
462   }
463 
464   private static boolean isMutable(ContentPackageFile pkg) {
465     return Strings.CS.equals("content", pkg.getPackageType());
466   }
467 
468   private static boolean mutableMatches(ContentPackageFile pkg1, ContentPackageFile pkg2) {
469     if (pkg1 == null || pkg2 == null) {
470       return false;
471     }
472     return isMutable(pkg1) == isMutable(pkg2);
473   }
474 
475   /**
476    * Build root path to be used for embedded package.
477    * @param groupName Group name
478    * @param packageName Package name
479    * @return Package path
480    */
481   private static String buildRootPath(String groupName, String packageName) {
482     return "/apps/" + groupName + "-" + packageName + "-packages";
483   }
484 
485   /**
486    * Generate suffix for instance and environment run modes.
487    * @param file File
488    * @return Suffix string
489    */
490   private static String buildRunModeSuffix(InstallableFile file, String environmentRunMode) {
491     StringBuilder runModeSuffix = new StringBuilder();
492     if (isOnlyAuthor(file)) {
493       runModeSuffix.append(".").append(RUNMODE_AUTHOR);
494     }
495     else if (isOnlyPublish(file)) {
496       runModeSuffix.append(".").append(RUNMODE_PUBLISH);
497     }
498     if (!Strings.CS.equals(environmentRunMode, RUNMODE_DEFAULT)) {
499       runModeSuffix.append(".").append(environmentRunMode);
500     }
501     return runModeSuffix.toString();
502   }
503 
504   /**
505    * Generate suffix for versions of content packages.
506    * @param pkg Content package
507    * @param ignoreSnapshot Do not build version suffix for SNAPSHOT versions
508    * @return Suffix string
509    */
510   private String buildVersionSuffix(ContentPackageFile pkg, boolean ignoreSnapshot) {
511     StringBuilder versionSuffix = new StringBuilder();
512 
513     if (this.packageVersionMode == PackageVersionMode.RELEASE_SUFFIX_VERSION
514         && (!ArtifactUtils.isSnapshot(pkg.getVersion()) || !ignoreSnapshot)
515         && !Strings.CS.equals(pkg.getVersion(), this.version)
516         && this.version != null) {
517       versionSuffix.append(VERSION_SUFFIX_SEPARATOR)
518           // replace dots with underlines in version suffix to avoid confusion with main version number
519           .append(Strings.CS.replace(this.version, ".", "_"));
520     }
521 
522     return versionSuffix.toString();
523   }
524 
525   /**
526    * Build path to be used for embedded package.
527    * @param pkg Package
528    * @param rootPath Root path
529    * @return Package path
530    */
531   @SuppressWarnings("java:S1075") // no filesystem path
532   private String buildPackagePath(ContentPackageFile pkg, String rootPath, String environmentRunMode) {
533     if (packageTypeValidation == PackageTypeValidation.STRICT && !isValidPackageType(pkg)) {
534       throw new IllegalArgumentException("Package " + pkg.getPackageInfo() + " has invalid package type: '" + pkg.getPackageType() + "'.");
535     }
536 
537     String runModeSuffix = buildRunModeSuffix(pkg, environmentRunMode);
538 
539     // add run mode suffix to both install folder path and package file name
540     String path = rootPath + "/" + Objects.toString(pkg.getPackageType(), "misc") + "/install" + runModeSuffix;
541 
542     String versionSuffix = "";
543     String packageVersion = pkg.getVersion();
544     String packageVersionWithoutSuffix = packageVersion;
545     if (this.packageVersionMode == PackageVersionMode.RELEASE_SUFFIX_VERSION && this.version != null) {
546       packageVersionWithoutSuffix = Strings.CS.removeEnd(packageVersion, buildVersionSuffix(pkg, false));
547     }
548     if (packageVersion != null && pkg.getFile().getName().contains(packageVersionWithoutSuffix)) {
549       versionSuffix = "-" + packageVersion;
550     }
551     String fileName = pkg.getName() + versionSuffix
552         + "." + FilenameUtils.getExtension(pkg.getFile().getName());
553     return path + "/" + fileName;
554   }
555 
556   /**
557    * Build path to be used for embedded bundle.
558    * @param bundleFile Bundle
559    * @param rootPath Root path
560    * @return Package path
561    */
562   private static String buildBundlePath(BundleFile bundleFile, String rootPath, String environmentRunMode) {
563     String runModeSuffix = buildRunModeSuffix(bundleFile, environmentRunMode);
564 
565     // add run mode suffix to both install folder path and package file name
566     String path = rootPath + "/application/install" + runModeSuffix;
567 
568     return path + "/" + bundleFile.getFile().getName();
569   }
570 
571   /**
572    * Rewrite content package ZIP file while adding to "all" package:
573    * Add dependency to previous package in CONGA configuration file oder.
574    * @param pkg Package to process (can be parent packe of the actual file)
575    * @param previousPkg Previous package to get dependency information from.
576    *          Is null if no previous package exists or auto dependency mode is switched off.
577    * @param environmentRunMode Environment run mode
578    * @param allPackagesFromFileSets Set with all packages from all file sets as dependency instances
579    * @return Returns a list of content package *temporary* files - have to be deleted when processing is completed.
580    * @throws IOException I/O error
581    */
582   @SuppressWarnings("java:S3776") // ignore complexity
583   private List<TemporaryContentPackageFile> processContentPackage(ContentPackageFile pkg,
584       ContentPackageFile previousPkg, String environmentRunMode,
585       Set<Dependency> allPackagesFromFileSets) throws IOException {
586 
587     List<TemporaryContentPackageFile> result = new ArrayList<>();
588     List<TemporaryContentPackageFile> subPackages = new ArrayList<>();
589 
590     // create temp zip file to create rewritten copy of package
591     File tempFile = File.createTempFile(FilenameUtils.getBaseName(pkg.getFile().getName()), ".zip");
592 
593     // open original content package
594     try (ZipFile zipFileIn = new ZipFile.Builder().setFile(pkg.getFile()).get()) {
595 
596       // iterate through entries and write them to the temp. zip file
597       try (FileOutputStream fos = new FileOutputStream(tempFile);
598           ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(fos)) {
599         Enumeration<? extends ZipArchiveEntry> zipInEntries = zipFileIn.getEntries();
600         while (zipInEntries.hasMoreElements()) {
601           ZipArchiveEntry zipInEntry = zipInEntries.nextElement();
602           if (!zipInEntry.isDirectory()) {
603             try (InputStream is = zipFileIn.getInputStream(zipInEntry)) {
604               boolean processedEntry = false;
605 
606               // if entry is properties.xml, update dependency information
607               if (Strings.CS.equals(zipInEntry.getName(), "META-INF/vault/properties.xml")) {
608                 FileVaultProperties fileVaultProps = new FileVaultProperties(is);
609                 Properties props = fileVaultProps.getProperties();
610                 addSuffixToPackageName(props, pkg, environmentRunMode);
611                 addSuffixToVersion(props, pkg);
612 
613                 // update package dependencies
614                 ContentPackageFile dependencyFile = previousPkg;
615                 if (autoDependenciesMode == AutoDependenciesMode.OFF || pkg.isDependencyChainIgnore()) {
616                   dependencyFile = null;
617                 }
618                 updateDependencies(pkg, props, dependencyFile, environmentRunMode, allPackagesFromFileSets);
619 
620                 // if package type is missing in package properties, put in the type defined in model
621                 String packageType = pkg.getPackageType();
622                 if (props.get(NAME_PACKAGE_TYPE) == null && packageType != null) {
623                   props.put(NAME_PACKAGE_TYPE, packageType);
624                 }
625 
626                 ZipArchiveEntry zipOutEntry = newZipEntry(zipInEntry);
627                 zipOut.putArchiveEntry(zipOutEntry);
628                 fileVaultProps.storeToXml(zipOut);
629                 zipOut.closeArchiveEntry();
630                 processedEntry = true;
631               }
632 
633               // process sub-packages as well: add runmode suffix and update dependencies
634               else if (Strings.CS.equals(FilenameUtils.getExtension(zipInEntry.getName()), "zip")) {
635                 File tempSubPackageFile = File.createTempFile(FilenameUtils.getBaseName(zipInEntry.getName()), ".zip");
636                 try (FileOutputStream subPackageFos = new FileOutputStream(tempSubPackageFile)) {
637                   IOUtils.copy(is, subPackageFos);
638                 }
639 
640                 // check if contained ZIP file is really a content package
641                 // then process it as well, remove if from the content package is was contained in
642                 // and add it as "1st level package" to the all package
643                 TemporaryContentPackageFile tempSubPackage = new TemporaryContentPackageFile(tempSubPackageFile, pkg.getVariants());
644                 if (packageTypeValidation == PackageTypeValidation.STRICT && !isValidPackageType(tempSubPackage)) {
645                   throw new IllegalArgumentException("Package " + pkg.getPackageInfo() + " contains sub package " + tempSubPackage.getPackageInfo()
646                       + " with invalid package type: '" + StringUtils.defaultString(tempSubPackage.getPackageType()) + "'");
647                 }
648                 if (StringUtils.isNoneBlank(tempSubPackage.getGroup(), tempSubPackage.getName())) {
649                   subPackages.add(tempSubPackage);
650                   processedEntry = true;
651                 }
652                 else {
653                   FileUtils.deleteQuietly(tempSubPackageFile);
654                 }
655               }
656 
657               // otherwise transfer the binary data 1:1
658               if (!processedEntry) {
659                 ZipArchiveEntry zipOutEntry = newZipEntry(zipInEntry);
660                 zipOut.putArchiveEntry(zipOutEntry);
661                 IOUtils.copy(is, zipOut);
662                 zipOut.closeArchiveEntry();
663               }
664             }
665 
666           }
667         }
668       }
669 
670       // add sub package metadata to set with dependency information
671       for (TemporaryContentPackageFile tempSubPackage : subPackages) {
672         addDependencyInformation(allPackagesFromFileSets, tempSubPackage);
673       }
674 
675       // process sub packages and add to result
676       for (TemporaryContentPackageFile tempSubPackage : subPackages) {
677         result.addAll(processContentPackage(tempSubPackage, previousPkg, environmentRunMode, allPackagesFromFileSets));
678       }
679 
680       result.add(new TemporaryContentPackageFile(tempFile, pkg.getVariants()));
681     }
682     return result;
683   }
684 
685   private ZipArchiveEntry newZipEntry(ZipArchiveEntry in) {
686     ZipArchiveEntry out = new ZipArchiveEntry(in.getName());
687     if (buildOutputTimestamp != null && buildOutputTimestamp.isValid()) {
688       out.setLastModifiedTime(buildOutputTimestamp.toFileTime());
689     }
690     else if (in.getLastModifiedTime() != null) {
691       out.setLastModifiedTime(in.getLastModifiedTime());
692     }
693     return out;
694   }
695 
696   /**
697    * Add dependency information to dependencies string in properties (if it does not exist already).
698    * @param pkg Current content package
699    * @param props Properties
700    * @param dependencyFile Dependency package
701    * @param allPackagesFromFileSets Set with all packages from all file sets as dependency instances
702    */
703   private void updateDependencies(ContentPackageFile pkg, Properties props, ContentPackageFile dependencyFile,
704       String environmentRunMode, Set<Dependency> allPackagesFromFileSets) {
705     String[] existingDepsStrings = StringUtils.split(props.getProperty(NAME_DEPENDENCIES), ",");
706     Dependency[] existingDeps = null;
707     if (existingDepsStrings != null && existingDepsStrings.length > 0) {
708       existingDeps = Dependency.fromString(existingDepsStrings);
709     }
710     if (existingDeps != null) {
711       existingDeps = autoDependenciesMode == AutoDependenciesMode.OFF
712           ? rewriteReferencesToManagedPackages(pkg, environmentRunMode, allPackagesFromFileSets, existingDeps)
713           : removeReferencesToManagedPackages(existingDeps, allPackagesFromFileSets);
714     }
715 
716     Dependency[] deps;
717     if (dependencyFile != null) {
718       Dependency newDependency = createDependencyFromContentPackageFile(dependencyFile, environmentRunMode);
719       deps = addDependency(existingDeps, newDependency);
720     }
721     else {
722       deps = existingDeps;
723     }
724 
725     if (deps != null) {
726       String dependenciesString = Dependency.toString(deps);
727       props.put(NAME_DEPENDENCIES, dependenciesString);
728     }
729   }
730 
731   private @NotNull Dependency createDependencyFromContentPackageFile(@NotNull ContentPackageFile dependencyFile,
732       @NotNull String environmentRunMode) {
733     String runModeSuffix = buildRunModeSuffix(dependencyFile, environmentRunMode);
734     String dependencyVersion = dependencyFile.getVersion() + buildVersionSuffix(dependencyFile, true);
735     return new Dependency(dependencyFile.getGroup(),
736         dependencyFile.getName() + runModeSuffix,
737         VersionRange.fromString(dependencyVersion));
738   }
739 
740   private static Dependency[] addDependency(Dependency[] existingDeps, Dependency newDependency) {
741     if (existingDeps != null) {
742       return DependencyUtil.add(existingDeps, newDependency);
743     }
744     else {
745       return new Dependency[] { newDependency };
746     }
747   }
748 
749   private static void addSuffixToPackageName(Properties props, ContentPackageFile pkg, String environmentRunMode) {
750     String runModeSuffix = buildRunModeSuffix(pkg, environmentRunMode);
751     String packageName = props.getProperty(NAME_NAME) + runModeSuffix;
752     props.put(NAME_NAME, packageName);
753   }
754 
755   private void addSuffixToVersion(Properties props, ContentPackageFile pkg) {
756     // package version
757     if (StringUtils.isEmpty(pkg.getVersion())) {
758       return;
759     }
760     String suffixedVersion = pkg.getVersion() + buildVersionSuffix(pkg, true);
761     props.put(NAME_VERSION, suffixedVersion);
762   }
763 
764   private @NotNull Dependency[] rewriteReferencesToManagedPackages(@NotNull ContentPackageFile pkg,
765       @NotNull String environmentRunMode, @NotNull Set<Dependency> allPackagesFromFileSets, @NotNull Dependency[] deps) {
766     return Arrays.stream(deps)
767         .map(dep -> rewriteReferenceIfDependencyIsManagedPackage(pkg, environmentRunMode, allPackagesFromFileSets, dep))
768         .toArray(Dependency[]::new);
769   }
770 
771   private @NotNull Dependency rewriteReferenceIfDependencyIsManagedPackage(@NotNull ContentPackageFile pkg,
772       @NotNull String environmentRunMode, @NotNull Set<Dependency> allPackagesFromFileSets, @NotNull Dependency dep) {
773     // not a managed package, return as is
774     if (!allPackagesFromFileSets.contains(dep)) {
775       return dep;
776     }
777     return findContentPackageFileForDependency(pkg, dep)
778         // found a content package file for the dependency, rewrite the dependency
779         .map(contentPackageFile -> createDependencyFromContentPackageFile(contentPackageFile, environmentRunMode))
780         // found no content package file for the dependency, use current run mode suffix
781         .orElseGet(() -> createDependencyWithCurrentPackageRunModeSuffix(pkg, environmentRunMode, dep));
782   }
783 
784   private @NotNull Optional<ContentPackageFile> findContentPackageFileForDependency(@NotNull ContentPackageFile pkg,
785       @NotNull Dependency dep) {
786     // look for content package in all file sets
787     return contentPackageFileSets.stream()
788             // prefer file set which contains the current package to use current run mode
789             .sorted((fileSet1, fileSet2) -> sortFileSetsContainingPackageFirst(pkg, fileSet1, fileSet2))
790             .flatMap(fileSet -> fileSet.getFiles().stream())
791             .filter(contentPackageFile -> isContentPackageForDependency(contentPackageFile, dep))
792             .findFirst();
793   }
794 
795   private int sortFileSetsContainingPackageFirst(@NotNull ContentPackageFile pkg,
796       @NotNull ContentPackageFileSet fileSet1, @NotNull ContentPackageFileSet fileSet2) {
797     int fileSet1ContainsPackage = fileSet1.getFiles().contains(pkg) ? 1 : 0;
798     int fileSet2ContainsPackage = fileSet2.getFiles().contains(pkg) ? 1 : 0;
799     return fileSet2ContainsPackage - fileSet1ContainsPackage;
800   }
801 
802   private boolean isContentPackageForDependency(@NotNull ContentPackageFile contentPackageFile, @NotNull Dependency dep) {
803     return contentPackageFile.getGroup().equals(dep.getGroup())
804             && contentPackageFile.getName().equals(dep.getName());
805   }
806 
807   private @NotNull Dependency createDependencyWithCurrentPackageRunModeSuffix(@NotNull ContentPackageFile pkg,
808       @NotNull String environmentRunMode, @NotNull Dependency dep) {
809     String runModeSuffix = buildRunModeSuffix(pkg, environmentRunMode);
810     return new Dependency(dep.getGroup(), dep.getName() + runModeSuffix, dep.getRange());
811   }
812 
813   /**
814    * Removes existing references to packages contained in the list of packages to manage by this builder because
815    * they are added new (and probably with a different package name) during processing.
816    * @param deps Dependencies list
817    * @param allPackagesFromFileSets Set with all packages from all file sets as dependency instances
818    * @return Dependencies list
819    */
820   private static Dependency[] removeReferencesToManagedPackages(Dependency[] deps, Set<Dependency> allPackagesFromFileSets) {
821     return Arrays.stream(deps)
822         .filter(dep -> !allPackagesFromFileSets.contains(dep))
823         .toArray(size -> new Dependency[size]);
824   }
825 
826   private static void addDependencyInformation(Set<Dependency> allPackagesFromFileSets, ContentPackageFile pkg) {
827     allPackagesFromFileSets.add(new Dependency(pkg.getGroup(), pkg.getName(), VersionRange.fromString(pkg.getVersion())));
828   }
829 
830   /**
831    * @return Group name
832    */
833   public String getGroupName() {
834     return this.groupName;
835   }
836 
837   /**
838    * @return Package name
839    */
840   public String getPackageName() {
841     return this.packageName;
842   }
843 
844   /**
845    * @return Target file
846    */
847   public File getTargetFile() {
848     return this.targetFile;
849   }
850 
851 }