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 }