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.RandomAccessFile;
25  import java.io.Reader;
26  import java.io.Writer;
27  import java.nio.Buffer;
28  import java.nio.ByteBuffer;
29  import java.nio.CharBuffer;
30  import java.nio.charset.Charset;
31  import java.nio.charset.CharsetEncoder;
32  import java.nio.charset.CoderResult;
33  import java.nio.file.Files;
34  import java.nio.file.LinkOption;
35  import java.nio.file.StandardCopyOption;
36  import java.util.StringTokenizer;
37  import java.util.regex.Pattern;
38  
39  import org.apache.commons.io.IOUtils;
40  import org.codehaus.plexus.util.Os;
41  
42  /**
43   * @author Olivier Lamy
44   * @author Dennis Lundberg
45   */
46  public final class FilteringUtils
47  {
48      /**
49       * The number of bytes in a kilobyte.
50       */
51      private static final int ONE_KB = 1024;
52  
53      /**
54       * The number of bytes in a megabyte.
55       */
56      private static final int ONE_MB = ONE_KB * ONE_KB;
57  
58      /**
59       * The file copy buffer size (30 MB)
60       */
61      private static final int FILE_COPY_BUFFER_SIZE = ONE_MB * 30;
62  
63      private static final String WINDOWS_PATH_PATTERN = "^(.*)[a-zA-Z]:\\\\(.*)";
64  
65      private static final Pattern PATTERN = Pattern.compile( WINDOWS_PATH_PATTERN );
66  
67      /**
68       *
69       */
70      private FilteringUtils()
71      {
72          // nothing just an util class
73      }
74  
75      // TODO: Correct to handle relative windows paths. (http://jira.apache.org/jira/browse/MSHARED-121)
76      // How do we distinguish a relative windows path from some other value that happens to contain backslashes??
77      /**
78       * @param val The value to be escaped.
79       * @return Escaped value
80       */
81      public static String escapeWindowsPath( String val )
82      {
83          if ( !isEmpty( val ) && PATTERN.matcher( val ).matches() )
84          {
85              // Adapted from StringUtils.replace in plexus-utils to accommodate pre-escaped backslashes.
86              StringBuilder buf = new StringBuilder( val.length() );
87              int start = 0, end = 0;
88              while ( ( end = val.indexOf( '\\', start ) ) != -1 )
89              {
90                  buf.append( val.substring( start, end ) ).append( "\\\\" );
91                  start = end + 1;
92  
93                  if ( val.indexOf( '\\', end + 1 ) == end + 1 )
94                  {
95                      start++;
96                  }
97              }
98  
99              buf.append( val.substring( start ) );
100 
101             return buf.toString();
102         }
103         return val;
104     }
105 
106 
107     /**
108      * Resolve a file <code>filename</code> to its canonical form. If <code>filename</code> is
109      * relative (doesn't start with <code>/</code>), it is resolved relative to
110      * <code>baseFile</code>. Otherwise it is treated as a normal root-relative path.
111      *
112      * @param baseFile where to resolve <code>filename</code> from, if <code>filename</code> is relative
113      * @param filename absolute or relative file path to resolve
114      * @return the canonical <code>File</code> of <code>filename</code>
115      */
116     public static File resolveFile( final File baseFile, String filename )
117     {
118         String filenm = filename;
119         if ( '/' != File.separatorChar )
120         {
121             filenm = filename.replace( '/', File.separatorChar );
122         }
123 
124         if ( '\\' != File.separatorChar )
125         {
126             filenm = filename.replace( '\\', File.separatorChar );
127         }
128 
129         // deal with absolute files
130         if ( filenm.startsWith( File.separator ) || ( Os.isFamily( Os.FAMILY_WINDOWS ) && filenm.indexOf( ":" ) > 0 ) )
131         {
132             File file = new File( filenm );
133 
134             try
135             {
136                 file = file.getCanonicalFile();
137             }
138             catch ( final IOException ioe )
139             {
140                 // nop
141             }
142 
143             return file;
144         }
145         // FIXME: I'm almost certain this // removal is unnecessary, as getAbsoluteFile() strips
146         // them. However, I'm not sure about this UNC stuff. (JT)
147         final char[] chars = filename.toCharArray();
148         final StringBuilder sb = new StringBuilder();
149 
150         //remove duplicate file separators in succession - except
151         //on win32 at start of filename as UNC filenames can
152         //be \\AComputer\AShare\myfile.txt
153         int start = 0;
154         if ( '\\' == File.separatorChar )
155         {
156             sb.append( filenm.charAt( 0 ) );
157             start++;
158         }
159 
160         for ( int i = start; i < chars.length; i++ )
161         {
162             final boolean doubleSeparator = File.separatorChar == chars[i] && File.separatorChar == chars[i - 1];
163 
164             if ( !doubleSeparator )
165             {
166                 sb.append( chars[i] );
167             }
168         }
169 
170         filenm = sb.toString();
171 
172         //must be relative
173         File file = ( new File( baseFile, filenm ) ).getAbsoluteFile();
174 
175         try
176         {
177             file = file.getCanonicalFile();
178         }
179         catch ( final IOException ioe )
180         {
181             // nop
182         }
183 
184         return file;
185     }
186 
187 
188     /**
189      * <p>This method can calculate the relative path between two paths on a file system.</p>
190      * <pre>
191      * PathTool.getRelativeFilePath( null, null )                                   = ""
192      * PathTool.getRelativeFilePath( null, "/usr/local/java/bin" )                  = ""
193      * PathTool.getRelativeFilePath( "/usr/local", null )                           = ""
194      * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin" )          = "java/bin"
195      * PathTool.getRelativeFilePath( "/usr/local", "/usr/local/java/bin/" )         = "java/bin"
196      * PathTool.getRelativeFilePath( "/usr/local/java/bin", "/usr/local/" )         = "../.."
197      * PathTool.getRelativeFilePath( "/usr/local/", "/usr/local/java/bin/java.sh" ) = "java/bin/java.sh"
198      * PathTool.getRelativeFilePath( "/usr/local/java/bin/java.sh", "/usr/local/" ) = "../../.."
199      * PathTool.getRelativeFilePath( "/usr/local/", "/bin" )                        = "../../bin"
200      * PathTool.getRelativeFilePath( "/bin", "/usr/local/" )                        = "../usr/local"
201      * </pre>
202      * Note: On Windows based system, the <code>/</code> character should be replaced by <code>\</code> character.
203      *
204      * @param oldPath old path
205      * @param newPath new path
206      * @return a relative file path from <code>oldPath</code>.
207      */
208     public static String getRelativeFilePath( final String oldPath, final String newPath )
209     {
210         if ( isEmpty( oldPath ) || isEmpty( newPath ) )
211         {
212             return "";
213         }
214 
215         // normalise the path delimiters
216         String fromPath = new File( oldPath ).getPath();
217         String toPath = new File( newPath ).getPath();
218 
219         // strip any leading slashes if its a windows path
220         if ( toPath.matches( "^\\[a-zA-Z]:" ) )
221         {
222             toPath = toPath.substring( 1 );
223         }
224         if ( fromPath.matches( "^\\[a-zA-Z]:" ) )
225         {
226             fromPath = fromPath.substring( 1 );
227         }
228 
229         // lowercase windows drive letters.
230         if ( fromPath.startsWith( ":", 1 ) )
231         {
232             fromPath = Character.toLowerCase( fromPath.charAt( 0 ) ) + fromPath.substring( 1 );
233         }
234         if ( toPath.startsWith( ":", 1 ) )
235         {
236             toPath = Character.toLowerCase( toPath.charAt( 0 ) ) + toPath.substring( 1 );
237         }
238 
239         // check for the presence of windows drives. No relative way of
240         // traversing from one to the other.
241         if ( ( toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) )
242                 && ( !toPath.substring( 0, 1 ).equals( fromPath.substring( 0, 1 ) ) ) )
243         {
244             // they both have drive path element but they dont match, no
245             // relative path
246             return null;
247         }
248 
249         if ( ( toPath.startsWith( ":", 1 ) && !fromPath.startsWith( ":", 1 ) )
250                 || ( !toPath.startsWith( ":", 1 ) && fromPath.startsWith( ":", 1 ) ) )
251         {
252             // one has a drive path element and the other doesnt, no relative
253             // path.
254             return null;
255         }
256 
257         String resultPath = buildRelativePath( toPath, fromPath, File.separatorChar );
258 
259         if ( newPath.endsWith( File.separator ) && !resultPath.endsWith( File.separator ) )
260         {
261             return resultPath + File.separator;
262         }
263 
264         return resultPath;
265     }
266 
267     private static String buildRelativePath( String toPath, String fromPath, final char separatorChar )
268     {
269         // use tokeniser to traverse paths and for lazy checking
270         StringTokenizer toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
271         StringTokenizer fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
272 
273         int count = 0;
274 
275         // walk along the to path looking for divergence from the from path
276         while ( toTokeniser.hasMoreTokens() && fromTokeniser.hasMoreTokens() )
277         {
278             if ( separatorChar == '\\' )
279             {
280                 if ( !fromTokeniser.nextToken().equalsIgnoreCase( toTokeniser.nextToken() ) )
281                 {
282                     break;
283                 }
284             }
285             else
286             {
287                 if ( !fromTokeniser.nextToken().equals( toTokeniser.nextToken() ) )
288                 {
289                     break;
290                 }
291             }
292 
293             count++;
294         }
295 
296         // reinitialise the tokenisers to count positions to retrieve the
297         // gobbled token
298 
299         toTokeniser = new StringTokenizer( toPath, String.valueOf( separatorChar ) );
300         fromTokeniser = new StringTokenizer( fromPath, String.valueOf( separatorChar ) );
301 
302         while ( count-- > 0 )
303         {
304             fromTokeniser.nextToken();
305             toTokeniser.nextToken();
306         }
307 
308         StringBuilder relativePath = new StringBuilder();
309 
310         // add back refs for the rest of from location.
311         while ( fromTokeniser.hasMoreTokens() )
312         {
313             fromTokeniser.nextToken();
314 
315             relativePath.append( ".." );
316 
317             if ( fromTokeniser.hasMoreTokens() )
318             {
319                 relativePath.append( separatorChar );
320             }
321         }
322 
323         if ( relativePath.length() != 0 && toTokeniser.hasMoreTokens() )
324         {
325             relativePath.append( separatorChar );
326         }
327 
328         // add fwd fills for whatevers left of newPath.
329         while ( toTokeniser.hasMoreTokens() )
330         {
331             relativePath.append( toTokeniser.nextToken() );
332 
333             if ( toTokeniser.hasMoreTokens() )
334             {
335                 relativePath.append( separatorChar );
336             }
337         }
338         return relativePath.toString();
339     }
340 
341     static boolean isEmpty( final String string )
342     {
343         return string == null || string.trim().isEmpty();
344     }
345 
346     /**
347      * <b>If wrappers is null or empty, the file will be copy only if to.lastModified() &lt; from.lastModified() or if
348      * overwrite is true</b>
349      *
350      * @param from the file to copy
351      * @param to the destination file
352      * @param encoding the file output encoding (only if wrappers is not empty)
353      * @param wrappers array of {@link FilterWrapper}
354      * @param overwrite if true and wrappers is null or empty, the file will be copied even if
355      *         to.lastModified() &lt; from.lastModified()
356      * @throws IOException if an IO error occurs during copying or filtering
357      */
358     public static void copyFile( File from, File to, String encoding,
359                                  FilterWrapper[] wrappers, boolean overwrite )
360             throws IOException
361     {
362         if ( wrappers == null || wrappers.length == 0 )
363         {
364             if ( overwrite || to.lastModified() < from.lastModified() )
365             {
366                 Files.copy( from.toPath(), to.toPath(), LinkOption.NOFOLLOW_LINKS,
367                         StandardCopyOption.REPLACE_EXISTING );
368             }
369         }
370         else
371         {
372             Charset charset = charset( encoding );
373 
374             // buffer so it isn't reading a byte at a time!
375             try ( Reader fileReader = Files.newBufferedReader( from.toPath(), charset ) )
376             {
377                 Reader wrapped = fileReader;
378                 for ( FilterWrapper wrapper : wrappers )
379                 {
380                     wrapped = wrapper.getReader( wrapped );
381                 }
382 
383                 if ( overwrite || !to.exists() )
384                 {
385                     try ( Writer fileWriter = Files.newBufferedWriter( to.toPath(), charset ) )
386                     {
387                         IOUtils.copy( wrapped, fileWriter );
388                     }
389                 }
390                 else
391                 {
392                     CharsetEncoder encoder = charset.newEncoder();
393 
394                     int totalBufferSize = FILE_COPY_BUFFER_SIZE;
395 
396                     int charBufferSize = ( int ) Math.floor( totalBufferSize / ( 2 + 2 * encoder.maxBytesPerChar() ) );
397                     int byteBufferSize = ( int ) Math.ceil( charBufferSize * encoder.maxBytesPerChar() );
398 
399                     CharBuffer newChars = CharBuffer.allocate( charBufferSize );
400                     ByteBuffer newBytes = ByteBuffer.allocate( byteBufferSize );
401                     ByteBuffer existingBytes = ByteBuffer.allocate( byteBufferSize );
402 
403                     CoderResult coderResult;
404                     int existingRead;
405                     boolean writing = false;
406 
407                     try ( final RandomAccessFile existing = new RandomAccessFile( to, "rw" ) )
408                     {
409                         int n;
410                         while ( -1 != ( n = wrapped.read( newChars ) ) )
411                         {
412                             ( (Buffer) newChars ).flip();
413 
414                             coderResult = encoder.encode( newChars, newBytes, n != 0 );
415                             if ( coderResult.isError() )
416                             {
417                                 coderResult.throwException();
418                             }
419 
420                             ( ( Buffer ) newBytes ).flip();
421 
422                             if ( !writing )
423                             {
424                                 existingRead = existing.read( existingBytes.array(), 0, newBytes.remaining() );
425                                 ( ( Buffer ) existingBytes ).position( existingRead );
426                                 ( ( Buffer ) existingBytes ).flip();
427 
428                                 if ( newBytes.compareTo( existingBytes ) != 0 )
429                                 {
430                                     writing = true;
431                                     if ( existingRead > 0 )
432                                     {
433                                         existing.seek( existing.getFilePointer() - existingRead );
434                                     }
435                                 }
436                             }
437 
438                             if ( writing )
439                             {
440                                 existing.write( newBytes.array(), 0, newBytes.remaining() );
441                             }
442 
443                             ( ( Buffer ) newChars ).clear();
444                             ( ( Buffer ) newBytes ).clear();
445                             ( ( Buffer ) existingBytes ).clear();
446                         }
447 
448                         if ( existing.length() > existing.getFilePointer() )
449                         {
450                             existing.setLength( existing.getFilePointer() );
451                         }
452                     }
453                 }
454             }
455         }
456 
457         copyFilePermissions( from, to );
458     }
459 
460 
461     /**
462      * Attempts to copy file permissions from the source to the destination file.
463      * Initially attempts to copy posix file permissions, assuming that the files are both on posix filesystems.
464      * If the initial attempts fail then a second attempt using less precise permissions model.
465      * Note that permissions are copied on a best-efforts basis,
466      * failure to copy permissions will not result in an exception.
467      *
468      * @param source the file to copy permissions from.
469      * @param destination the file to copy permissions to.
470      */
471     private static void copyFilePermissions( File source, File destination )
472             throws IOException
473     {
474         try
475         {
476             // attempt to copy posix file permissions
477             Files.setPosixFilePermissions(
478                     destination.toPath(),
479                     Files.getPosixFilePermissions( source.toPath() )
480             );
481         }
482         catch ( UnsupportedOperationException e )
483         {
484             // fallback to setting partial permissions
485             destination.setExecutable( source.canExecute() );
486             destination.setReadable( source.canRead() );
487             destination.setWritable( source.canWrite() );
488         }
489     }
490 
491     private static Charset charset( String encoding )
492     {
493         if ( encoding == null || encoding.isEmpty() )
494         {
495             return Charset.defaultCharset();
496         }
497         else
498         {
499             return Charset.forName( encoding );
500         }
501     }
502 
503 }