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