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