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