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