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