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