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 }