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 }