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         copyFile(sourceId, context, file, targetFilename, targetFilename);
153     }
154 
155     /**
156      * Adds originalFilename for exclusion checks.
157      */
158     void copyFile(
159             String sourceId,
160             final WarPackagingContext context,
161             final File file,
162             String targetFilename,
163             String originalFilename)
164             throws IOException
165                 // CHECKSTYLE_ON: LineLength
166             {
167         if (isExcluded(originalFilename, context.getPackagingIncludes(), context.getPackagingExcludes())) {
168             context.getLog().debug("Skipping excluded file: " + targetFilename);
169             return;
170         }
171         final File targetFile = new File(context.getWebappDirectory(), targetFilename);
172 
173         if (file.isFile()) {
174             context.getWebappStructure()
175                     .registerFile(sourceId, targetFilename, new WebappStructure.RegistrationCallback() {
176                         public void registered(String ownerId, String targetFilename) throws IOException {
177                             copyFile(context, file, targetFile, targetFilename, false);
178                         }
179 
180                         public void alreadyRegistered(String ownerId, String targetFilename) throws IOException {
181                             copyFile(context, file, targetFile, targetFilename, true);
182                         }
183 
184                         public void refused(String ownerId, String targetFilename, String actualOwnerId)
185                                 throws IOException {
186                             context.getLog()
187                                     .debug(" - "
188                                             + targetFilename
189                                             + " wasn't copied because it has "
190                                             + "already been packaged for overlay ["
191                                             + actualOwnerId + "].");
192                         }
193 
194                         public void superseded(String ownerId, String targetFilename, String deprecatedOwnerId)
195                                 throws IOException {
196                             context.getLog()
197                                     .info("File ["
198                                             + targetFilename
199                                             + "] belonged to overlay ["
200                                             + deprecatedOwnerId
201                                             + "] so it will be overwritten.");
202                             copyFile(context, file, targetFile, targetFilename, false);
203                         }
204 
205                         public void supersededUnknownOwner(String ownerId, String targetFilename, String unknownOwnerId)
206                                 throws IOException {
207                             // CHECKSTYLE_OFF: LineLength
208                             context.getLog()
209                                     .warn("File ["
210                                             + targetFilename
211                                             + "] belonged to overlay ["
212                                             + unknownOwnerId
213                                             + "] which does not exist anymore in the current project. It is recommended to invoke "
214                                             + "clean if the dependencies of the project changed.");
215                             // CHECKSTYLE_ON: LineLength
216                             copyFile(context, file, targetFile, targetFilename, false);
217                         }
218                     });
219         } else if (!targetFile.exists() && !targetFile.mkdirs()) {
220             context.getLog().info("Failed to create directory " + targetFile.getAbsolutePath());
221         }
222     }
223 
224     /**
225      * Copy the specified file if the target location has not yet already been used and filter its content with the
226      * configured filter properties.
227      *
228      * The {@code targetFileName} is the relative path according to the root of the generated web application.
229      *
230      * @param sourceId the source id
231      * @param context the context to use
232      * @param file the file to copy
233      * @param targetFilename the relative path according to the root of the webapp
234      * @return true if the file has been copied, false otherwise
235      * @throws IOException if an error occurred while copying
236      * @throws MojoExecutionException if an error occurred while retrieving the filter properties
237      */
238     protected boolean copyFilteredFile(
239             String sourceId, final WarPackagingContext context, File file, String targetFilename)
240             throws IOException, MojoExecutionException {
241         context.addResource(targetFilename);
242 
243         if (context.getWebappStructure().registerFile(sourceId, targetFilename)) {
244             final File targetFile = new File(context.getWebappDirectory(), targetFilename);
245             final String encoding;
246             try {
247                 if (isXmlFile(file)) {
248                     // For xml-files we extract the encoding from the files
249                     encoding = getEncoding(file);
250                 } else if (isPropertiesFile(file) && StringUtils.isNotEmpty(context.getPropertiesEncoding())) {
251                     encoding = context.getPropertiesEncoding();
252                 } else {
253                     // For all others we use the configured encoding
254                     encoding = context.getResourceEncoding();
255                 }
256                 // fix for MWAR-36, ensures that the parent dir are created first
257                 targetFile.getParentFile().mkdirs();
258 
259                 context.getMavenFileFilter().copyFile(file, targetFile, true, context.getFilterWrappers(), encoding);
260             } catch (MavenFilteringException e) {
261                 throw new MojoExecutionException(e.getMessage(), e);
262             }
263             // CHECKSTYLE_OFF: LineLength
264             // Add the file to the protected list
265             context.getLog().debug(" + " + targetFilename + " has been copied (filtered encoding='" + encoding + "').");
266             // CHECKSTYLE_ON: LineLength
267             return true;
268         } else {
269             context.getLog()
270                     .debug(" - " + targetFilename + " wasn't copied because it has already been packaged (filtered).");
271             return false;
272         }
273     }
274 
275     /**
276      * Unpacks the specified file to the specified directory.
277      *
278      * @param context the packaging context
279      * @param file the file to unpack
280      * @param unpackDirectory the directory to use for th unpacked file
281      * @throws MojoExecutionException if an error occurred while unpacking the file
282      */
283     protected void doUnpack(WarPackagingContext context, File file, File unpackDirectory)
284             throws MojoExecutionException {
285         String archiveExt = FileUtils.getExtension(file.getAbsolutePath()).toLowerCase();
286 
287         try {
288             UnArchiver unArchiver = context.getArchiverManager().getUnArchiver(archiveExt);
289             unArchiver.setSourceFile(file);
290             unArchiver.setDestDirectory(unpackDirectory);
291             unArchiver.setOverwrite(true);
292             unArchiver.extract();
293         } catch (ArchiverException e) {
294             throw new MojoExecutionException(
295                     "Error unpacking file [" + file.getAbsolutePath() + "]" + " to ["
296                             + unpackDirectory.getAbsolutePath() + "]",
297                     e);
298         } catch (NoSuchArchiverException e) {
299             context.getLog()
300                     .warn("Skip unpacking dependency file [" + file.getAbsolutePath() + " with unknown extension ["
301                             + archiveExt + "]");
302         }
303     }
304 
305     /**
306      * Copy file from source to destination. The directories up to <code>destination</code> will be created if they
307      * don't already exist. if the <code>onlyIfModified</code> flag is {@code false}, <code>destination</code> will be
308      * overwritten if it already exists. If the flag is {@code true} destination will be overwritten if it's not up to
309      * date.
310      *
311      * @param context the packaging context
312      * @param source an existing non-directory <code>File</code> to copy bytes from
313      * @param destination a non-directory <code>File</code> to write bytes to (possibly overwriting)
314      * @param targetFilename the relative path of the file from the webapp root directory
315      * @param onlyIfModified if true, copy the file only if the source has changed, always copy otherwise
316      * @return true if the file has been copied/updated, false otherwise
317      * @throws IOException if <code>source</code> does not exist, <code>destination</code> cannot be written to, or an
318      *             IO error occurs during copying
319      */
320     protected boolean copyFile(
321             WarPackagingContext context, File source, File destination, String targetFilename, boolean onlyIfModified)
322             throws IOException {
323         context.addResource(targetFilename);
324 
325         BasicFileAttributes readAttributes = Files.readAttributes(source.toPath(), BasicFileAttributes.class);
326         if (onlyIfModified
327                 && destination.lastModified()
328                         >= readAttributes.lastModifiedTime().toMillis()) {
329             context.getLog().debug(" * " + targetFilename + " is up to date.");
330             return false;
331         } else {
332             if (readAttributes.isDirectory()) {
333                 context.getLog().warn(" + " + targetFilename + " is packaged from the source folder");
334 
335                 try {
336                     JarArchiver archiver = context.getJarArchiver();
337                     archiver.addDirectory(source);
338                     archiver.setDestFile(destination);
339                     archiver.createArchive();
340                 } catch (ArchiverException e) {
341                     String msg = "Failed to create " + targetFilename;
342                     context.getLog().error(msg, e);
343                     throw new IOException(msg, e);
344                 }
345             } else {
346                 FileUtils.copyFile(source.getCanonicalFile(), destination);
347                 // preserve timestamp
348                 destination.setLastModified(readAttributes.lastModifiedTime().toMillis());
349                 context.getLog().debug(" + " + targetFilename + " has been copied.");
350             }
351             return true;
352         }
353     }
354 
355     /**
356      * Get the encoding from an XML-file.
357      *
358      * @param webXml the XML-file
359      * @return the encoding of the XML-file, or UTF-8 if it's not specified in the file
360      * @throws java.io.IOException if an error occurred while reading the file
361      */
362     protected String getEncoding(File webXml) throws IOException {
363         try (XmlStreamReader xmlReader = new XmlStreamReader(webXml)) {
364             return xmlReader.getEncoding();
365         }
366     }
367 
368     /**
369      * Returns the file to copy. If the includes are {@code null} or empty, the default includes are used.
370      *
371      * @param baseDir the base directory to start from
372      * @param includes the includes
373      * @param excludes the excludes
374      * @return the files to copy
375      */
376     protected PathSet getFilesToIncludes(File baseDir, String[] includes, String[] excludes) {
377         return getFilesToIncludes(baseDir, includes, excludes, false);
378     }
379 
380     /**
381      * Returns the file to copy. If the includes are {@code null} or empty, the default includes are used.
382      *
383      * @param baseDir the base directory to start from
384      * @param includes the includes
385      * @param excludes the excludes
386      * @param includeDirectories include directories yes or not
387      * @return the files to copy
388      */
389     // CHECKSTYLE_OFF: LineLength
390     protected PathSet getFilesToIncludes(
391             File baseDir, String[] includes, String[] excludes, boolean includeDirectories)
392                 // CHECKSTYLE_ON: LineLength
393             {
394         final DirectoryScanner scanner = new DirectoryScanner();
395         scanner.setBasedir(baseDir);
396 
397         if (excludes != null) {
398             scanner.setExcludes(excludes);
399         }
400         scanner.addDefaultExcludes();
401 
402         if (includes != null && includes.length > 0) {
403             scanner.setIncludes(includes);
404         } else {
405             scanner.setIncludes(DEFAULT_INCLUDES);
406         }
407 
408         scanner.scan();
409 
410         PathSet pathSet = new PathSet(scanner.getIncludedFiles());
411 
412         if (includeDirectories) {
413             pathSet.addAll(scanner.getIncludedDirectories());
414         }
415 
416         return pathSet;
417     }
418 
419     /**
420      * Returns the final name of the specified artifact.
421      *
422      * If the {@code outputFileNameMapping} is set, it is used, otherwise the standard naming scheme is used.
423      *
424      * @param context the packaging context
425      * @param artifact the artifact
426      * @return the converted filename of the artifact
427      * @throws InterpolationException in case of interpolation problem
428      */
429     protected String getArtifactFinalName(WarPackagingContext context, Artifact artifact)
430             throws InterpolationException {
431         if (context.getOutputFileNameMapping() != null) {
432             return MappingUtils.evaluateFileNameMapping(context.getOutputFileNameMapping(), artifact);
433         }
434 
435         String classifier = artifact.getClassifier();
436         if ((classifier != null) && !("".equals(classifier.trim()))) {
437             return MappingUtils.evaluateFileNameMapping(MappingUtils.DEFAULT_FILE_NAME_MAPPING_CLASSIFIER, artifact);
438         } else {
439             return MappingUtils.evaluateFileNameMapping(MappingUtils.DEFAULT_FILE_NAME_MAPPING, artifact);
440         }
441     }
442 
443     /**
444      * Determine whether a file is of a certain type, by looking at the file extension.
445      *
446      * @param file the file to check
447      * @param extension the extension for a file type, including the '.'
448      * @return <code>true</code> if the file is a file of the specified type, otherwise <code>false</code>
449      * @since 3.4.0
450      */
451     private boolean isFileOfType(File file, String extension) {
452         return file != null && file.isFile() && file.getName().endsWith(extension);
453     }
454 
455     /**
456      * Determine whether a file is a properties file or not.
457      *
458      * @param file the file to check
459      * @return <code>true</code> if the file is a properties file, otherwise <code>false</code>
460      * @since 3.4.0
461      */
462     private boolean isPropertiesFile(File file) {
463         return isFileOfType(file, ".properties");
464     }
465 
466     /**
467      * Returns <code>true</code> if the <code>File</code>-object is a file (not a directory) that is not
468      * <code>null</code> and has a file name that ends in ".xml".
469      *
470      * @param file the file to check
471      * @return <code>true</code> if the file is an xml-file, otherwise <code>false</code>
472      * @since 2.3
473      */
474     private boolean isXmlFile(File file) {
475         return isFileOfType(file, ".xml");
476     }
477 
478     /**
479      * Check whether the specified file is excluded or not.
480      *
481      * @param targetFilename the target filename
482      * @param packagingIncludes the includes
483      * @param packagingExcludes the excludes
484      * @return true if the file is excluded
485      */
486     private boolean isExcluded(String targetFilename, List<String> packagingIncludes, List<String> packagingExcludes) {
487         for (String exclude : packagingExcludes) {
488             if (SelectorUtils.matchPath(exclude.trim(), targetFilename)) {
489                 return true;
490             }
491         }
492         for (String include : packagingIncludes) {
493             if (SelectorUtils.matchPath(include.trim(), targetFilename)) {
494                 return false;
495             }
496         }
497         return true;
498     }
499 }