View Javadoc
1   package org.apache.maven.plugin.war.packaging;
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.IOException;
24  
25  import org.apache.commons.io.input.XmlStreamReader;
26  import org.apache.maven.artifact.Artifact;
27  import org.apache.maven.plugin.MojoExecutionException;
28  import org.apache.maven.plugin.war.util.PathSet;
29  import org.apache.maven.plugin.war.util.WebappStructure;
30  import org.apache.maven.shared.filtering.MavenFilteringException;
31  import org.apache.maven.shared.mapping.MappingUtils;
32  import org.codehaus.plexus.archiver.ArchiverException;
33  import org.codehaus.plexus.archiver.UnArchiver;
34  import org.codehaus.plexus.archiver.jar.JarArchiver;
35  import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
36  import org.codehaus.plexus.interpolation.InterpolationException;
37  import org.codehaus.plexus.util.DirectoryScanner;
38  import org.codehaus.plexus.util.FileUtils;
39  import org.codehaus.plexus.util.IOUtil;
40  
41  /**
42   * @author Stephane Nicoll
43   * @version $Id: AbstractWarPackagingTask.html 935522 2015-01-08 20:15:45Z khmarbaise $
44   */
45  public abstract class AbstractWarPackagingTask
46      implements WarPackagingTask
47  {
48      /**
49       * The default list of includes.
50       */
51      public static final String[] DEFAULT_INCLUDES = { "**/**" };
52  
53      /**
54       * The {@code WEB-INF} path.
55       */
56      public static final String WEB_INF_PATH = "WEB-INF";
57  
58      /**
59       * The {@code META-INF} path.
60       */
61      public static final String META_INF_PATH = "META-INF";
62  
63      /**
64       * The {@code classes} path.
65       */
66      public static final String CLASSES_PATH = "WEB-INF/classes/";
67  
68      /**
69       * The {@code lib} path.
70       */
71      public static final String LIB_PATH = "WEB-INF/lib/";
72  
73      /**
74       * Copies the files if possible with an optional target prefix.
75       * <p/>
76       * Copy uses a first-win strategy: files that have already been copied by previous tasks are ignored. This method
77       * makes sure to update the list of protected files which gives the list of files that have already been copied.
78       * <p/>
79       * If the structure of the source directory is not the same as the root of the webapp, use the <tt>targetPrefix</tt>
80       * parameter to specify in which particular directory the files should be copied. Use <tt>null</tt> to copy the
81       * files with the same structure
82       *
83       * @param sourceId the source id
84       * @param context the context to use
85       * @param sourceBaseDir the base directory from which the <tt>sourceFilesSet</tt> will be copied
86       * @param sourceFilesSet the files to be copied
87       * @param targetPrefix the prefix to add to the target file name
88       * @param filtered filter or not.
89       * @throws IOException if an error occurred while copying the files
90       * @throws MojoExecutionException if an error occurs.
91       */
92      protected void copyFiles( String sourceId, WarPackagingContext context, File sourceBaseDir, PathSet sourceFilesSet,
93                                String targetPrefix, boolean filtered )
94          throws IOException, MojoExecutionException
95      {
96          for ( String fileToCopyName : sourceFilesSet.paths() )
97          {
98              final File sourceFile = new File( sourceBaseDir, fileToCopyName );
99  
100             String destinationFileName;
101             if ( targetPrefix == null )
102             {
103                 destinationFileName = fileToCopyName;
104             }
105             else
106             {
107                 destinationFileName = targetPrefix + fileToCopyName;
108             }
109 
110             if ( filtered && !context.isNonFilteredExtension( sourceFile.getName() ) )
111             {
112                 copyFilteredFile( sourceId, context, sourceFile, destinationFileName );
113             }
114             else
115             {
116                 copyFile( sourceId, context, sourceFile, destinationFileName );
117             }
118         }
119     }
120 
121     /**
122      * Copies the files if possible as is.
123      * <p/>
124      * Copy uses a first-win strategy: files that have already been copied by previous tasks are ignored. This method
125      * makes sure to update the list of protected files which gives the list of files that have already been copied.
126      *
127      * @param sourceId the source id
128      * @param context the context to use
129      * @param sourceBaseDir the base directory from which the <tt>sourceFilesSet</tt> will be copied
130      * @param sourceFilesSet the files to be copied
131      * @param filtered filter or not.
132      * @throws IOException if an error occurred while copying the files
133      * @throws MojoExecutionException break the build.
134      */
135     protected void copyFiles( String sourceId, WarPackagingContext context, File sourceBaseDir, PathSet sourceFilesSet,
136                               boolean filtered )
137         throws IOException, MojoExecutionException
138     {
139         copyFiles( sourceId, context, sourceBaseDir, sourceFilesSet, null, filtered );
140     }
141 
142     /**
143      * Copy the specified file if the target location has not yet already been used.
144      * <p/>
145      * The <tt>targetFileName</tt> is the relative path according to the root of the generated web application.
146      *
147      * @param sourceId the source id
148      * @param context the context to use
149      * @param file the file to copy
150      * @param targetFilename the relative path according to the root of the webapp
151      * @throws IOException if an error occurred while copying
152      */
153     // CHECKSTYLE_OFF: LineLength
154     protected void copyFile( String sourceId, final WarPackagingContext context, final File file, String targetFilename )
155         throws IOException
156     // CHECKSTYLE_ON: LineLength
157     {
158         final File targetFile = new File( context.getWebappDirectory(), targetFilename );
159 
160         if ( file.isFile() )
161         {
162             context.getWebappStructure().registerFile( sourceId, targetFilename,
163            new WebappStructure.RegistrationCallback()
164            {
165                public void registered( String ownerId, String targetFilename )
166                    throws IOException
167                {
168                    copyFile( context, file, targetFile, targetFilename,
169                              false );
170                }
171     
172                public void alreadyRegistered( String ownerId,
173                                               String targetFilename )
174                    throws IOException
175                {
176                    copyFile( context, file, targetFile, targetFilename,
177                              true );
178                }
179     
180                public void refused( String ownerId, String targetFilename,
181                                     String actualOwnerId )
182                    throws IOException
183                {
184                    context.getLog().debug( " - "
185                                                + targetFilename
186                                                + " wasn't copied because it has "
187                                                + "already been packaged for overlay ["
188                                                + actualOwnerId + "]." );
189                }
190     
191                public void superseded( String ownerId,
192                                        String targetFilename,
193                                        String deprecatedOwnerId )
194                    throws IOException
195                {
196                    context.getLog().info( "File ["
197                                               + targetFilename
198                                               + "] belonged to overlay ["
199                                               + deprecatedOwnerId
200                                               + "] so it will be overwritten." );
201                    copyFile( context, file, targetFile, targetFilename,
202                              false );
203                }
204     
205                public void supersededUnknownOwner( String ownerId,
206                                                    String targetFilename,
207                                                    String unknownOwnerId )
208                    throws IOException
209                {
210                    // CHECKSTYLE_OFF: LineLength
211                    context.getLog().warn( "File ["
212                                               + targetFilename
213                                               + "] belonged to overlay ["
214                                               + unknownOwnerId
215                                               + "] which does not exist anymore in the current project. It is recommended to invoke "
216                                               + "clean if the dependencies of the project changed." );
217                    // CHECKSTYLE_ON: LineLength
218                    copyFile( context, file, targetFile, targetFilename,
219                              false );
220                }
221            } );
222         }
223         else if ( !targetFile.exists() && !targetFile.mkdirs() )
224         {
225             context.getLog().info( "Failed to create directory " + targetFile.getAbsolutePath() );
226         }
227     }
228 
229     /**
230      * Copy the specified file if the target location has not yet already been used and filter its content with the
231      * configured filter properties.
232      * <p/>
233      * The <tt>targetFileName</tt> is the relative path according to the root of the generated web application.
234      *
235      * @param sourceId the source id
236      * @param context the context to use
237      * @param file the file to copy
238      * @param targetFilename the relative path according to the root of the webapp
239      * @return true if the file has been copied, false otherwise
240      * @throws IOException if an error occurred while copying
241      * @throws MojoExecutionException if an error occurred while retrieving the filter properties
242      */
243     protected boolean copyFilteredFile( String sourceId, final WarPackagingContext context, File file,
244                                         String targetFilename )
245         throws IOException, MojoExecutionException
246     {
247 
248         if ( context.getWebappStructure().registerFile( sourceId, targetFilename ) )
249         {
250             final File targetFile = new File( context.getWebappDirectory(), targetFilename );
251             final String encoding;
252             try
253             {
254                 if ( isXmlFile( file ) )
255                 {
256                     // For xml-files we extract the encoding from the files
257                     encoding = getEncoding( file );
258                 }
259                 else
260                 {
261                     // For all others we use the configured encoding
262                     encoding = context.getResourceEncoding();
263                 }
264                 // fix for MWAR-36, ensures that the parent dir are created first
265                 targetFile.getParentFile().mkdirs();
266 
267                 context.getMavenFileFilter().copyFile( file, targetFile, true, context.getFilterWrappers(), encoding );
268             }
269             catch ( MavenFilteringException e )
270             {
271                 throw new MojoExecutionException( e.getMessage(), e );
272             }
273             // CHECKSTYLE_OFF: LineLength
274             // Add the file to the protected list
275             context.getLog().debug( " + " + targetFilename + " has been copied (filtered encoding='" + encoding + "')." );
276             // CHECKSTYLE_ON: LineLength
277             return true;
278         }
279         else
280         {
281             context.getLog().debug( " - " + targetFilename
282                                         + " wasn't copied because it has already been packaged (filtered)." );
283             return false;
284         }
285     }
286 
287     /**
288      * Unpacks the specified file to the specified directory.
289      *
290      * @param context the packaging context
291      * @param file the file to unpack
292      * @param unpackDirectory the directory to use for th unpacked file
293      * @throws MojoExecutionException if an error occurred while unpacking the file
294      */
295     protected void doUnpack( WarPackagingContext context, File file, File unpackDirectory )
296         throws MojoExecutionException
297     {
298         String archiveExt = FileUtils.getExtension( file.getAbsolutePath() ).toLowerCase();
299 
300         try
301         {
302             UnArchiver unArchiver = context.getArchiverManager().getUnArchiver( archiveExt );
303             unArchiver.setSourceFile( file );
304             unArchiver.setUseJvmChmod( context.isUseJvmChmod() );
305             unArchiver.setDestDirectory( unpackDirectory );
306             unArchiver.setOverwrite( true );
307             unArchiver.extract();
308         }
309         catch ( ArchiverException e )
310         {
311             throw new MojoExecutionException( "Error unpacking file [" + file.getAbsolutePath() + "]" + "to ["
312                 + unpackDirectory.getAbsolutePath() + "]", e );
313         }
314         catch ( NoSuchArchiverException e )
315         {
316             context.getLog().warn( "Skip unpacking dependency file [" + file.getAbsolutePath()
317                                        + " with unknown extension [" + archiveExt + "]" );
318         }
319     }
320 
321     /**
322      * Copy file from source to destination. The directories up to <code>destination</code> will be created if they
323      * don't already exist. if the <code>onlyIfModified</code> flag is <tt>false</tt>, <code>destination</code> will be
324      * overwritten if it already exists. If the flag is <tt>true</tt> destination will be overwritten if it's not up to
325      * date.
326      * <p/>
327      *
328      * @param context the packaging context
329      * @param source an existing non-directory <code>File</code> to copy bytes from
330      * @param destination a non-directory <code>File</code> to write bytes to (possibly overwriting).
331      * @param targetFilename the relative path of the file from the webapp root directory
332      * @param onlyIfModified if true, copy the file only if the source has changed, always copy otherwise
333      * @return true if the file has been copied/updated, false otherwise
334      * @throws IOException if <code>source</code> does not exist, <code>destination</code> cannot be written to, or an
335      *             IO error occurs during copying
336      */
337     protected boolean copyFile( WarPackagingContext context, File source, File destination, String targetFilename,
338                                 boolean onlyIfModified )
339         throws IOException
340     {
341         if ( onlyIfModified && destination.lastModified() >= source.lastModified() )
342         {
343             context.getLog().debug( " * " + targetFilename + " is up to date." );
344             return false;
345         }
346         else
347         {
348             if ( source.isDirectory() )
349             {
350                 context.getLog().warn( " + " + targetFilename + " is packaged from the source folder" );
351 
352                 try
353                 {
354                     JarArchiver archiver = context.getJarArchiver();
355                     archiver.addDirectory( source );
356                     archiver.setDestFile( destination );
357                     archiver.createArchive();
358                 }
359                 catch ( ArchiverException e )
360                 {
361                     String msg = "Failed to create " + targetFilename;
362                     context.getLog().error( msg, e );
363                     IOException ioe = new IOException( msg );
364                     ioe.initCause( e );
365                     throw ioe;
366                 }
367             }
368             else
369             {
370                 FileUtils.copyFile( source.getCanonicalFile(), destination );
371                 // preserve timestamp
372                 destination.setLastModified( source.lastModified() );
373                 context.getLog().debug( " + " + targetFilename + " has been copied." );
374             }
375             return true;
376         }
377     }
378 
379     /**
380      * Get the encoding from an XML-file.
381      *
382      * @param webXml the XML-file
383      * @return The encoding of the XML-file, or UTF-8 if it's not specified in the file
384      * @throws java.io.IOException if an error occurred while reading the file
385      */
386     protected String getEncoding( File webXml )
387         throws IOException
388     {
389         XmlStreamReader xmlReader = new XmlStreamReader( webXml );
390         try
391         {
392             return xmlReader.getEncoding();
393         }
394         finally
395         {
396             IOUtil.close( xmlReader );
397         }
398     }
399 
400     /**
401      * Returns the file to copy. If the includes are <tt>null</tt> or empty, the default includes are used.
402      *
403      * @param baseDir the base directory to start from
404      * @param includes the includes
405      * @param excludes the excludes
406      * @return the files to copy
407      */
408     protected PathSet getFilesToIncludes( File baseDir, String[] includes, String[] excludes )
409     {
410         return getFilesToIncludes( baseDir, includes, excludes, false );
411     }
412 
413     /**
414      * Returns the file to copy. If the includes are <tt>null</tt> or empty, the default includes are used.
415      *
416      * @param baseDir the base directory to start from
417      * @param includes the includes
418      * @param excludes the excludes
419      * @param includeDirectories include directories yes or not.
420      * @return the files to copy
421      */
422     // CHECKSTYLE_OFF: LineLength
423     protected PathSet getFilesToIncludes( File baseDir, String[] includes, String[] excludes, boolean includeDirectories )
424     // CHECKSTYLE_ON: LineLength
425     {
426         final DirectoryScanner scanner = new DirectoryScanner();
427         scanner.setBasedir( baseDir );
428 
429         if ( excludes != null )
430         {
431             scanner.setExcludes( excludes );
432         }
433         scanner.addDefaultExcludes();
434 
435         if ( includes != null && includes.length > 0 )
436         {
437             scanner.setIncludes( includes );
438         }
439         else
440         {
441             scanner.setIncludes( DEFAULT_INCLUDES );
442         }
443 
444         scanner.scan();
445 
446         PathSet pathSet = new PathSet( scanner.getIncludedFiles() );
447 
448         if ( includeDirectories )
449         {
450             pathSet.addAll( scanner.getIncludedDirectories() );
451         }
452 
453         return pathSet;
454     }
455 
456     /**
457      * Returns the final name of the specified artifact.
458      * <p/>
459      * If the <tt>outputFileNameMapping</tt> is set, it is used, otherwise the standard naming scheme is used.
460      *
461      * @param context the packaging context
462      * @param artifact the artifact
463      * @return the converted filename of the artifact
464      * @throws InterpolationException in case of interpolation problem.
465      */
466     protected String getArtifactFinalName( WarPackagingContext context, Artifact artifact )
467         throws InterpolationException
468     {
469         if ( context.getOutputFileNameMapping() != null )
470         {
471             return MappingUtils.evaluateFileNameMapping( context.getOutputFileNameMapping(), artifact );
472         }
473 
474         String classifier = artifact.getClassifier();
475         if ( ( classifier != null ) && !( "".equals( classifier.trim() ) ) )
476         {
477             return MappingUtils.evaluateFileNameMapping( MappingUtils.DEFAULT_FILE_NAME_MAPPING_CLASSIFIER, artifact );
478         }
479         else
480         {
481             return MappingUtils.evaluateFileNameMapping( MappingUtils.DEFAULT_FILE_NAME_MAPPING, artifact );
482         }
483 
484     }
485 
486     /**
487      * Returns <code>true</code> if the <code>File</code>-object is a file (not a directory) that is not
488      * <code>null</code> and has a file name that ends in ".xml".
489      *
490      * @param file The file to check
491      * @return <code>true</code> if the file is an xml-file, otherwise <code>false</code>
492      * @since 2.3
493      */
494     private boolean isXmlFile( File file )
495     {
496         return file != null && file.isFile() && file.getName().endsWith( ".xml" );
497     }
498 }