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