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