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.shared.filtering;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.io.Reader;
28  import java.io.StringReader;
29  import java.io.StringWriter;
30  import java.nio.file.Path;
31  import java.nio.file.Paths;
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.List;
35  import java.util.Locale;
36  
37  import org.apache.commons.io.FilenameUtils;
38  import org.apache.commons.io.IOUtils;
39  import org.apache.commons.lang3.StringUtils;
40  import org.apache.maven.model.Resource;
41  import org.codehaus.plexus.util.DirectoryScanner;
42  import org.codehaus.plexus.util.Scanner;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  import org.sonatype.plexus.build.incremental.BuildContext;
46  
47  import static java.util.Objects.requireNonNull;
48  
49  /**
50   * @author Olivier Lamy
51   */
52  @Singleton
53  @Named
54  public class DefaultMavenResourcesFiltering implements MavenResourcesFiltering {
55      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMavenResourcesFiltering.class);
56  
57      private static final String[] EMPTY_STRING_ARRAY = {};
58  
59      private static final String[] DEFAULT_INCLUDES = {"**/**"};
60  
61      private final List<String> defaultNonFilteredFileExtensions;
62  
63      private final MavenFileFilter mavenFileFilter;
64  
65      private final BuildContext buildContext;
66  
67      @Inject
68      public DefaultMavenResourcesFiltering(MavenFileFilter mavenFileFilter, BuildContext buildContext) {
69          this.mavenFileFilter = requireNonNull(mavenFileFilter);
70          this.buildContext = requireNonNull(buildContext);
71          this.defaultNonFilteredFileExtensions = new ArrayList<>(5);
72          this.defaultNonFilteredFileExtensions.add("jpg");
73          this.defaultNonFilteredFileExtensions.add("jpeg");
74          this.defaultNonFilteredFileExtensions.add("gif");
75          this.defaultNonFilteredFileExtensions.add("bmp");
76          this.defaultNonFilteredFileExtensions.add("png");
77          this.defaultNonFilteredFileExtensions.add("ico");
78      }
79  
80      @Override
81      public boolean filteredFileExtension(String fileName, List<String> userNonFilteredFileExtensions) {
82          List<String> nonFilteredFileExtensions = new ArrayList<>(getDefaultNonFilteredFileExtensions());
83          if (userNonFilteredFileExtensions != null) {
84              nonFilteredFileExtensions.addAll(userNonFilteredFileExtensions);
85          }
86          String extension = getExtension(fileName);
87          boolean filteredFileExtension = !nonFilteredFileExtensions.contains(extension);
88          if (LOGGER.isDebugEnabled()) {
89              LOGGER.debug("file " + fileName + " has a" + (filteredFileExtension ? " " : " non ")
90                      + "filtered file extension");
91          }
92          return filteredFileExtension;
93      }
94  
95      private static String getExtension(String fileName) {
96          String rawExt = FilenameUtils.getExtension(fileName);
97          return rawExt == null ? null : rawExt.toLowerCase(Locale.ROOT);
98      }
99  
100     @Override
101     public List<String> getDefaultNonFilteredFileExtensions() {
102         return this.defaultNonFilteredFileExtensions;
103     }
104 
105     @Override
106     public void filterResources(MavenResourcesExecution mavenResourcesExecution) throws MavenFilteringException {
107         if (mavenResourcesExecution == null) {
108             throw new MavenFilteringException("mavenResourcesExecution cannot be null");
109         }
110 
111         if (mavenResourcesExecution.getResources() == null) {
112             LOGGER.info("No resources configured skip copying/filtering");
113             return;
114         }
115 
116         if (mavenResourcesExecution.getOutputDirectory() == null) {
117             throw new MavenFilteringException("outputDirectory cannot be null");
118         }
119 
120         if (mavenResourcesExecution.isUseDefaultFilterWrappers()) {
121             handleDefaultFilterWrappers(mavenResourcesExecution);
122         }
123 
124         if (mavenResourcesExecution.getEncoding() == null
125                 || mavenResourcesExecution.getEncoding().length() < 1) {
126             LOGGER.warn("Using platform encoding (" + System.getProperty("file.encoding")
127                     + " actually) to copy filtered resources, i.e. build is platform dependent!");
128         } else {
129             LOGGER.debug("Using '" + mavenResourcesExecution.getEncoding() + "' encoding to copy filtered resources.");
130         }
131 
132         if (mavenResourcesExecution.getPropertiesEncoding() == null
133                 || mavenResourcesExecution.getPropertiesEncoding().length() < 1) {
134             LOGGER.debug("Using '" + mavenResourcesExecution.getEncoding()
135                     + "' encoding to copy filtered properties files.");
136         } else {
137             LOGGER.debug("Using '" + mavenResourcesExecution.getPropertiesEncoding()
138                     + "' encoding to copy filtered properties files.");
139         }
140 
141         // Keep track of filtering being used and the properties files being filtered
142         boolean isFilteringUsed = false;
143         List<File> propertiesFiles = new ArrayList<>();
144 
145         for (Resource resource : mavenResourcesExecution.getResources()) {
146 
147             if (LOGGER.isDebugEnabled()) {
148                 String ls = System.lineSeparator();
149                 StringBuilder debugMessage = new StringBuilder("resource with targetPath ")
150                         .append(resource.getTargetPath())
151                         .append(ls);
152                 debugMessage
153                         .append("directory ")
154                         .append(resource.getDirectory())
155                         .append(ls);
156 
157                 // @formatter:off
158                 debugMessage
159                         .append("excludes ")
160                         .append(
161                                 resource.getExcludes() == null
162                                         ? " empty "
163                                         : resource.getExcludes().toString())
164                         .append(ls);
165                 debugMessage
166                         .append("includes ")
167                         .append(
168                                 resource.getIncludes() == null
169                                         ? " empty "
170                                         : resource.getIncludes().toString());
171 
172                 // @formatter:on
173                 LOGGER.debug(debugMessage.toString());
174             }
175 
176             String targetPath = resource.getTargetPath();
177 
178             File resourceDirectory = (resource.getDirectory() == null) ? null : new File(resource.getDirectory());
179 
180             if (resourceDirectory != null && !resourceDirectory.isAbsolute()) {
181                 resourceDirectory =
182                         new File(mavenResourcesExecution.getResourcesBaseDirectory(), resourceDirectory.getPath());
183             }
184 
185             if (resourceDirectory == null || !resourceDirectory.exists()) {
186                 LOGGER.info("skip non existing resourceDirectory " + resourceDirectory);
187                 continue;
188             }
189 
190             // this part is required in case the user specified "../something"
191             // as destination
192             // see MNG-1345
193             File outputDirectory = mavenResourcesExecution.getOutputDirectory();
194             if (!outputDirectory.mkdirs() && !outputDirectory.exists()) {
195                 throw new MavenFilteringException("Cannot create resource output directory: " + outputDirectory);
196             }
197 
198             if (resource.isFiltering()) {
199                 isFilteringUsed = true;
200             }
201 
202             boolean filtersFileChanged = buildContext.hasDelta(mavenResourcesExecution.getFileFilters());
203             Path resourcePath = resourceDirectory.toPath();
204             DirectoryScanner scanner = new DirectoryScanner() {
205                 @Override
206                 protected boolean isSelected(String name, File file) {
207                     if (filtersFileChanged) {
208                         // if the file filters file has a change we must assume everything is out of
209                         // date
210                         return true;
211                     }
212                     if (file.isFile()) {
213                         try {
214                             File targetFile = getTargetFile(file);
215                             if (targetFile.isFile() && buildContext.isUptodate(targetFile, file)) {
216                                 return false;
217                             }
218                         } catch (MavenFilteringException e) {
219                             // can't really do anything than to assume we must copy the file...
220                         }
221                     }
222                     return true;
223                 }
224 
225                 private File getTargetFile(File file) throws MavenFilteringException {
226                     Path relativize = resourcePath.relativize(file.toPath());
227                     return getDestinationFile(
228                             outputDirectory, targetPath, relativize.toString(), mavenResourcesExecution);
229                 }
230             };
231             scanner.setBasedir(resourceDirectory);
232 
233             setupScanner(resource, scanner, mavenResourcesExecution.isAddDefaultExcludes());
234 
235             scanner.scan();
236 
237             if (mavenResourcesExecution.isIncludeEmptyDirs()) {
238                 try {
239                     File targetDirectory = targetPath == null ? outputDirectory : new File(outputDirectory, targetPath);
240                     copyDirectoryLayout(resourceDirectory, targetDirectory, scanner);
241                 } catch (IOException e) {
242                     throw new MavenFilteringException("Cannot copy directory structure from "
243                             + resourceDirectory.getPath() + " to " + outputDirectory.getPath());
244                 }
245             }
246 
247             List<String> includedFiles = Arrays.asList(scanner.getIncludedFiles());
248 
249             try {
250                 Path basedir = mavenResourcesExecution
251                         .getMavenProject()
252                         .getBasedir()
253                         .getAbsoluteFile()
254                         .toPath();
255                 Path destination = getDestinationFile(outputDirectory, targetPath, "", mavenResourcesExecution)
256                         .getAbsoluteFile()
257                         .toPath();
258                 String origin = basedir.relativize(
259                                 resourceDirectory.getAbsoluteFile().toPath())
260                         .toString();
261                 if (StringUtils.isEmpty(origin)) {
262                     origin = ".";
263                 }
264                 LOGGER.info("Copying " + includedFiles.size() + " resource" + (includedFiles.size() > 1 ? "s" : "")
265                         + " from "
266                         + origin
267                         + " to "
268                         + basedir.relativize(destination));
269             } catch (Exception e) {
270                 // be foolproof: if for ANY reason throws, do not abort, just fall back to old message
271                 LOGGER.info("Copying " + includedFiles.size() + " resource" + (includedFiles.size() > 1 ? "s" : "")
272                         + (targetPath == null ? "" : " to " + targetPath));
273             }
274 
275             for (String name : includedFiles) {
276 
277                 LOGGER.debug("Copying file " + name);
278                 File source = new File(resourceDirectory, name);
279 
280                 File destinationFile = getDestinationFile(outputDirectory, targetPath, name, mavenResourcesExecution);
281 
282                 if (mavenResourcesExecution.isFlatten() && destinationFile.exists()) {
283                     if (mavenResourcesExecution.isOverwrite()) {
284                         LOGGER.warn("existing file " + destinationFile.getName() + " will be overwritten by " + name);
285                     } else {
286                         throw new MavenFilteringException("existing file " + destinationFile.getName()
287                                 + " will be overwritten by " + name + " and overwrite was not set to true");
288                     }
289                 }
290                 boolean filteredExt =
291                         filteredFileExtension(source.getName(), mavenResourcesExecution.getNonFilteredFileExtensions());
292                 if (resource.isFiltering() && isPropertiesFile(source)) {
293                     propertiesFiles.add(source);
294                 }
295 
296                 // Determine which encoding to use when filtering this file
297                 String encoding = getEncoding(
298                         source, mavenResourcesExecution.getEncoding(), mavenResourcesExecution.getPropertiesEncoding());
299                 LOGGER.debug("Using '" + encoding + "' encoding to copy filtered resource '" + source.getName() + "'.");
300                 mavenFileFilter.copyFile(
301                         source,
302                         destinationFile,
303                         resource.isFiltering() && filteredExt,
304                         mavenResourcesExecution.getFilterWrappers(),
305                         encoding,
306                         mavenResourcesExecution.isOverwrite());
307             }
308 
309             // deal with deleted source files
310 
311             Scanner deleteScanner = buildContext.newDeleteScanner(resourceDirectory);
312 
313             setupScanner(resource, deleteScanner, mavenResourcesExecution.isAddDefaultExcludes());
314 
315             deleteScanner.scan();
316 
317             for (String name : deleteScanner.getIncludedFiles()) {
318                 File destinationFile = getDestinationFile(outputDirectory, targetPath, name, mavenResourcesExecution);
319 
320                 destinationFile.delete();
321 
322                 buildContext.refresh(destinationFile);
323             }
324         }
325 
326         // Warn the user if all of the following requirements are met, to avoid those that are not affected
327         // - the propertiesEncoding parameter has not been set
328         // - properties is a filtered extension
329         // - filtering is enabled for at least one resource
330         // - there is at least one properties file in one of the resources that has filtering enabled
331         if ((mavenResourcesExecution.getPropertiesEncoding() == null
332                         || mavenResourcesExecution.getPropertiesEncoding().length() < 1)
333                 && !mavenResourcesExecution.getNonFilteredFileExtensions().contains("properties")
334                 && isFilteringUsed
335                 && propertiesFiles.size() > 0) {
336             // @todo Sometime in the future we should change this to be a warning
337             LOGGER.info("The encoding used to copy filtered properties files has not been set."
338                     + " This means that the same encoding will be used to copy filtered properties files"
339                     + " as when copying other filtered resources. This might not be what you want!"
340                     + " Run your build with --debug to see which files might be affected."
341                     + " Read more at "
342                     + "https://maven.apache.org/plugins/maven-resources-plugin/"
343                     + "examples/filtering-properties-files.html");
344 
345             StringBuilder affectedFiles = new StringBuilder();
346             affectedFiles.append("Here is a list of the filtered properties files in your project that might be"
347                     + " affected by encoding problems: ");
348             for (File propertiesFile : propertiesFiles) {
349                 affectedFiles.append(System.lineSeparator()).append(" - ").append(propertiesFile.getPath());
350             }
351             LOGGER.debug(affectedFiles.toString());
352         }
353     }
354 
355     /**
356      * Get the encoding to use when filtering the specified file. Properties files can be configured to use a different
357      * encoding than regular files.
358      *
359      * @param file The file to check
360      * @param encoding The encoding to use for regular files
361      * @param propertiesEncoding The encoding to use for properties files
362      * @return The encoding to use when filtering the specified file
363      * @since 3.2.0
364      */
365     static String getEncoding(File file, String encoding, String propertiesEncoding) {
366         if (isPropertiesFile(file)) {
367             if (propertiesEncoding == null) {
368                 // Since propertiesEncoding is a new feature, not all plugins will have implemented support for it.
369                 // These plugins will have propertiesEncoding set to null.
370                 return encoding;
371             } else {
372                 return propertiesEncoding;
373             }
374         } else {
375             return encoding;
376         }
377     }
378 
379     /**
380      * Determine whether a file is a properties file or not.
381      *
382      * @param file The file to check
383      * @return <code>true</code> if the file name has an extension of "properties", otherwise <code>false</code>
384      * @since 3.2.0
385      */
386     static boolean isPropertiesFile(File file) {
387         return "properties".equals(getExtension(file.getName()));
388     }
389 
390     private void handleDefaultFilterWrappers(MavenResourcesExecution mavenResourcesExecution)
391             throws MavenFilteringException {
392         List<FilterWrapper> filterWrappers = new ArrayList<>();
393         if (mavenResourcesExecution.getFilterWrappers() != null) {
394             filterWrappers.addAll(mavenResourcesExecution.getFilterWrappers());
395         }
396         filterWrappers.addAll(mavenFileFilter.getDefaultFilterWrappers(mavenResourcesExecution));
397         mavenResourcesExecution.setFilterWrappers(filterWrappers);
398     }
399 
400     private File getDestinationFile(
401             File outputDirectory, String targetPath, String name, MavenResourcesExecution mavenResourcesExecution)
402             throws MavenFilteringException {
403         String destination;
404         if (!mavenResourcesExecution.isFlatten()) {
405             destination = name;
406         } else {
407             Path path = Paths.get(name);
408             Path filePath = path.getFileName();
409             destination = filePath.toString();
410         }
411 
412         if (mavenResourcesExecution.isFilterFilenames()
413                 && mavenResourcesExecution.getFilterWrappers().size() > 0) {
414             destination = filterFileName(destination, mavenResourcesExecution.getFilterWrappers());
415         }
416 
417         if (targetPath != null) {
418             destination = targetPath + "/" + destination;
419         }
420 
421         File destinationFile = new File(destination);
422         if (!destinationFile.isAbsolute()) {
423             destinationFile = new File(outputDirectory, destination);
424         }
425 
426         if (!destinationFile.getParentFile().exists()) {
427             destinationFile.getParentFile().mkdirs();
428         }
429         return destinationFile;
430     }
431 
432     private String[] setupScanner(Resource resource, Scanner scanner, boolean addDefaultExcludes) {
433         String[] includes;
434         if (resource.getIncludes() != null && !resource.getIncludes().isEmpty()) {
435             includes = resource.getIncludes().toArray(EMPTY_STRING_ARRAY);
436         } else {
437             includes = DEFAULT_INCLUDES;
438         }
439         scanner.setIncludes(includes);
440 
441         String[] excludes = null;
442         if (resource.getExcludes() != null && !resource.getExcludes().isEmpty()) {
443             excludes = resource.getExcludes().toArray(EMPTY_STRING_ARRAY);
444             scanner.setExcludes(excludes);
445         }
446 
447         if (addDefaultExcludes) {
448             scanner.addDefaultExcludes();
449         }
450         return includes;
451     }
452 
453     private void copyDirectoryLayout(File sourceDirectory, File destinationDirectory, Scanner scanner)
454             throws IOException {
455         if (sourceDirectory == null) {
456             throw new IOException("source directory can't be null.");
457         }
458 
459         if (destinationDirectory == null) {
460             throw new IOException("destination directory can't be null.");
461         }
462 
463         if (sourceDirectory.equals(destinationDirectory)) {
464             throw new IOException("source and destination are the same directory.");
465         }
466 
467         if (!sourceDirectory.exists()) {
468             throw new IOException("Source directory doesn't exist (" + sourceDirectory.getAbsolutePath() + ").");
469         }
470 
471         for (String name : scanner.getIncludedDirectories()) {
472             File source = new File(sourceDirectory, name);
473 
474             if (source.equals(sourceDirectory)) {
475                 continue;
476             }
477 
478             File destination = new File(destinationDirectory, name);
479             destination.mkdirs();
480         }
481     }
482 
483     private String getRelativeOutputDirectory(MavenResourcesExecution execution) {
484         String relOutDir = execution.getOutputDirectory().getAbsolutePath();
485 
486         if (execution.getMavenProject() != null && execution.getMavenProject().getBasedir() != null) {
487             String basedir = execution.getMavenProject().getBasedir().getAbsolutePath();
488             relOutDir = FilteringUtils.getRelativeFilePath(basedir, relOutDir);
489             if (relOutDir == null) {
490                 relOutDir = execution.getOutputDirectory().getPath();
491             } else {
492                 relOutDir = relOutDir.replace('\\', '/');
493             }
494         }
495 
496         return relOutDir;
497     }
498 
499     /*
500      * Filter the name of a file using the same mechanism for filtering the content of the file.
501      */
502     private String filterFileName(String name, List<FilterWrapper> wrappers) throws MavenFilteringException {
503 
504         Reader reader = new StringReader(name);
505         for (FilterWrapper wrapper : wrappers) {
506             reader = wrapper.getReader(reader);
507         }
508 
509         try (StringWriter writer = new StringWriter()) {
510             IOUtils.copy(reader, writer);
511             String filteredFilename = writer.toString();
512 
513             if (LOGGER.isDebugEnabled()) {
514                 LOGGER.debug("renaming filename " + name + " to " + filteredFilename);
515             }
516             return filteredFilename;
517         } catch (IOException e) {
518             throw new MavenFilteringException("Failed filtering filename" + name, e);
519         }
520     }
521 }