View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.war.packaging;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.nio.file.Files;
24  import java.nio.file.attribute.BasicFileAttributes;
25  import java.util.List;
26  
27  import org.apache.commons.io.input.XmlStreamReader;
28  import org.apache.maven.artifact.Artifact;
29  import org.apache.maven.plugin.MojoExecutionException;
30  import org.apache.maven.plugins.war.util.PathSet;
31  import org.apache.maven.plugins.war.util.WebappStructure;
32  import org.apache.maven.shared.filtering.MavenFilteringException;
33  import org.apache.maven.shared.mapping.MappingUtils;
34  import org.apache.maven.shared.utils.StringUtils;
35  import org.codehaus.plexus.archiver.ArchiverException;
36  import org.codehaus.plexus.archiver.UnArchiver;
37  import org.codehaus.plexus.archiver.jar.JarArchiver;
38  import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
39  import org.codehaus.plexus.interpolation.InterpolationException;
40  import org.codehaus.plexus.util.DirectoryScanner;
41  import org.codehaus.plexus.util.FileUtils;
42  import org.codehaus.plexus.util.SelectorUtils;
43  
44  /**
45   * @author Stephane Nicoll
46   */
47  public abstract class AbstractWarPackagingTask implements WarPackagingTask {
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 {@code targetPrefix}
80       * parameter to specify in which particular directory the files should be copied. Use {@code null} 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 {@code sourceFilesSet} 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(
93              String sourceId,
94              WarPackagingContext context,
95              File sourceBaseDir,
96              PathSet sourceFilesSet,
97              String targetPrefix,
98              boolean filtered)
99              throws IOException, MojoExecutionException {
100         for (String fileToCopyName : sourceFilesSet.paths()) {
101             final File sourceFile = new File(sourceBaseDir, fileToCopyName);
102 
103             String destinationFileName;
104             if (targetPrefix == null) {
105                 destinationFileName = fileToCopyName;
106             } else {
107                 destinationFileName = targetPrefix + fileToCopyName;
108             }
109 
110             if (filtered && !context.isNonFilteredExtension(sourceFile.getName())) {
111                 copyFilteredFile(sourceId, context, sourceFile, destinationFileName);
112             } else {
113                 copyFile(sourceId, context, sourceFile, destinationFileName);
114             }
115         }
116     }
117 
118     /**
119      * Copies the files if possible as is.
120      *
121      * Copy uses a first-win strategy: files that have already been copied by previous tasks are ignored. This method
122      * makes sure to update the list of protected files which gives the list of files that have already been copied.
123      *
124      * @param sourceId the source id
125      * @param context the context to use
126      * @param sourceBaseDir the base directory from which the {@code sourceFilesSet} will be copied
127      * @param sourceFilesSet the files to be copied
128      * @param filtered filter or not.
129      * @throws IOException if an error occurred while copying the files
130      * @throws MojoExecutionException break the build.
131      */
132     protected void copyFiles(
133             String sourceId, WarPackagingContext context, File sourceBaseDir, PathSet sourceFilesSet, boolean filtered)
134             throws IOException, MojoExecutionException {
135         copyFiles(sourceId, context, sourceBaseDir, sourceFilesSet, null, filtered);
136     }
137 
138     /**
139      * Copy the specified file if the target location has not yet already been used.
140      *
141      * The {@code targetFileName} is the relative path according to the root of the generated web application.
142      *
143      * @param sourceId the source id
144      * @param context the context to use
145      * @param file the file to copy
146      * @param targetFilename the relative path according to the root of the webapp
147      * @throws IOException if an error occurred while copying
148      */
149     // CHECKSTYLE_OFF: LineLength
150     protected void copyFile(String sourceId, final WarPackagingContext context, final File file, String targetFilename)
151             throws IOException
152                 // CHECKSTYLE_ON: LineLength
153             {
154         if (isExcluded(targetFilename, context.getPackagingIncludes(), context.getPackagingExcludes())) {
155             context.getLog().debug("Skipping excluded file: " + targetFilename);
156             return;
157         }
158         final File targetFile = new File(context.getWebappDirectory(), targetFilename);
159 
160         if (file.isFile()) {
161             context.getWebappStructure()
162                     .registerFile(sourceId, targetFilename, new WebappStructure.RegistrationCallback() {
163                         public void registered(String ownerId, String targetFilename) throws IOException {
164                             copyFile(context, file, targetFile, targetFilename, false);
165                         }
166 
167                         public void alreadyRegistered(String ownerId, String targetFilename) throws IOException {
168                             copyFile(context, file, targetFile, targetFilename, true);
169                         }
170 
171                         public void refused(String ownerId, String targetFilename, String actualOwnerId)
172                                 throws IOException {
173                             context.getLog()
174                                     .debug(" - "
175                                             + targetFilename
176                                             + " wasn't copied because it has "
177                                             + "already been packaged for overlay ["
178                                             + actualOwnerId + "].");
179                         }
180 
181                         public void superseded(String ownerId, String targetFilename, String deprecatedOwnerId)
182                                 throws IOException {
183                             context.getLog()
184                                     .info("File ["
185                                             + targetFilename
186                                             + "] belonged to overlay ["
187                                             + deprecatedOwnerId
188                                             + "] so it will be overwritten.");
189                             copyFile(context, file, targetFile, targetFilename, false);
190                         }
191 
192                         public void supersededUnknownOwner(String ownerId, String targetFilename, String unknownOwnerId)
193                                 throws IOException {
194                             // CHECKSTYLE_OFF: LineLength
195                             context.getLog()
196                                     .warn("File ["
197                                             + targetFilename
198                                             + "] belonged to overlay ["
199                                             + unknownOwnerId
200                                             + "] which does not exist anymore in the current project. It is recommended to invoke "
201                                             + "clean if the dependencies of the project changed.");
202                             // CHECKSTYLE_ON: LineLength
203                             copyFile(context, file, targetFile, targetFilename, false);
204                         }
205                     });
206         } else if (!targetFile.exists() && !targetFile.mkdirs()) {
207             context.getLog().info("Failed to create directory " + targetFile.getAbsolutePath());
208         }
209     }
210 
211     /**
212      * Copy the specified file if the target location has not yet already been used and filter its content with the
213      * configured filter properties.
214      *
215      * The {@code targetFileName} is the relative path according to the root of the generated web application.
216      *
217      * @param sourceId the source id
218      * @param context the context to use
219      * @param file the file to copy
220      * @param targetFilename the relative path according to the root of the webapp
221      * @return true if the file has been copied, false otherwise
222      * @throws IOException if an error occurred while copying
223      * @throws MojoExecutionException if an error occurred while retrieving the filter properties
224      */
225     protected boolean copyFilteredFile(
226             String sourceId, final WarPackagingContext context, File file, String targetFilename)
227             throws IOException, MojoExecutionException {
228         context.addResource(targetFilename);
229 
230         if (context.getWebappStructure().registerFile(sourceId, targetFilename)) {
231             final File targetFile = new File(context.getWebappDirectory(), targetFilename);
232             final String encoding;
233             try {
234                 if (isXmlFile(file)) {
235                     // For xml-files we extract the encoding from the files
236                     encoding = getEncoding(file);
237                 } else if (isPropertiesFile(file) && StringUtils.isNotEmpty(context.getPropertiesEncoding())) {
238                     encoding = context.getPropertiesEncoding();
239                 } else {
240                     // For all others we use the configured encoding
241                     encoding = context.getResourceEncoding();
242                 }
243                 // fix for MWAR-36, ensures that the parent dir are created first
244                 targetFile.getParentFile().mkdirs();
245 
246                 context.getMavenFileFilter().copyFile(file, targetFile, true, context.getFilterWrappers(), encoding);
247             } catch (MavenFilteringException e) {
248                 throw new MojoExecutionException(e.getMessage(), e);
249             }
250             // CHECKSTYLE_OFF: LineLength
251             // Add the file to the protected list
252             context.getLog().debug(" + " + targetFilename + " has been copied (filtered encoding='" + encoding + "').");
253             // CHECKSTYLE_ON: LineLength
254             return true;
255         } else {
256             context.getLog()
257                     .debug(" - " + targetFilename + " 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         String archiveExt = FileUtils.getExtension(file.getAbsolutePath()).toLowerCase();
273 
274         try {
275             UnArchiver unArchiver = context.getArchiverManager().getUnArchiver(archiveExt);
276             unArchiver.setSourceFile(file);
277             unArchiver.setDestDirectory(unpackDirectory);
278             unArchiver.setOverwrite(true);
279             unArchiver.extract();
280         } catch (ArchiverException e) {
281             throw new MojoExecutionException(
282                     "Error unpacking file [" + file.getAbsolutePath() + "]" + " to ["
283                             + unpackDirectory.getAbsolutePath() + "]",
284                     e);
285         } catch (NoSuchArchiverException e) {
286             context.getLog()
287                     .warn("Skip unpacking dependency file [" + file.getAbsolutePath() + " with unknown extension ["
288                             + archiveExt + "]");
289         }
290     }
291 
292     /**
293      * Copy file from source to destination. The directories up to <code>destination</code> will be created if they
294      * don't already exist. if the <code>onlyIfModified</code> flag is {@code false}, <code>destination</code> will be
295      * overwritten if it already exists. If the flag is {@code true} destination will be overwritten if it's not up to
296      * date.
297      *
298      * @param context the packaging context
299      * @param source an existing non-directory <code>File</code> to copy bytes from
300      * @param destination a non-directory <code>File</code> to write bytes to (possibly overwriting).
301      * @param targetFilename the relative path of the file from the webapp root directory
302      * @param onlyIfModified if true, copy the file only if the source has changed, always copy otherwise
303      * @return true if the file has been copied/updated, false otherwise
304      * @throws IOException if <code>source</code> does not exist, <code>destination</code> cannot be written to, or an
305      *             IO error occurs during copying
306      */
307     protected boolean copyFile(
308             WarPackagingContext context, File source, File destination, String targetFilename, boolean onlyIfModified)
309             throws IOException {
310         context.addResource(targetFilename);
311 
312         BasicFileAttributes readAttributes = Files.readAttributes(source.toPath(), BasicFileAttributes.class);
313         if (onlyIfModified
314                 && destination.lastModified()
315                         >= readAttributes.lastModifiedTime().toMillis()) {
316             context.getLog().debug(" * " + targetFilename + " is up to date.");
317             return false;
318         } else {
319             if (readAttributes.isDirectory()) {
320                 context.getLog().warn(" + " + targetFilename + " is packaged from the source folder");
321 
322                 try {
323                     JarArchiver archiver = context.getJarArchiver();
324                     archiver.addDirectory(source);
325                     archiver.setDestFile(destination);
326                     archiver.createArchive();
327                 } catch (ArchiverException e) {
328                     String msg = "Failed to create " + targetFilename;
329                     context.getLog().error(msg, e);
330                     throw new IOException(msg, e);
331                 }
332             } else {
333                 FileUtils.copyFile(source.getCanonicalFile(), destination);
334                 // preserve timestamp
335                 destination.setLastModified(readAttributes.lastModifiedTime().toMillis());
336                 context.getLog().debug(" + " + targetFilename + " has been copied.");
337             }
338             return true;
339         }
340     }
341 
342     /**
343      * Get the encoding from an XML-file.
344      *
345      * @param webXml the XML-file
346      * @return The encoding of the XML-file, or UTF-8 if it's not specified in the file
347      * @throws java.io.IOException if an error occurred while reading the file
348      */
349     protected String getEncoding(File webXml) throws IOException {
350         try (XmlStreamReader xmlReader = new XmlStreamReader(webXml)) {
351             return xmlReader.getEncoding();
352         }
353     }
354 
355     /**
356      * Returns the file to copy. If the includes are {@code null} or empty, the default includes are used.
357      *
358      * @param baseDir the base directory to start from
359      * @param includes the includes
360      * @param excludes the excludes
361      * @return the files to copy
362      */
363     protected PathSet getFilesToIncludes(File baseDir, String[] includes, String[] excludes) {
364         return getFilesToIncludes(baseDir, includes, excludes, false);
365     }
366 
367     /**
368      * Returns the file to copy. If the includes are {@code null} or empty, the default includes are used.
369      *
370      * @param baseDir the base directory to start from
371      * @param includes the includes
372      * @param excludes the excludes
373      * @param includeDirectories include directories yes or not.
374      * @return the files to copy
375      */
376     // CHECKSTYLE_OFF: LineLength
377     protected PathSet getFilesToIncludes(
378             File baseDir, String[] includes, String[] excludes, boolean includeDirectories)
379                 // CHECKSTYLE_ON: LineLength
380             {
381         final DirectoryScanner scanner = new DirectoryScanner();
382         scanner.setBasedir(baseDir);
383 
384         if (excludes != null) {
385             scanner.setExcludes(excludes);
386         }
387         scanner.addDefaultExcludes();
388 
389         if (includes != null && includes.length > 0) {
390             scanner.setIncludes(includes);
391         } else {
392             scanner.setIncludes(DEFAULT_INCLUDES);
393         }
394 
395         scanner.scan();
396 
397         PathSet pathSet = new PathSet(scanner.getIncludedFiles());
398 
399         if (includeDirectories) {
400             pathSet.addAll(scanner.getIncludedDirectories());
401         }
402 
403         return pathSet;
404     }
405 
406     /**
407      * Returns the final name of the specified artifact.
408      *
409      * If the {@code outputFileNameMapping} is set, it is used, otherwise the standard naming scheme is used.
410      *
411      * @param context the packaging context
412      * @param artifact the artifact
413      * @return the converted filename of the artifact
414      * @throws InterpolationException in case of interpolation problem.
415      */
416     protected String getArtifactFinalName(WarPackagingContext context, Artifact artifact)
417             throws InterpolationException {
418         if (context.getOutputFileNameMapping() != null) {
419             return MappingUtils.evaluateFileNameMapping(context.getOutputFileNameMapping(), artifact);
420         }
421 
422         String classifier = artifact.getClassifier();
423         if ((classifier != null) && !("".equals(classifier.trim()))) {
424             return MappingUtils.evaluateFileNameMapping(MappingUtils.DEFAULT_FILE_NAME_MAPPING_CLASSIFIER, artifact);
425         } else {
426             return MappingUtils.evaluateFileNameMapping(MappingUtils.DEFAULT_FILE_NAME_MAPPING, artifact);
427         }
428     }
429 
430     /**
431      * Determine whether a file is of a certain type, by looking at the file extension.
432      *
433      * @param file The file to check
434      * @param extension The extension for a file type, including the '.'
435      * @return <code>true</code> if the file is a file of the specified type, otherwise <code>false</code>
436      * @since 3.4.0
437      */
438     private boolean isFileOfType(File file, String extension) {
439         return file != null && file.isFile() && file.getName().endsWith(extension);
440     }
441 
442     /**
443      * Determine whether a file is a properties file or not.
444      *
445      * @param file The file to check
446      * @return <code>true</code> if the file is a properties file, otherwise <code>false</code>
447      * @since 3.4.0
448      */
449     private boolean isPropertiesFile(File file) {
450         return isFileOfType(file, ".properties");
451     }
452 
453     /**
454      * Returns <code>true</code> if the <code>File</code>-object is a file (not a directory) that is not
455      * <code>null</code> and has a file name that ends in ".xml".
456      *
457      * @param file The file to check
458      * @return <code>true</code> if the file is an xml-file, otherwise <code>false</code>
459      * @since 2.3
460      */
461     private boolean isXmlFile(File file) {
462         return isFileOfType(file, ".xml");
463     }
464 
465     /**
466      * Check whether the specified file is excluded or not.
467      *
468      * @param targetFilename the target filename
469      * @param packagingIncludes the includes
470      * @param packagingExcludes the excludes
471      * @return true if the file is excluded
472      */
473     private boolean isExcluded(String targetFilename, List<String> packagingIncludes, List<String> packagingExcludes) {
474         for (String exclude : packagingExcludes) {
475             if (SelectorUtils.matchPath(exclude.trim(), targetFilename)) {
476                 return true;
477             }
478         }
479         for (String include : packagingIncludes) {
480             if (SelectorUtils.matchPath(include.trim(), targetFilename)) {
481                 return false;
482             }
483         }
484         return true;
485     }
486 }