View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.ear;
20  
21  import javax.inject.Inject;
22  
23  import java.io.BufferedWriter;
24  import java.io.File;
25  import java.io.IOException;
26  import java.io.InputStream;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.FileSystem;
29  import java.nio.file.FileSystems;
30  import java.nio.file.FileVisitResult;
31  import java.nio.file.Files;
32  import java.nio.file.LinkOption;
33  import java.nio.file.Path;
34  import java.nio.file.Paths;
35  import java.nio.file.ProviderMismatchException;
36  import java.nio.file.SimpleFileVisitor;
37  import java.nio.file.StandardCopyOption;
38  import java.nio.file.StandardOpenOption;
39  import java.nio.file.attribute.BasicFileAttributes;
40  import java.nio.file.attribute.FileTime;
41  import java.util.ArrayList;
42  import java.util.Arrays;
43  import java.util.Collection;
44  import java.util.List;
45  import java.util.Objects;
46  
47  import org.apache.maven.archiver.MavenArchiveConfiguration;
48  import org.apache.maven.archiver.MavenArchiver;
49  import org.apache.maven.artifact.Artifact;
50  import org.apache.maven.artifact.DependencyResolutionRequiredException;
51  import org.apache.maven.execution.MavenSession;
52  import org.apache.maven.plugin.MojoExecutionException;
53  import org.apache.maven.plugin.MojoFailureException;
54  import org.apache.maven.plugins.annotations.LifecyclePhase;
55  import org.apache.maven.plugins.annotations.Mojo;
56  import org.apache.maven.plugins.annotations.Parameter;
57  import org.apache.maven.plugins.annotations.ResolutionScope;
58  import org.apache.maven.plugins.ear.util.EarMavenArchiver;
59  import org.apache.maven.plugins.ear.util.JavaEEVersion;
60  import org.apache.maven.project.MavenProjectHelper;
61  import org.apache.maven.shared.filtering.FilterWrapper;
62  import org.apache.maven.shared.filtering.MavenFileFilter;
63  import org.apache.maven.shared.filtering.MavenFilteringException;
64  import org.apache.maven.shared.filtering.MavenResourcesExecution;
65  import org.apache.maven.shared.filtering.MavenResourcesFiltering;
66  import org.apache.maven.shared.mapping.MappingUtils;
67  import org.apache.maven.shared.utils.io.FileUtils;
68  import org.codehaus.plexus.archiver.ArchiverException;
69  import org.codehaus.plexus.archiver.UnArchiver;
70  import org.codehaus.plexus.archiver.ear.EarArchiver;
71  import org.codehaus.plexus.archiver.jar.JarArchiver;
72  import org.codehaus.plexus.archiver.jar.Manifest;
73  import org.codehaus.plexus.archiver.jar.Manifest.Attribute;
74  import org.codehaus.plexus.archiver.jar.ManifestException;
75  import org.codehaus.plexus.archiver.manager.ArchiverManager;
76  import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
77  import org.codehaus.plexus.components.io.filemappers.FileMapper;
78  import org.codehaus.plexus.interpolation.InterpolationException;
79  import org.codehaus.plexus.util.DirectoryScanner;
80  import org.codehaus.plexus.util.StringUtils;
81  
82  /**
83   * Builds J2EE Enterprise Archive (EAR) files.
84   *
85   * @author <a href="snicoll@apache.org">Stephane Nicoll</a>
86   */
87  @Mojo(
88          name = "ear",
89          defaultPhase = LifecyclePhase.PACKAGE,
90          threadSafe = true,
91          requiresDependencyResolution = ResolutionScope.TEST)
92  public class EarMojo extends AbstractEarMojo {
93      /**
94       * Default file name mapping used by artifacts located in local repository.
95       */
96      private static final String ARTIFACT_DEFAULT_FILE_NAME_MAPPING =
97              "@{artifactId}@-@{version}@@{dashClassifier?}@.@{extension}@";
98  
99      /**
100      * Single directory for extra files to include in the EAR.
101      */
102     @Parameter(defaultValue = "${basedir}/src/main/application", required = true)
103     private File earSourceDirectory;
104 
105     /**
106      * The comma separated list of tokens to include in the EAR.
107      */
108     @Parameter(alias = "includes", defaultValue = "**")
109     private String earSourceIncludes;
110 
111     /**
112      * The comma separated list of tokens to exclude from the EAR.
113      */
114     @Parameter(alias = "excludes")
115     private String earSourceExcludes;
116 
117     /**
118      * Specify that the EAR sources should be filtered.
119      *
120      * @since 2.3.2
121      */
122     @Parameter(defaultValue = "false")
123     private boolean filtering;
124 
125     /**
126      * Filters (property files) to include during the interpolation of the pom.xml.
127      *
128      * @since 2.3.2
129      */
130     @Parameter
131     private List<String> filters;
132 
133     /**
134      * A list of file extensions that should not be filtered if filtering is enabled.
135      *
136      * @since 2.3.2
137      */
138     @Parameter
139     private List<String> nonFilteredFileExtensions;
140 
141     /**
142      * To escape interpolated value with Windows path c:\foo\bar will be replaced with c:\\foo\\bar.
143      *
144      * @since 2.3.2
145      */
146     @Parameter(defaultValue = "false")
147     private boolean escapedBackslashesInFilePath;
148 
149     /**
150      * Expression preceded with this String won't be interpolated \${foo} will be replaced with ${foo}.
151      *
152      * @since 2.3.2
153      */
154     @Parameter
155     protected String escapeString;
156 
157     /**
158      * In case of using the {@link #skinnyWars} and {@link #defaultLibBundleDir} usually the classpath will be modified.
159      * By settings this option {@code true} you can change this and keep the classpath untouched. This option has been
160      * introduced to keep the backward compatibility with earlier versions of the plugin.
161      *
162      * @since 2.10
163      */
164     @Parameter(defaultValue = "false")
165     private boolean skipClassPathModification;
166 
167     /**
168      * The location of a custom application.xml file to be used within the EAR file.
169      */
170     @Parameter
171     private File applicationXml;
172 
173     /**
174      * The directory for the generated EAR.
175      */
176     @Parameter(defaultValue = "${project.build.directory}", required = true)
177     private String outputDirectory;
178 
179     /**
180      * The name of the EAR file to generate.
181      */
182     @Parameter(defaultValue = "${project.build.finalName}", required = true, readonly = true)
183     private String finalName;
184 
185     /**
186      * The comma separated list of artifact's type(s) to unpack by default.
187      */
188     @Parameter
189     private String unpackTypes;
190 
191     /**
192      * Classifier to add to the artifact generated. If given, the artifact will be an attachment instead.
193      */
194     @Parameter
195     private String classifier;
196 
197     /**
198      * A comma separated list of tokens to exclude when packaging the EAR. By default nothing is excluded. Note that you
199      * can use the Java Regular Expressions engine to include and exclude specific pattern using the expression
200      * %regex[]. Hint: read the about (?!Pattern).
201      *
202      * @since 2.7
203      */
204     @Parameter
205     private String packagingExcludes;
206 
207     /**
208      * A comma separated list of tokens to include when packaging the EAR. By default everything is included. Note that
209      * you can use the Java Regular Expressions engine to include and exclude specific pattern using the expression
210      * %regex[].
211      *
212      * @since 2.7
213      */
214     @Parameter
215     private String packagingIncludes;
216 
217     /**
218      * Whether to create skinny WARs or not. A skinny WAR is a WAR that does not have all of its dependencies in
219      * WEB-INF/lib. Instead those dependencies are shared between the WARs through the EAR.
220      *
221      * @since 2.7
222      */
223     @Parameter(defaultValue = "false")
224     private boolean skinnyWars;
225 
226     /**
227      * Whether to create skinny EAR modules or not. A skinny EAR module is a WAR, SAR, HAR, RAR or WSR module that
228      * does not contain all of its dependencies in it. Instead those dependencies are shared between the WARs, SARs,
229      * HARs, RARs and WSRs through the EAR. This option takes precedence over {@link #skinnyWars} option. That is if
230      * skinnyModules is {@code true} but {@link #skinnyWars} is {@code false} (explicitly or by default) then all
231      * modules including WARs are skinny.
232      *
233      * @since 3.2.0
234      */
235     @Parameter(defaultValue = "false")
236     private boolean skinnyModules;
237 
238     /**
239      * The Plexus EAR archiver to create the output archive.
240      */
241     private EarArchiver earArchiver;
242 
243     /**
244      * The Plexus JAR archiver to create the output archive if no EAR application descriptor is provided (JavaEE 5+).
245      */
246     private JarArchiver jarArchiver;
247 
248     /**
249      * The archive configuration to use. See <a href="https://maven.apache.org/shared/maven-archiver/">Maven Archiver
250      * Reference</a>.
251      */
252     @Parameter
253     private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();
254 
255     /**
256      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
257      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
258      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
259      *
260      * @since 3.1.0
261      */
262     @Parameter(defaultValue = "${project.build.outputTimestamp}")
263     private String outputTimestamp;
264 
265     private MavenProjectHelper projectHelper;
266 
267     /**
268      * The archive manager.
269      */
270     private ArchiverManager archiverManager;
271 
272     private MavenFileFilter mavenFileFilter;
273 
274     private MavenResourcesFiltering mavenResourcesFiltering;
275 
276     /**
277      * @since 2.3.2
278      */
279     @Parameter(defaultValue = "${session}", readonly = true, required = true)
280     private MavenSession session;
281 
282     private List<FilterWrapper> filterWrappers;
283 
284     @Inject
285     public EarMojo(
286             EarArchiver earArchiver,
287             JarArchiver jarArchiver,
288             MavenProjectHelper projectHelper,
289             ArchiverManager archiverManager,
290             MavenFileFilter mavenFileFilter,
291             MavenResourcesFiltering mavenResourcesFiltering) {
292         this.earArchiver = earArchiver;
293         this.jarArchiver = jarArchiver;
294         this.projectHelper = projectHelper;
295         this.archiverManager = archiverManager;
296         this.mavenFileFilter = mavenFileFilter;
297         this.mavenResourcesFiltering = mavenResourcesFiltering;
298     }
299 
300     /** {@inheritDoc} */
301     @Override
302     public void execute() throws MojoExecutionException, MojoFailureException {
303         // Initializes ear modules
304         super.execute();
305 
306         File earFile = getEarFile(outputDirectory, finalName, classifier);
307         MavenArchiver archiver = new EarMavenArchiver(getModules());
308         File ddFile = new File(getWorkDirectory(), APPLICATION_XML_URI);
309 
310         JarArchiver theArchiver;
311         if (ddFile.exists()) {
312             earArchiver.setAppxml(ddFile);
313             theArchiver = earArchiver;
314         } else {
315             // current Plexus EarArchiver does not support application.xml-less JavaEE 5+ case
316             // => fallback to Plexus Jar archiver
317             theArchiver = jarArchiver;
318         }
319         getLog().debug("Ear archiver implementation [" + theArchiver.getClass().getName() + "]");
320         archiver.setArchiver(theArchiver);
321         archiver.setOutputFile(earFile);
322         archiver.setCreatedBy("Maven EAR Plugin", "org.apache.maven.plugins", "maven-ear-plugin");
323 
324         // configure for Reproducible Builds based on outputTimestamp value
325         archiver.configureReproducibleBuild(outputTimestamp);
326 
327         final JavaEEVersion javaEEVersion = JavaEEVersion.getJavaEEVersion(version);
328 
329         final Collection<String> outdatedResources = initOutdatedResources();
330 
331         // Initializes unpack types
332         List<String> unpackTypesList = createUnpackList();
333 
334         // Copy modules
335         copyModules(javaEEVersion, unpackTypesList, outdatedResources);
336 
337         // Copy source files
338         try {
339             File earSourceDir = earSourceDirectory;
340 
341             if (earSourceDir.exists()) {
342                 getLog().info("Copy ear sources to " + getWorkDirectory().getAbsolutePath());
343                 String[] fileNames = getEarFiles(earSourceDir);
344                 for (String fileName : fileNames) {
345                     copyFile(new File(earSourceDir, fileName), new File(getWorkDirectory(), fileName));
346                     outdatedResources.remove(Paths.get(fileName).toString());
347                 }
348             }
349 
350             if (applicationXml != null) {
351                 // rename to application.xml
352                 getLog().info("Including custom application.xml[" + applicationXml + "]");
353                 File metaInfDir = new File(getWorkDirectory(), META_INF);
354                 copyFile(applicationXml, new File(metaInfDir, "/application.xml"));
355                 outdatedResources.remove(Paths.get("META-INF/application.xml").toString());
356             }
357         } catch (IOException e) {
358             throw new MojoExecutionException("Error copying EAR sources", e);
359         } catch (MavenFilteringException e) {
360             throw new MojoExecutionException("Error filtering EAR sources", e);
361         }
362 
363         // Check if deployment descriptor is there
364         if (!ddFile.exists() && (javaEEVersion.lt(JavaEEVersion.FIVE))) {
365             throw new MojoExecutionException("Deployment descriptor: " + ddFile.getAbsolutePath() + " does not exist.");
366         }
367         // no need to check timestamp for descriptors: removing if outdated does not really make sense
368         outdatedResources.remove(Paths.get(APPLICATION_XML_URI).toString());
369         if (getJbossConfiguration() != null) {
370             outdatedResources.remove(Paths.get("META-INF/jboss-app.xml").toString());
371         }
372 
373         deleteOutdatedResources(outdatedResources);
374 
375         try {
376             getLog().debug("Excluding " + Arrays.asList(getPackagingExcludes()) + " from the generated EAR.");
377             getLog().debug("Including " + Arrays.asList(getPackagingIncludes()) + " in the generated EAR.");
378 
379             archiver.getArchiver().addDirectory(getWorkDirectory(), getPackagingIncludes(), getPackagingExcludes());
380 
381             archiver.createArchive(session, getProject(), archive);
382         } catch (ManifestException | IOException | DependencyResolutionRequiredException e) {
383             throw new MojoExecutionException("Error assembling EAR", e);
384         }
385 
386         if (classifier != null) {
387             projectHelper.attachArtifact(getProject(), "ear", classifier, earFile);
388         } else {
389             getProject().getArtifact().setFile(earFile);
390         }
391     }
392 
393     private void copyModules(
394             final JavaEEVersion javaEEVersion, List<String> unpackTypesList, Collection<String> outdatedResources)
395             throws MojoExecutionException, MojoFailureException {
396         try {
397             for (EarModule module : getModules()) {
398                 final File sourceFile = module.getArtifact().getFile();
399                 final File destinationFile = buildDestinationFile(getWorkDirectory(), module.getUri());
400                 if (!sourceFile.isFile()) {
401                     throw new MojoExecutionException("Cannot copy a directory: " + sourceFile.getAbsolutePath()
402                             + "; Did you package/install " + module.getArtifact() + "?");
403                 }
404 
405                 if (destinationFile.getCanonicalPath().equals(sourceFile.getCanonicalPath())) {
406                     getLog().info("Skipping artifact [" + module + "], as it already exists at [" + module.getUri()
407                             + "]");
408                     // FIXME: Shouldn't that result in a build failure!?
409                     continue;
410                 }
411 
412                 // If the module is within the unpack list, make sure that no unpack wasn't forced (null or true)
413                 // If the module is not in the unpack list, it should be true
414                 if ((unpackTypesList.contains(module.getType())
415                                 && (module.shouldUnpack() == null || module.shouldUnpack()))
416                         || (module.shouldUnpack() != null && module.shouldUnpack())) {
417                     getLog().info("Copying artifact [" + module + "] to [" + module.getUri() + "] (unpacked)");
418                     // Make sure that the destination is a directory to avoid plexus nasty stuff :)
419                     if (!destinationFile.isDirectory() && !destinationFile.mkdirs()) {
420                         throw new MojoExecutionException("Error creating " + destinationFile);
421                     }
422                     unpack(sourceFile, destinationFile, outdatedResources);
423 
424                     if (module.changeManifestClasspath()) {
425                         changeManifestClasspath(module, destinationFile, javaEEVersion, outdatedResources);
426                     }
427                 } else {
428                     if (sourceFile.lastModified() > destinationFile.lastModified()) {
429                         getLog().info("Copying artifact [" + module + "] to [" + module.getUri() + "]");
430                         createParentIfNecessary(destinationFile);
431                         Files.copy(
432                                 sourceFile.toPath(),
433                                 destinationFile.toPath(),
434                                 LinkOption.NOFOLLOW_LINKS,
435                                 StandardCopyOption.REPLACE_EXISTING);
436                         if (module.changeManifestClasspath()) {
437                             changeManifestClasspath(module, destinationFile, javaEEVersion, outdatedResources);
438                         }
439                     } else {
440                         getLog().debug("Skipping artifact [" + module + "], as it is already up to date at ["
441                                 + module.getUri() + "]");
442                     }
443                     removeFromOutdatedResources(destinationFile.toPath(), outdatedResources);
444                 }
445             }
446         } catch (IOException e) {
447             throw new MojoExecutionException("Error copying EAR modules", e);
448         } catch (ArchiverException e) {
449             throw new MojoExecutionException("Error unpacking EAR modules", e);
450         } catch (NoSuchArchiverException e) {
451             throw new MojoExecutionException("No Archiver found for EAR modules", e);
452         }
453     }
454 
455     private List<String> createUnpackList() throws MojoExecutionException {
456         List<String> unpackTypesList = new ArrayList<>();
457         if (unpackTypes != null) {
458             unpackTypesList = Arrays.asList(unpackTypes.split(","));
459             for (String type : unpackTypesList) {
460                 if (!EarModuleFactory.isStandardArtifactType(type)) {
461                     throw new MojoExecutionException("Invalid type [" + type + "] supported types are "
462                             + EarModuleFactory.getStandardArtifactTypes());
463                 }
464             }
465             getLog().debug("Initialized unpack types " + unpackTypesList);
466         }
467         return unpackTypesList;
468     }
469 
470     /**
471      * @return {@link #applicationXml}
472      */
473     public File getApplicationXml() {
474         return applicationXml;
475     }
476 
477     /**
478      * @param applicationXml {@link #applicationXml}
479      */
480     public void setApplicationXml(File applicationXml) {
481         this.applicationXml = applicationXml;
482     }
483 
484     /**
485      * Returns a string array of the excludes to be used when assembling/copying the ear.
486      *
487      * @return an array of tokens to exclude
488      */
489     protected String[] getExcludes() {
490         List<String> excludeList = new ArrayList<>(FileUtils.getDefaultExcludesAsList());
491         if (earSourceExcludes != null && !"".equals(earSourceExcludes)) {
492             excludeList.addAll(Arrays.asList(StringUtils.split(earSourceExcludes, ",")));
493         }
494 
495         // if applicationXml is specified, omit the one in the source directory
496         if (getApplicationXml() != null && !"".equals(getApplicationXml())) {
497             excludeList.add("**/" + META_INF + "/application.xml");
498         }
499 
500         return excludeList.toArray(new String[excludeList.size()]);
501     }
502 
503     /**
504      * Returns a string array of the includes to be used when assembling/copying the ear.
505      *
506      * @return an array of tokens to include
507      */
508     protected String[] getIncludes() {
509         return StringUtils.split(Objects.toString(earSourceIncludes, ""), ",");
510     }
511 
512     /**
513      * @return The array with the packaging excludes.
514      */
515     public String[] getPackagingExcludes() {
516         if (packagingExcludes == null || packagingExcludes.isEmpty()) {
517             return new String[0];
518         } else {
519             return StringUtils.split(packagingExcludes, ",");
520         }
521     }
522 
523     /**
524      * @param packagingExcludes {@link #packagingExcludes}
525      */
526     public void setPackagingExcludes(String packagingExcludes) {
527         this.packagingExcludes = packagingExcludes;
528     }
529 
530     /**
531      * @return the arrays with the includes
532      */
533     public String[] getPackagingIncludes() {
534         if (packagingIncludes == null || packagingIncludes.isEmpty()) {
535             return new String[] {"**"};
536         } else {
537             return StringUtils.split(packagingIncludes, ",");
538         }
539     }
540 
541     /**
542      * @param packagingIncludes {@link #packagingIncludes}
543      */
544     public void setPackagingIncludes(String packagingIncludes) {
545         this.packagingIncludes = packagingIncludes;
546     }
547 
548     private static File buildDestinationFile(File buildDir, String uri) {
549         return new File(buildDir, uri);
550     }
551 
552     /**
553      * Returns the EAR file to generate, based on an optional classifier.
554      *
555      * @param basedir the output directory
556      * @param finalName the name of the ear file
557      * @param classifier an optional classifier
558      * @return the EAR file to generate
559      */
560     private static File getEarFile(String basedir, String finalName, String classifier) {
561         if (classifier == null) {
562             classifier = "";
563         } else if (classifier.trim().length() > 0 && !classifier.startsWith("-")) {
564             classifier = "-" + classifier;
565         }
566 
567         return new File(basedir, finalName + classifier + ".ear");
568     }
569 
570     /**
571      * Returns a list of filenames that should be copied over to the destination directory.
572      *
573      * @param sourceDir the directory to be scanned
574      * @return the array of filenames, relative to the sourceDir
575      */
576     private String[] getEarFiles(File sourceDir) {
577         DirectoryScanner scanner = new DirectoryScanner();
578         scanner.setBasedir(sourceDir);
579         scanner.setExcludes(getExcludes());
580         scanner.addDefaultExcludes();
581 
582         scanner.setIncludes(getIncludes());
583 
584         scanner.scan();
585 
586         return scanner.getIncludedFiles();
587     }
588 
589     /**
590      * Unpacks the module into the EAR structure.
591      *
592      * @param source file to be unpacked
593      * @param destDir where to put the unpacked files
594      * @param outdatedResources currently outdated resources
595      * @throws ArchiverException a corrupt archive
596      * @throws NoSuchArchiverException if we don't have an appropriate archiver
597      * @throws IOException in case of a general IOException
598      */
599     public void unpack(File source, final File destDir, final Collection<String> outdatedResources)
600             throws ArchiverException, NoSuchArchiverException, IOException {
601         Path destPath = destDir.toPath();
602 
603         UnArchiver unArchiver = archiverManager.getUnArchiver("zip");
604         unArchiver.setSourceFile(source);
605         unArchiver.setDestDirectory(destDir);
606         unArchiver.setFileMappers(new FileMapper[] {
607             pName -> {
608                 removeFromOutdatedResources(destPath.resolve(pName), outdatedResources);
609                 return pName;
610             }
611         });
612 
613         // Extract the module
614         unArchiver.extract();
615     }
616 
617     private void copyFile(File source, File target)
618             throws MavenFilteringException, IOException, MojoExecutionException {
619         createParentIfNecessary(target);
620         if (filtering && !isNonFilteredExtension(source.getName())) {
621             mavenFileFilter.copyFile(source, target, true, getFilterWrappers(), encoding);
622         } else {
623             Files.copy(
624                     source.toPath(), target.toPath(), LinkOption.NOFOLLOW_LINKS, StandardCopyOption.REPLACE_EXISTING);
625         }
626     }
627 
628     private void createParentIfNecessary(File target) throws IOException {
629         // Silly that we have to do this ourselves
630         File parentDirectory = target.getParentFile();
631         if (parentDirectory != null && !parentDirectory.exists()) {
632             Files.createDirectories(parentDirectory.toPath());
633         }
634     }
635 
636     /**
637      * @param fileName the name of the file which should be checked
638      * @return {@code true} if the name is part of the non filtered extensions; {@code false} otherwise
639      */
640     public boolean isNonFilteredExtension(String fileName) {
641         return !mavenResourcesFiltering.filteredFileExtension(fileName, nonFilteredFileExtensions);
642     }
643 
644     private List<FilterWrapper> getFilterWrappers() throws MojoExecutionException {
645         if (filterWrappers == null) {
646             try {
647                 MavenResourcesExecution mavenResourcesExecution = new MavenResourcesExecution();
648                 mavenResourcesExecution.setMavenProject(getProject());
649                 mavenResourcesExecution.setEscapedBackslashesInFilePath(escapedBackslashesInFilePath);
650                 mavenResourcesExecution.setFilters(filters);
651                 mavenResourcesExecution.setEscapeString(escapeString);
652 
653                 filterWrappers = mavenFileFilter.getDefaultFilterWrappers(mavenResourcesExecution);
654             } catch (MavenFilteringException e) {
655                 getLog().error("Fail to build filtering wrappers " + e.getMessage());
656                 throw new MojoExecutionException(e.getMessage(), e);
657             }
658         }
659         return filterWrappers;
660     }
661 
662     private void changeManifestClasspath(
663             EarModule module, File original, JavaEEVersion javaEEVersion, Collection<String> outdatedResources)
664             throws MojoFailureException {
665         final String moduleLibDir = module.getLibDir();
666         if (!((moduleLibDir == null) || skinnyModules || (skinnyWars && module instanceof WebModule))) {
667             return;
668         }
669 
670         // for new created items
671         FileTime outputFileTime = MavenArchiver.parseBuildOutputTimestamp(outputTimestamp)
672                 .map(FileTime::from)
673                 .orElse(null);
674 
675         FileSystem fileSystem = null;
676 
677         try {
678             Path workDirectory;
679 
680             // Handle the case that the destination might be a directory (project-038)
681             // We can get FileSystems only for files
682             if (original.isFile()) {
683                 fileSystem = FileSystems.newFileSystem(
684                         original.toPath(), Thread.currentThread().getContextClassLoader());
685                 workDirectory = fileSystem.getRootDirectories().iterator().next();
686             } else {
687                 workDirectory = original.toPath();
688             }
689 
690             // Create a META-INF/MANIFEST.MF file if it doesn't exist (project-038)
691             Path metaInfDirectory = workDirectory.resolve("META-INF");
692             if (!Files.exists(metaInfDirectory)) {
693                 Files.createDirectory(metaInfDirectory);
694                 if (outputFileTime != null) {
695                     Files.setLastModifiedTime(metaInfDirectory, outputFileTime);
696                 }
697                 getLog().debug(
698                                 "This project did not have a META-INF directory before, so a new directory was created.");
699             }
700             Path manifestFile = metaInfDirectory.resolve("MANIFEST.MF");
701             if (!Files.exists(manifestFile)) {
702                 Files.createFile(manifestFile);
703                 if (outputFileTime != null) {
704                     Files.setLastModifiedTime(manifestFile, outputFileTime);
705                 }
706                 getLog().debug(
707                                 "This project did not have a META-INF/MANIFEST.MF file before, so a new file was created.");
708             }
709 
710             Manifest mf = readManifest(manifestFile);
711             Attribute classPath = mf.getMainSection().getAttribute("Class-Path");
712             List<String> classPathElements = new ArrayList<>();
713 
714             boolean classPathExists;
715             if (classPath != null) {
716                 classPathExists = true;
717                 classPathElements.addAll(Arrays.asList(classPath.getValue().split(" ")));
718             } else {
719                 classPathExists = false;
720                 classPath = new Attribute("Class-Path", "");
721             }
722 
723             if ((moduleLibDir != null) && (skinnyModules || (skinnyWars && module instanceof WebModule))) {
724                 // Remove modules
725                 for (EarModule otherModule : getAllEarModules()) {
726                     if (module.equals(otherModule)) {
727                         continue;
728                     }
729                     // MEAR-189:
730                     // We use the original name, cause in case of outputFileNameMapping
731                     // we could not not delete it and it will end up in the resulting EAR and the WAR
732                     // will not be cleaned up.
733                     final Path workLibDir = workDirectory.resolve(moduleLibDir);
734                     Path artifact =
735                             workLibDir.resolve(module.getArtifact().getFile().getName());
736 
737                     // MEAR-217
738                     // If WAR contains files with timestamps, but EAR strips them away (useBaseVersion=true)
739                     // the artifact is not found. Therefore, respect the current fileNameMapping additionally.
740 
741                     if (!Files.exists(artifact)) {
742                         getLog().debug("module does not exist with original file name.");
743                         artifact = workLibDir.resolve(otherModule.getBundleFileName());
744                         getLog().debug("Artifact with mapping: " + artifact.toAbsolutePath());
745                     }
746 
747                     if (!Files.exists(artifact)) {
748                         getLog().debug("Artifact with mapping does not exist.");
749                         artifact = workLibDir.resolve(
750                                 otherModule.getArtifact().getFile().getName());
751                         getLog().debug("Artifact with original file name: " + artifact.toAbsolutePath());
752                     }
753 
754                     if (!Files.exists(artifact)) {
755                         getLog().debug("Artifact with original file name does not exist.");
756                         final Artifact otherModuleArtifact = otherModule.getArtifact();
757                         if (otherModuleArtifact.isSnapshot()) {
758                             try {
759                                 artifact = workLibDir.resolve(MappingUtils.evaluateFileNameMapping(
760                                         ARTIFACT_DEFAULT_FILE_NAME_MAPPING, otherModuleArtifact));
761                                 getLog().debug("Artifact with default mapping file name: " + artifact.toAbsolutePath());
762                             } catch (InterpolationException e) {
763                                 getLog().warn("Failed to evaluate file name for [" + otherModule
764                                         + "] module using mapping: " + ARTIFACT_DEFAULT_FILE_NAME_MAPPING);
765                             }
766                         }
767                     }
768 
769                     if (Files.exists(artifact)) {
770                         getLog().debug(" -> Artifact to delete: " + artifact);
771                         Files.delete(artifact);
772                     }
773                 }
774             }
775 
776             // Modify the classpath entries in the manifest
777             final boolean forceClassPathModification =
778                     javaEEVersion.lt(JavaEEVersion.FIVE) || defaultLibBundleDir == null;
779             final boolean classPathExtension = !skipClassPathModification || forceClassPathModification;
780             for (EarModule otherModule : getModules()) {
781                 if (module.equals(otherModule)) {
782                     continue;
783                 }
784                 final int moduleClassPathIndex = findModuleInClassPathElements(classPathElements, otherModule);
785                 if (moduleClassPathIndex != -1) {
786                     if (otherModule.isClassPathItem()) {
787                         classPathElements.set(moduleClassPathIndex, otherModule.getUri());
788                     } else {
789                         classPathElements.remove(moduleClassPathIndex);
790                     }
791                 } else if (otherModule.isClassPathItem() && classPathExtension) {
792                     classPathElements.add(otherModule.getUri());
793                 }
794             }
795 
796             // Remove provided modules from classpath
797             for (EarModule otherModule : getProvidedEarModules()) {
798                 final int moduleClassPathIndex = findModuleInClassPathElements(classPathElements, otherModule);
799                 if (moduleClassPathIndex != -1) {
800                     classPathElements.remove(moduleClassPathIndex);
801                 }
802             }
803 
804             if (!skipClassPathModification || !classPathElements.isEmpty() || classPathExists) {
805                 classPath.setValue(StringUtils.join(classPathElements.iterator(), " "));
806                 mf.getMainSection().addConfiguredAttribute(classPath);
807 
808                 // Write the manifest to disk, preserve timestamp
809                 FileTime lastModifiedTime = Files.getLastModifiedTime(manifestFile);
810                 try (BufferedWriter writer = Files.newBufferedWriter(
811                         manifestFile,
812                         StandardCharsets.UTF_8,
813                         StandardOpenOption.WRITE,
814                         StandardOpenOption.CREATE,
815                         StandardOpenOption.TRUNCATE_EXISTING)) {
816                     mf.write(writer);
817                 }
818                 Files.setLastModifiedTime(manifestFile, lastModifiedTime);
819                 removeFromOutdatedResources(manifestFile, outdatedResources);
820             }
821 
822             if (fileSystem != null) {
823                 fileSystem.close();
824                 fileSystem = null;
825             }
826         } catch (ManifestException | IOException | ArchiverException e) {
827             throw new MojoFailureException(e.getMessage(), e);
828         } finally {
829             if (fileSystem != null) {
830                 try {
831                     fileSystem.close();
832                 } catch (IOException e) {
833                     // ignore here
834                 }
835             }
836         }
837     }
838 
839     private static Manifest readManifest(Path manifestFile) throws IOException {
840         // Read the manifest from disk
841         try (InputStream in = Files.newInputStream(manifestFile)) {
842             return new Manifest(in);
843         }
844     }
845 
846     private Collection<String> initOutdatedResources() {
847         final Collection<String> outdatedResources = new ArrayList<>();
848 
849         if (getWorkDirectory().exists()) {
850             try {
851                 Files.walkFileTree(getWorkDirectory().toPath(), new SimpleFileVisitor<Path>() {
852                     @Override
853                     public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
854                         outdatedResources.add(
855                                 getWorkDirectory().toPath().relativize(file).toString());
856                         return super.visitFile(file, attrs);
857                     }
858                 });
859             } catch (IOException e) {
860                 getLog().warn("Can't detect outdated resources", e);
861             }
862         }
863 
864         getLog().debug("initOutdatedResources: " + outdatedResources);
865         return outdatedResources;
866     }
867 
868     private void deleteOutdatedResources(final Collection<String> outdatedResources) {
869         getLog().debug("deleteOutdatedResources: " + outdatedResources);
870         final long startTime = session.getStartTime().getTime();
871 
872         getLog().debug("deleteOutdatedResources session startTime: " + startTime);
873 
874         for (String outdatedResource : outdatedResources) {
875             File resourceFile = new File(getWorkDirectory(), outdatedResource);
876             if (resourceFile.lastModified() < startTime) {
877                 getLog().info("deleting outdated resource " + outdatedResource);
878                 getLog().debug(outdatedResource + " last modified: " + resourceFile.lastModified());
879                 resourceFile.delete();
880             }
881         }
882     }
883 
884     private void removeFromOutdatedResources(Path destination, Collection<String> outdatedResources) {
885         Path relativeDestFile;
886         try {
887             relativeDestFile = getWorkDirectory().toPath().relativize(destination.normalize());
888         } catch (ProviderMismatchException e) {
889             relativeDestFile = destination.normalize();
890         }
891 
892         if (outdatedResources.remove(relativeDestFile.toString())) {
893             getLog().debug("Remove from outdatedResources: " + relativeDestFile);
894         }
895     }
896 
897     /**
898      * Searches for the given JAR module in the list of classpath elements. If JAR module is found among specified
899      * classpath elements then returns index of first matching element. Returns -1 otherwise.
900      *
901      * @param classPathElements classpath elements to search among
902      * @param module module to find among classpath elements defined by {@code classPathElements}
903      * @return -1 if {@code module} was not found in {@code classPathElements} or index of item of
904      * {@code classPathElements} which matches {@code module}
905      */
906     private int findModuleInClassPathElements(final List<String> classPathElements, final EarModule module) {
907         if (classPathElements.isEmpty()) {
908             return -1;
909         }
910         int moduleClassPathIndex = classPathElements.indexOf(module.getBundleFileName());
911         if (moduleClassPathIndex != -1) {
912             return moduleClassPathIndex;
913         }
914         final Artifact artifact = module.getArtifact();
915         moduleClassPathIndex = classPathElements.indexOf(artifact.getFile().getName());
916         if (moduleClassPathIndex != -1) {
917             return moduleClassPathIndex;
918         }
919         if (artifact.isSnapshot()) {
920             try {
921                 moduleClassPathIndex = classPathElements.indexOf(
922                         MappingUtils.evaluateFileNameMapping(ARTIFACT_DEFAULT_FILE_NAME_MAPPING, artifact));
923                 if (moduleClassPathIndex != -1) {
924                     return moduleClassPathIndex;
925                 }
926             } catch (InterpolationException e) {
927                 getLog().warn("Failed to evaluate file name for [" + module + "] module using mapping: "
928                         + ARTIFACT_DEFAULT_FILE_NAME_MAPPING);
929             }
930         }
931         return classPathElements.indexOf(module.getUri());
932     }
933 }