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