View Javadoc
1   package org.apache.maven.plugins.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.plugins.war.util.PathSet;
29  import org.apache.maven.plugins.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.java 1756988 2016-08-20 10:50:13Z 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       * 
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       * 
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      * 
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      * 
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      * 
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      *
327      * @param context the packaging context
328      * @param source an existing non-directory <code>File</code> to copy bytes from
329      * @param destination a non-directory <code>File</code> to write bytes to (possibly overwriting).
330      * @param targetFilename the relative path of the file from the webapp root directory
331      * @param onlyIfModified if true, copy the file only if the source has changed, always copy otherwise
332      * @return true if the file has been copied/updated, false otherwise
333      * @throws IOException if <code>source</code> does not exist, <code>destination</code> cannot be written to, or an
334      *             IO error occurs during copying
335      */
336     protected boolean copyFile( WarPackagingContext context, File source, File destination, String targetFilename,
337                                 boolean onlyIfModified )
338         throws IOException
339     {
340         if ( onlyIfModified && destination.lastModified() >= source.lastModified() )
341         {
342             context.getLog().debug( " * " + targetFilename + " is up to date." );
343             return false;
344         }
345         else
346         {
347             if ( source.isDirectory() )
348             {
349                 context.getLog().warn( " + " + targetFilename + " is packaged from the source folder" );
350 
351                 try
352                 {
353                     JarArchiver archiver = context.getJarArchiver();
354                     archiver.addDirectory( source );
355                     archiver.setDestFile( destination );
356                     archiver.createArchive();
357                 }
358                 catch ( ArchiverException e )
359                 {
360                     String msg = "Failed to create " + targetFilename;
361                     context.getLog().error( msg, e );
362                     IOException ioe = new IOException( msg );
363                     ioe.initCause( e );
364                     throw ioe;
365                 }
366             }
367             else
368             {
369                 FileUtils.copyFile( source.getCanonicalFile(), destination );
370                 // preserve timestamp
371                 destination.setLastModified( source.lastModified() );
372                 context.getLog().debug( " + " + targetFilename + " has been copied." );
373             }
374             return true;
375         }
376     }
377 
378     /**
379      * Get the encoding from an XML-file.
380      *
381      * @param webXml the XML-file
382      * @return The encoding of the XML-file, or UTF-8 if it's not specified in the file
383      * @throws java.io.IOException if an error occurred while reading the file
384      */
385     protected String getEncoding( File webXml )
386         throws IOException
387     {
388         XmlStreamReader xmlReader = new XmlStreamReader( webXml );
389         try
390         {
391             return xmlReader.getEncoding();
392         }
393         finally
394         {
395             IOUtil.close( xmlReader );
396         }
397     }
398 
399     /**
400      * Returns the file to copy. If the includes are <tt>null</tt> or empty, the default includes are used.
401      *
402      * @param baseDir the base directory to start from
403      * @param includes the includes
404      * @param excludes the excludes
405      * @return the files to copy
406      */
407     protected PathSet getFilesToIncludes( File baseDir, String[] includes, String[] excludes )
408     {
409         return getFilesToIncludes( baseDir, includes, excludes, false );
410     }
411 
412     /**
413      * Returns the file to copy. If the includes are <tt>null</tt> or empty, the default includes are used.
414      *
415      * @param baseDir the base directory to start from
416      * @param includes the includes
417      * @param excludes the excludes
418      * @param includeDirectories include directories yes or not.
419      * @return the files to copy
420      */
421     // CHECKSTYLE_OFF: LineLength
422     protected PathSet getFilesToIncludes( File baseDir, String[] includes, String[] excludes, boolean includeDirectories )
423     // CHECKSTYLE_ON: LineLength
424     {
425         final DirectoryScanner scanner = new DirectoryScanner();
426         scanner.setBasedir( baseDir );
427 
428         if ( excludes != null )
429         {
430             scanner.setExcludes( excludes );
431         }
432         scanner.addDefaultExcludes();
433 
434         if ( includes != null && includes.length > 0 )
435         {
436             scanner.setIncludes( includes );
437         }
438         else
439         {
440             scanner.setIncludes( DEFAULT_INCLUDES );
441         }
442 
443         scanner.scan();
444 
445         PathSet pathSet = new PathSet( scanner.getIncludedFiles() );
446 
447         if ( includeDirectories )
448         {
449             pathSet.addAll( scanner.getIncludedDirectories() );
450         }
451 
452         return pathSet;
453     }
454 
455     /**
456      * Returns the final name of the specified artifact.
457      * 
458      * If the <tt>outputFileNameMapping</tt> is set, it is used, otherwise the standard naming scheme is used.
459      *
460      * @param context the packaging context
461      * @param artifact the artifact
462      * @return the converted filename of the artifact
463      * @throws InterpolationException in case of interpolation problem.
464      */
465     protected String getArtifactFinalName( WarPackagingContext context, Artifact artifact )
466         throws InterpolationException
467     {
468         if ( context.getOutputFileNameMapping() != null )
469         {
470             return MappingUtils.evaluateFileNameMapping( context.getOutputFileNameMapping(), artifact );
471         }
472 
473         String classifier = artifact.getClassifier();
474         if ( ( classifier != null ) && !( "".equals( classifier.trim() ) ) )
475         {
476             return MappingUtils.evaluateFileNameMapping( MappingUtils.DEFAULT_FILE_NAME_MAPPING_CLASSIFIER, artifact );
477         }
478         else
479         {
480             return MappingUtils.evaluateFileNameMapping( MappingUtils.DEFAULT_FILE_NAME_MAPPING, artifact );
481         }
482 
483     }
484 
485     /**
486      * Returns <code>true</code> if the <code>File</code>-object is a file (not a directory) that is not
487      * <code>null</code> and has a file name that ends in ".xml".
488      *
489      * @param file The file to check
490      * @return <code>true</code> if the file is an xml-file, otherwise <code>false</code>
491      * @since 2.3
492      */
493     private boolean isXmlFile( File file )
494     {
495         return file != null && file.isFile() && file.getName().endsWith( ".xml" );
496     }
497 }