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.File;
23  import java.io.FileInputStream;
24  import java.io.FileOutputStream;
25  import java.io.IOException;
26  import java.io.OutputStreamWriter;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.FileVisitResult;
29  import java.nio.file.Files;
30  import java.nio.file.LinkOption;
31  import java.nio.file.Path;
32  import java.nio.file.Paths;
33  import java.nio.file.SimpleFileVisitor;
34  import java.nio.file.StandardCopyOption;
35  import java.nio.file.attribute.BasicFileAttributes;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.Collection;
39  import java.util.Date;
40  import java.util.List;
41  import java.util.Objects;
42  
43  import org.apache.maven.archiver.MavenArchiveConfiguration;
44  import org.apache.maven.archiver.MavenArchiver;
45  import org.apache.maven.artifact.Artifact;
46  import org.apache.maven.artifact.DependencyResolutionRequiredException;
47  import org.apache.maven.execution.MavenSession;
48  import org.apache.maven.plugin.MojoExecutionException;
49  import org.apache.maven.plugin.MojoFailureException;
50  import org.apache.maven.plugins.annotations.Component;
51  import org.apache.maven.plugins.annotations.LifecyclePhase;
52  import org.apache.maven.plugins.annotations.Mojo;
53  import org.apache.maven.plugins.annotations.Parameter;
54  import org.apache.maven.plugins.annotations.ResolutionScope;
55  import org.apache.maven.plugins.ear.util.EarMavenArchiver;
56  import org.apache.maven.plugins.ear.util.JavaEEVersion;
57  import org.apache.maven.project.MavenProjectHelper;
58  import org.apache.maven.shared.filtering.MavenFileFilter;
59  import org.apache.maven.shared.filtering.MavenFilteringException;
60  import org.apache.maven.shared.filtering.MavenResourcesExecution;
61  import org.apache.maven.shared.filtering.MavenResourcesFiltering;
62  import org.apache.maven.shared.mapping.MappingUtils;
63  import org.apache.maven.shared.utils.io.FileUtils;
64  import org.codehaus.plexus.archiver.Archiver;
65  import org.codehaus.plexus.archiver.ArchiverException;
66  import org.codehaus.plexus.archiver.UnArchiver;
67  import org.codehaus.plexus.archiver.ear.EarArchiver;
68  import org.codehaus.plexus.archiver.jar.JarArchiver;
69  import org.codehaus.plexus.archiver.jar.Manifest;
70  import org.codehaus.plexus.archiver.jar.Manifest.Attribute;
71  import org.codehaus.plexus.archiver.jar.ManifestException;
72  import org.codehaus.plexus.archiver.manager.ArchiverManager;
73  import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
74  import org.codehaus.plexus.archiver.zip.ZipArchiver;
75  import org.codehaus.plexus.archiver.zip.ZipUnArchiver;
76  import org.codehaus.plexus.components.io.filemappers.FileMapper;
77  import org.codehaus.plexus.interpolation.InterpolationException;
78  import org.codehaus.plexus.util.DirectoryScanner;
79  import org.codehaus.plexus.util.StringUtils;
80  
81  /**
82   * Builds J2EE Enterprise Archive (EAR) files.
83   * 
84   * @author <a href="snicoll@apache.org">Stephane Nicoll</a>
85   */
86  @Mojo( name = "ear",
87         defaultPhase = LifecyclePhase.PACKAGE,
88         threadSafe = true,
89         requiresDependencyResolution = ResolutionScope.TEST )
90  public class EarMojo
91      extends AbstractEarMojo
92  {
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 String 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     @Component( role = Archiver.class, hint = "ear" )
242     private EarArchiver earArchiver;
243 
244     /**
245      * The Plexus JAR archiver to create the output archive if not EAR application descriptor is provided (JavaEE 5+).
246      */
247     @Component( role = Archiver.class, hint = "jar" )
248     private JarArchiver jarArchiver;
249 
250     /**
251      * The Plexus Zip archiver for Skinny WAR repackaging.
252      */
253     @Component( role = Archiver.class, hint = "zip" )
254     private ZipArchiver zipArchiver;
255 
256     /**
257      * The Plexus Zip Un archiver for Skinny WAR repackaging.
258      */
259     @Component( role = UnArchiver.class, hint = "zip" )
260     private ZipUnArchiver zipUnArchiver;
261 
262     /**
263      * The archive configuration to use. See <a href="https://maven.apache.org/shared/maven-archiver/">Maven Archiver
264      * Reference</a>.
265      */
266     @Parameter
267     private MavenArchiveConfiguration archive = new MavenArchiveConfiguration();
268 
269     /**
270      * Timestamp for reproducible output archive entries, either formatted as ISO 8601
271      * <code>yyyy-MM-dd'T'HH:mm:ssXXX</code> or as an int representing seconds since the epoch (like
272      * <a href="https://reproducible-builds.org/docs/source-date-epoch/">SOURCE_DATE_EPOCH</a>).
273      *
274      * @since 3.1.0
275      */
276     @Parameter( defaultValue = "${project.build.outputTimestamp}" )
277     private String outputTimestamp;
278 
279     /**
280      */
281     @Component
282     private MavenProjectHelper projectHelper;
283 
284     /**
285      * The archive manager.
286      */
287     @Component
288     private ArchiverManager archiverManager;
289 
290     /**
291      */
292     @Component( role = MavenFileFilter.class, hint = "default" )
293     private MavenFileFilter mavenFileFilter;
294 
295     /**
296      */
297     @Component( role = MavenResourcesFiltering.class, hint = "default" )
298     private MavenResourcesFiltering mavenResourcesFiltering;
299 
300     /**
301      * @since 2.3.2
302      */
303     @Parameter( defaultValue = "${session}", readonly = true, required = true )
304     private MavenSession session;
305 
306     private List<FileUtils.FilterWrapper> filterWrappers;
307 
308     /**
309      * @since 2.9
310      */
311     @Parameter( defaultValue = "true" )
312     private boolean useJvmChmod = true;
313 
314     /** {@inheritDoc} */
315     public void execute()
316         throws MojoExecutionException, MojoFailureException
317     {
318         // Initializes ear modules
319         super.execute();
320 
321         File earFile = getEarFile( outputDirectory, finalName, classifier );
322         MavenArchiver archiver = new EarMavenArchiver( getModules() );
323         File ddFile = new File( getWorkDirectory(), APPLICATION_XML_URI );
324 
325         JarArchiver theArchiver;
326         if ( ddFile.exists() )
327         {
328             earArchiver.setAppxml( ddFile );
329             theArchiver = earArchiver;
330         }
331         else
332         {
333             // current Plexus EarArchiver does not support application.xml-less JavaEE 5+ case
334             // => fallback to Plexus Jar archiver 
335             theArchiver = jarArchiver;
336         }
337         getLog().debug( "Ear archiver implementation [" + theArchiver.getClass().getName() + "]" );
338         archiver.setArchiver( theArchiver );
339         archiver.setOutputFile( earFile );
340         archiver.setCreatedBy( "Maven EAR Plugin", "org.apache.maven.plugins", "maven-ear-plugin" );
341 
342         // configure for Reproducible Builds based on outputTimestamp value
343         Date reproducibleLastModifiedDate = archiver.configureReproducible( outputTimestamp );
344 
345         zipArchiver.setUseJvmChmod( useJvmChmod );
346         if ( reproducibleLastModifiedDate != null )
347         {
348             zipArchiver.configureReproducible( reproducibleLastModifiedDate );
349         }
350         zipUnArchiver.setUseJvmChmod( useJvmChmod );
351 
352         final JavaEEVersion javaEEVersion = JavaEEVersion.getJavaEEVersion( version );
353 
354         final Collection<String> outdatedResources = initOutdatedResources();
355 
356         // Initializes unpack types
357         List<String> unpackTypesList = createUnpackList();
358 
359         // Copy modules
360         copyModules( javaEEVersion, unpackTypesList, outdatedResources );
361 
362         // Copy source files
363         try
364         {
365             File earSourceDir = earSourceDirectory;
366 
367             if ( earSourceDir.exists() )
368             {
369                 getLog().info( "Copy ear sources to " + getWorkDirectory().getAbsolutePath() );
370                 String[] fileNames = getEarFiles( earSourceDir );
371                 for ( String fileName : fileNames )
372                 {
373                     copyFile( new File( earSourceDir, fileName ), new File( getWorkDirectory(), fileName ) );
374                     outdatedResources.remove( Paths.get( fileName ).toString() );
375                 }
376             }
377 
378             if ( applicationXml != null && !"".equals( applicationXml ) )
379             {
380                 // rename to application.xml
381                 getLog().info( "Including custom application.xml[" + applicationXml + "]" );
382                 File metaInfDir = new File( getWorkDirectory(), META_INF );
383                 copyFile( new File( applicationXml ), new File( metaInfDir, "/application.xml" ) );
384                 outdatedResources.remove( Paths.get( "META-INF/application.xml" ).toString() );
385             }
386         }
387         catch ( IOException e )
388         {
389             throw new MojoExecutionException( "Error copying EAR sources", e );
390         }
391         catch ( MavenFilteringException e )
392         {
393             throw new MojoExecutionException( "Error filtering EAR sources", e );
394         }
395 
396         // Check if deployment descriptor is there
397         if ( !ddFile.exists() && ( javaEEVersion.lt( JavaEEVersion.FIVE ) ) )
398         {
399             throw new MojoExecutionException( "Deployment descriptor: " + ddFile.getAbsolutePath()
400                 + " does not exist." );
401         }
402         // no need to check timestamp for descriptors: removing if outdated does not really make sense
403         outdatedResources.remove( Paths.get( APPLICATION_XML_URI ).toString() );
404         if ( getJbossConfiguration() != null )
405         {
406             outdatedResources.remove( Paths.get( "META-INF/jboss-app.xml" ).toString() );
407         }
408 
409         deleteOutdatedResources( outdatedResources );
410 
411         try
412         {
413             getLog().debug( "Excluding " + Arrays.asList( getPackagingExcludes() ) + " from the generated EAR." );
414             getLog().debug( "Including " + Arrays.asList( getPackagingIncludes() ) + " in the generated EAR." );
415 
416             archiver.getArchiver().addDirectory( getWorkDirectory(), getPackagingIncludes(), getPackagingExcludes() );
417 
418             archiver.createArchive( session, getProject(), archive );
419         }
420         catch ( ManifestException | IOException | DependencyResolutionRequiredException e )
421         {
422             throw new MojoExecutionException( "Error assembling EAR", e );
423         }
424 
425         if ( classifier != null )
426         {
427             projectHelper.attachArtifact( getProject(), "ear", classifier, earFile );
428         }
429         else
430         {
431             getProject().getArtifact().setFile( earFile );
432         }
433     }
434 
435     private void copyModules( final JavaEEVersion javaEEVersion, 
436                               List<String> unpackTypesList, 
437                               Collection<String> outdatedResources )
438         throws MojoExecutionException, MojoFailureException
439     {
440         final Path workingDir = getWorkDirectory().toPath();
441 
442         try
443         {
444             for ( EarModule module : getModules() )
445             {
446                 final File sourceFile = module.getArtifact().getFile();
447                 final File destinationFile = buildDestinationFile( getWorkDirectory(), module.getUri() );
448                 if ( !sourceFile.isFile() )
449                 {
450                     throw new MojoExecutionException( "Cannot copy a directory: " + sourceFile.getAbsolutePath()
451                         + "; Did you package/install " + module.getArtifact() + "?" );
452                 }
453 
454                 if ( destinationFile.getCanonicalPath().equals( sourceFile.getCanonicalPath() ) )
455                 {
456                     getLog().info( "Skipping artifact [" + module + "], as it already exists at [" + module.getUri()
457                         + "]" );
458                     // FIXME: Shouldn't that result in a build failure!?
459                     continue;
460                 }
461 
462                 // If the module is within the unpack list, make sure that no unpack wasn't forced (null or true)
463                 // If the module is not in the unpack list, it should be true
464                 if ( ( unpackTypesList.contains( module.getType() )
465                     && ( module.shouldUnpack() == null || module.shouldUnpack() ) )
466                     || ( module.shouldUnpack() != null && module.shouldUnpack() ) )
467                 {
468                     getLog().info( "Copying artifact [" + module + "] to [" + module.getUri() + "] (unpacked)" );
469                     // Make sure that the destination is a directory to avoid plexus nasty stuff :)
470                     if ( !destinationFile.isDirectory() && !destinationFile.mkdirs() )
471                     {
472                         throw new MojoExecutionException( "Error creating " + destinationFile );
473                     }
474                     unpack( sourceFile, destinationFile, outdatedResources );
475 
476                     if ( module.changeManifestClasspath() )
477                     {
478                         changeManifestClasspath( module, destinationFile, javaEEVersion );
479                     }
480                 }
481                 else
482                 {
483                     if ( sourceFile.lastModified() > destinationFile.lastModified() )
484                     {
485                         getLog().info( "Copying artifact [" + module + "] to [" + module.getUri() + "]" );
486                         createParentIfNecessary( destinationFile );
487                         Files.copy( sourceFile.toPath(), destinationFile.toPath(),
488                             LinkOption.NOFOLLOW_LINKS, StandardCopyOption.REPLACE_EXISTING );
489                         if ( module.changeManifestClasspath() )
490                         {
491                             changeManifestClasspath( module, destinationFile, javaEEVersion );
492                         }
493                     }
494                     else
495                     {
496                         getLog().debug( "Skipping artifact [" + module + "], as it is already up to date at ["
497                             + module.getUri() + "]" );
498                     }
499                     outdatedResources.remove( workingDir.relativize( destinationFile.toPath() ).toString() );
500                 }
501             }
502         }
503         catch ( IOException e )
504         {
505             throw new MojoExecutionException( "Error copying EAR modules", e );
506         }
507         catch ( ArchiverException e )
508         {
509             throw new MojoExecutionException( "Error unpacking EAR modules", e );
510         }
511         catch ( NoSuchArchiverException e )
512         {
513             throw new MojoExecutionException( "No Archiver found for EAR modules", e );
514         }
515     }
516 
517     private List<String> createUnpackList()
518         throws MojoExecutionException
519     {
520         List<String> unpackTypesList = new ArrayList<String>();
521         if ( unpackTypes != null )
522         {
523             unpackTypesList = Arrays.asList( unpackTypes.split( "," ) );
524             for ( String type : unpackTypesList )
525             {
526                 if ( !EarModuleFactory.isStandardArtifactType( type ) )
527                 {
528                     throw new MojoExecutionException( "Invalid type [" + type + "] supported types are "
529                         + EarModuleFactory.getStandardArtifactTypes() );
530                 }
531             }
532             getLog().debug( "Initialized unpack types " + unpackTypesList );
533         }
534         return unpackTypesList;
535     }
536 
537     /**
538      * @return {@link #applicationXml}
539      */
540     public String getApplicationXml()
541     {
542         return applicationXml;
543     }
544 
545     /**
546      * @param applicationXml {@link #applicationXml}
547      */
548     public void setApplicationXml( String applicationXml )
549     {
550         this.applicationXml = applicationXml;
551     }
552 
553     /**
554      * Returns a string array of the excludes to be used when assembling/copying the ear.
555      * 
556      * @return an array of tokens to exclude
557      */
558     protected String[] getExcludes()
559     {
560         List<String> excludeList = new ArrayList<String>( FileUtils.getDefaultExcludesAsList() );
561         if ( earSourceExcludes != null && !"".equals( earSourceExcludes ) )
562         {
563             excludeList.addAll( Arrays.asList( StringUtils.split( earSourceExcludes, "," ) ) );
564         }
565 
566         // if applicationXml is specified, omit the one in the source directory
567         if ( getApplicationXml() != null && !"".equals( getApplicationXml() ) )
568         {
569             excludeList.add( "**/" + META_INF + "/application.xml" );
570         }
571 
572         return excludeList.toArray( new String[excludeList.size()] );
573     }
574 
575     /**
576      * Returns a string array of the includes to be used when assembling/copying the ear.
577      * 
578      * @return an array of tokens to include
579      */
580     protected String[] getIncludes()
581     {
582         return StringUtils.split( Objects.toString( earSourceIncludes, "" ), "," );
583     }
584 
585     /**
586      * @return The array with the packaging excludes.
587      */
588     public String[] getPackagingExcludes()
589     {
590         if ( StringUtils.isEmpty( packagingExcludes ) )
591         {
592             return new String[0];
593         }
594         else
595         {
596             return StringUtils.split( packagingExcludes, "," );
597         }
598     }
599 
600     /**
601      * @param packagingExcludes {@link #packagingExcludes}
602      */
603     public void setPackagingExcludes( String packagingExcludes )
604     {
605         this.packagingExcludes = packagingExcludes;
606     }
607 
608     /**
609      * @return the arrays with the includes
610      */
611     public String[] getPackagingIncludes()
612     {
613         if ( StringUtils.isEmpty( packagingIncludes ) )
614         {
615             return new String[] { "**" };
616         }
617         else
618         {
619             return StringUtils.split( packagingIncludes, "," );
620         }
621     }
622 
623     /**
624      * @param packagingIncludes {@link #packagingIncludes}
625      */
626     public void setPackagingIncludes( String packagingIncludes )
627     {
628         this.packagingIncludes = packagingIncludes;
629     }
630 
631     private static File buildDestinationFile( File buildDir, String uri )
632     {
633         return new File( buildDir, uri );
634     }
635 
636     /**
637      * Returns the EAR file to generate, based on an optional classifier.
638      * 
639      * @param basedir the output directory
640      * @param finalName the name of the ear file
641      * @param classifier an optional classifier
642      * @return the EAR file to generate
643      */
644     private static File getEarFile( String basedir, String finalName, String classifier )
645     {
646         if ( classifier == null )
647         {
648             classifier = "";
649         }
650         else if ( classifier.trim().length() > 0 && !classifier.startsWith( "-" ) )
651         {
652             classifier = "-" + classifier;
653         }
654 
655         return new File( basedir, finalName + classifier + ".ear" );
656     }
657 
658     /**
659      * Returns a list of filenames that should be copied over to the destination directory.
660      * 
661      * @param sourceDir the directory to be scanned
662      * @return the array of filenames, relative to the sourceDir
663      */
664     private String[] getEarFiles( File sourceDir )
665     {
666         DirectoryScanner scanner = new DirectoryScanner();
667         scanner.setBasedir( sourceDir );
668         scanner.setExcludes( getExcludes() );
669         scanner.addDefaultExcludes();
670 
671         scanner.setIncludes( getIncludes() );
672 
673         scanner.scan();
674 
675         return scanner.getIncludedFiles();
676     }
677 
678     /**
679      * Unpacks the module into the EAR structure.
680      * 
681      * @param source file to be unpacked
682      * @param destDir where to put the unpacked files
683      * @param outdatedResources currently outdated resources
684      * @throws ArchiverException a corrupt archive
685      * @throws NoSuchArchiverException if we don't have an appropriate archiver
686      * @throws IOException in case of a general IOException
687      */
688     public void unpack( File source, final File destDir, final Collection<String> outdatedResources )
689         throws ArchiverException, NoSuchArchiverException, IOException
690     {
691         UnArchiver unArchiver = archiverManager.getUnArchiver( "zip" );
692         unArchiver.setSourceFile( source );
693         unArchiver.setDestDirectory( destDir );
694         unArchiver.setFileMappers( new FileMapper[] {
695             new FileMapper()
696             {
697                 @Override
698                 public String getMappedFileName( String pName )
699                 {
700                     Path destFile = destDir.toPath().resolve( pName );
701                     outdatedResources.remove( getWorkDirectory().toPath().relativize( destFile ).toString() );
702                     return pName;
703                 }
704             }
705         } );
706 
707         // Extract the module
708         unArchiver.extract();
709     }
710 
711     private void copyFile( File source, File target )
712         throws MavenFilteringException, IOException, MojoExecutionException
713     {
714         createParentIfNecessary( target );
715         if ( filtering && !isNonFilteredExtension( source.getName() ) )
716         {
717             mavenFileFilter.copyFile( source, target, true, getFilterWrappers(), encoding );
718         }
719         else
720         {
721             Files.copy( source.toPath(), target.toPath(), LinkOption.NOFOLLOW_LINKS,
722                        StandardCopyOption.REPLACE_EXISTING );
723         }
724     }
725 
726     private void createParentIfNecessary( File target )
727         throws IOException
728     {
729         // Silly that we have to do this ourselves
730         File parentDirectory = target.getParentFile();
731         if ( parentDirectory != null && !parentDirectory.exists() )
732         {
733             Files.createDirectories( parentDirectory.toPath() );
734         }
735     }
736 
737     /**
738      * @param fileName the name of the file which should be checked
739      * @return {@code true} if the name is part of the non filtered extensions; {@code false} otherwise
740      */
741     public boolean isNonFilteredExtension( String fileName )
742     {
743         return !mavenResourcesFiltering.filteredFileExtension( fileName, nonFilteredFileExtensions );
744     }
745 
746     private List<FileUtils.FilterWrapper> getFilterWrappers()
747         throws MojoExecutionException
748     {
749         if ( filterWrappers == null )
750         {
751             try
752             {
753                 MavenResourcesExecution mavenResourcesExecution = new MavenResourcesExecution();
754                 mavenResourcesExecution.setMavenProject( getProject() );
755                 mavenResourcesExecution.setEscapedBackslashesInFilePath( escapedBackslashesInFilePath );
756                 mavenResourcesExecution.setFilters( filters );
757                 mavenResourcesExecution.setEscapeString( escapeString );
758 
759                 filterWrappers = mavenFileFilter.getDefaultFilterWrappers( mavenResourcesExecution );
760             }
761             catch ( MavenFilteringException e )
762             {
763                 getLog().error( "Fail to build filtering wrappers " + e.getMessage() );
764                 throw new MojoExecutionException( e.getMessage(), e );
765             }
766         }
767         return filterWrappers;
768     }
769 
770     private void changeManifestClasspath( EarModule module, File original, JavaEEVersion javaEEVersion )
771         throws MojoFailureException
772     {
773         final String moduleLibDir = module.getLibDir();
774         if ( !( ( moduleLibDir == null ) || skinnyModules || ( skinnyWars && module instanceof WebModule ) ) )
775         {
776             return;
777         }
778         try
779         {
780             File workDirectory;
781 
782             // Handle the case that the destination might be a directory (project-038)
783             if ( original.isFile() )
784             {
785                 // Create a temporary work directory
786                 // MEAR-167 use uri as directory to prevent merging of artifacts with the same artifactId
787                 workDirectory = new File( new File( getTempFolder(), "temp" ), module.getUri() );
788                 if ( !workDirectory.isDirectory() )
789                 {
790                     if ( workDirectory.mkdirs() )
791                     {
792                         getLog().debug( "Created a temporary work directory: " + workDirectory.getAbsolutePath() );
793                     }
794                     else
795                     {
796                         throw new MojoFailureException( "Failed to create directory " + workDirectory );
797                     }
798                 }
799                 // Unpack the archive to a temporary work directory
800                 zipUnArchiver.setSourceFile( original );
801                 zipUnArchiver.setDestDirectory( workDirectory );
802                 zipUnArchiver.extract();
803             }
804             else
805             {
806                 workDirectory = original;
807             }
808 
809             // Create a META-INF/MANIFEST.MF file if it doesn't exist (project-038)
810             File metaInfDirectory = new File( workDirectory, "META-INF" );
811             boolean newMetaInfCreated = metaInfDirectory.mkdirs();
812             if ( newMetaInfCreated )
813             {
814                 getLog().debug(
815                     "This project did not have a META-INF directory before, so a new directory was created." );
816             }
817             File manifestFile = new File( metaInfDirectory, "MANIFEST.MF" );
818             boolean newManifestCreated = manifestFile.createNewFile();
819             if ( newManifestCreated )
820             {
821                 getLog().debug(
822                     "This project did not have a META-INF/MANIFEST.MF file before, so a new file was created." );
823             }
824 
825             Manifest mf = readManifest( manifestFile );
826             Attribute classPath = mf.getMainSection().getAttribute( "Class-Path" );
827             List<String> classPathElements = new ArrayList<String>();
828 
829             boolean classPathExists;
830             if ( classPath != null )
831             {
832                 classPathExists = true;
833                 classPathElements.addAll( Arrays.asList( classPath.getValue().split( " " ) ) );
834             }
835             else
836             {
837                 classPathExists = false;
838                 classPath = new Attribute( "Class-Path", "" );
839             }
840 
841             if ( ( moduleLibDir != null ) && ( skinnyModules || ( skinnyWars && module instanceof WebModule ) ) )
842             {
843                 // Remove modules
844                 for ( EarModule otherModule : getAllEarModules() )
845                 {
846                     if ( module.equals( otherModule ) )
847                     {
848                         continue;
849                     }
850                     // MEAR-189:
851                     // We use the original name, cause in case of outputFileNameMapping
852                     // we could not not delete it and it will end up in the resulting EAR and the WAR
853                     // will not be cleaned up.
854                     final File workLibDir = new File( workDirectory, moduleLibDir );
855                     File artifact = new File( workLibDir, module.getArtifact().getFile().getName() );
856 
857                     // MEAR-217
858                     // If WAR contains files with timestamps, but EAR strips them away (useBaseVersion=true)
859                     // the artifact is not found. Therefore respect the current fileNameMapping additionally.
860 
861                     if ( !artifact.exists() )
862                     {
863                         getLog().debug( "module does not exist with original file name." );
864                         artifact = new File( workLibDir, otherModule.getBundleFileName() );
865                         getLog().debug( "Artifact with mapping: " + artifact.getAbsolutePath() );
866                     }
867 
868                     if ( !artifact.exists() )
869                     {
870                         getLog().debug( "Artifact with mapping does not exist." );
871                         artifact = new File( workLibDir, otherModule.getArtifact().getFile().getName() );
872                         getLog().debug( "Artifact with original file name: " + artifact.getAbsolutePath() );
873                     }
874 
875                     if ( !artifact.exists() )
876                     {
877                         getLog().debug( "Artifact with original file name does not exist." );
878                         final Artifact otherModuleArtifact = otherModule.getArtifact();
879                         if ( otherModuleArtifact.isSnapshot() )
880                         {
881                             try
882                             {
883                                 artifact = new File( workLibDir, MappingUtils.evaluateFileNameMapping(
884                                         ARTIFACT_DEFAULT_FILE_NAME_MAPPING, otherModuleArtifact ) );
885                                 getLog()
886                                     .debug( "Artifact with default mapping file name: " + artifact.getAbsolutePath() );
887                             }
888                             catch ( InterpolationException e )
889                             {
890                                 getLog().warn(
891                                     "Failed to evaluate file name for [" + otherModule + "] module using mapping: "
892                                         + ARTIFACT_DEFAULT_FILE_NAME_MAPPING );
893                             }
894                         }
895                     }
896 
897                     if ( artifact.exists() )
898                     {
899                         getLog().debug( " -> Artifact to delete: " + artifact );
900                         if ( !artifact.delete() )
901                         {
902                             getLog().error( "Could not delete '" + artifact + "'" );
903                         }
904                     }
905                 }
906             }
907 
908             // Modify the classpath entries in the manifest
909             final boolean forceClassPathModification =
910                 javaEEVersion.lt( JavaEEVersion.FIVE ) || defaultLibBundleDir == null;
911             final boolean classPathExtension = !skipClassPathModification || forceClassPathModification;
912             for ( EarModule otherModule : getModules() )
913             {
914                 if ( module.equals( otherModule ) )
915                 {
916                     continue;
917                 }
918                 final int moduleClassPathIndex = findModuleInClassPathElements( classPathElements, otherModule );
919                 if ( moduleClassPathIndex != -1 )
920                 {
921                     if ( otherModule.isClassPathItem() )
922                     {
923                         classPathElements.set( moduleClassPathIndex, otherModule.getUri() );
924                     }
925                     else
926                     {
927                         classPathElements.remove( moduleClassPathIndex );
928                     }
929                 }
930                 else if ( otherModule.isClassPathItem() && classPathExtension )
931                 {
932                     classPathElements.add( otherModule.getUri() );
933                 }
934             }
935 
936             // Remove provided modules from classpath
937             for ( EarModule otherModule : getProvidedEarModules() )
938             {
939                 final int moduleClassPathIndex = findModuleInClassPathElements( classPathElements, otherModule );
940                 if ( moduleClassPathIndex != -1 )
941                 {
942                     classPathElements.remove( moduleClassPathIndex );
943                 }
944             }
945 
946             if ( !skipClassPathModification || !classPathElements.isEmpty() || classPathExists )
947             {
948                 classPath.setValue( StringUtils.join( classPathElements.iterator(), " " ) );
949                 mf.getMainSection().addConfiguredAttribute( classPath );
950 
951                 // Write the manifest to disk
952                 try ( FileOutputStream out = new FileOutputStream( manifestFile );
953                       OutputStreamWriter writer = new OutputStreamWriter( out, StandardCharsets.UTF_8 ) )
954                 {
955                     mf.write( writer );
956                 }
957             }
958 
959             if ( original.isFile() )
960             {
961                 // Pack up the archive again from the work directory
962                 if ( !original.delete() )
963                 {
964                     getLog().error( "Could not delete original artifact file " + original );
965                 }
966 
967                 getLog().debug( "Zipping module" );
968                 zipArchiver.setDestFile( original );
969                 zipArchiver.addDirectory( workDirectory );
970                 zipArchiver.createArchive();
971             }
972         }
973         catch ( ManifestException | IOException | ArchiverException e )
974         {
975             throw new MojoFailureException( e.getMessage(), e );
976         }
977     }
978 
979     private static Manifest readManifest( File manifestFile )
980         throws IOException
981     {
982         // Read the manifest from disk
983         try ( FileInputStream in = new FileInputStream( manifestFile ) )
984         {
985             Manifest manifest = new Manifest( in );
986             return manifest;
987         }
988     }
989 
990     private Collection<String> initOutdatedResources()
991     {
992         final Collection<String> outdatedResources = new ArrayList<>();
993         
994         if ( getWorkDirectory().exists() )
995         {
996             try
997             {
998                 Files.walkFileTree( getWorkDirectory().toPath(), new SimpleFileVisitor<Path>() 
999                 {
1000                     @Override
1001                     public FileVisitResult visitFile( Path file, BasicFileAttributes attrs )
1002                         throws IOException
1003                     {
1004                         outdatedResources.add( getWorkDirectory().toPath().relativize( file ).toString() );
1005                         return super.visitFile( file, attrs );
1006                     }
1007                 } );
1008             }
1009             catch ( IOException e )
1010             {
1011                 getLog().warn( "Can't detect outdated resources", e );
1012             } 
1013         }
1014         return outdatedResources;
1015     }
1016 
1017     private void deleteOutdatedResources( final Collection<String> outdatedResources )
1018     {
1019         final long startTime = session.getStartTime().getTime();
1020         
1021         for ( String outdatedResource : outdatedResources )
1022         {
1023             if ( new File( getWorkDirectory(), outdatedResource ).lastModified() < startTime )
1024             {
1025                 getLog().info( "deleting outdated resource " + outdatedResource );
1026                 new File( getWorkDirectory(), outdatedResource ).delete();
1027             }
1028         }
1029     }
1030 
1031     /**
1032      * Searches for the given JAR module in the list of classpath elements. If JAR module is found among specified
1033      * classpath elements then returns index of first matching element. Returns -1 otherwise.
1034      *
1035      * @param classPathElements classpath elements to search among
1036      * @param module module to find among classpath elements defined by {@code classPathElements}
1037      * @return -1 if {@code module} was not found in {@code classPathElements} or index of item of
1038      * {@code classPathElements} which matches {@code module}
1039      */
1040     private int findModuleInClassPathElements( final List<String> classPathElements, final EarModule module )
1041     {
1042         if ( classPathElements.isEmpty() )
1043         {
1044             return -1;
1045         }
1046         int moduleClassPathIndex = classPathElements.indexOf( module.getBundleFileName() );
1047         if ( moduleClassPathIndex != -1 )
1048         {
1049             return moduleClassPathIndex;
1050         }
1051         final Artifact artifact = module.getArtifact();
1052         moduleClassPathIndex = classPathElements.indexOf( artifact.getFile().getName() );
1053         if ( moduleClassPathIndex != -1 )
1054         {
1055             return moduleClassPathIndex;
1056         }
1057         if ( artifact.isSnapshot() )
1058         {
1059             try
1060             {
1061                 moduleClassPathIndex = classPathElements
1062                     .indexOf( MappingUtils.evaluateFileNameMapping( ARTIFACT_DEFAULT_FILE_NAME_MAPPING, artifact ) );
1063                 if ( moduleClassPathIndex != -1 )
1064                 {
1065                     return moduleClassPathIndex;
1066                 }
1067             }
1068             catch ( InterpolationException e )
1069             {
1070                 getLog().warn( "Failed to evaluate file name for [" + module + "] module using mapping: "
1071                     + ARTIFACT_DEFAULT_FILE_NAME_MAPPING );
1072             }
1073         }
1074         return classPathElements.indexOf( module.getUri() );
1075     }
1076 }