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