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 }