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