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