001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.camel.util;
018    
019    
020    import java.io.File;
021    import java.io.FileInputStream;
022    import java.io.FileOutputStream;
023    import java.io.IOException;
024    import java.net.URI;
025    import java.net.URISyntaxException;
026    import java.nio.channels.FileChannel;
027    import java.util.Iterator;
028    import java.util.Locale;
029    import java.util.Random;
030    import java.util.Stack;
031    
032    import org.slf4j.Logger;
033    import org.slf4j.LoggerFactory;
034    
035    /**
036     * File utilities
037     */
038    public final class FileUtil {
039        
040        public static final int BUFFER_SIZE = 128 * 1024;
041    
042        private static final transient Logger LOG = LoggerFactory.getLogger(FileUtil.class);
043        private static final int RETRY_SLEEP_MILLIS = 10;
044        private static File defaultTempDir;
045        
046        // current value of the "user.dir" property
047        private static String gUserDir;
048        // cached URI object for the current value of the escaped "user.dir" property stored as a URI
049        private static URI gUserDirURI;
050        // which ASCII characters need to be escaped
051        private static boolean gNeedEscaping[] = new boolean[128];
052        // the first hex character if a character needs to be escaped
053        private static char gAfterEscaping1[] = new char[128];
054        // the second hex character if a character needs to be escaped
055        private static char gAfterEscaping2[] = new char[128];
056        private static char[] gHexChs = {'0', '1', '2', '3', '4', '5', '6', '7',
057                                         '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
058        // initialize the above 3 arrays
059        static {
060            for (int i = 0; i <= 0x1f; i++) {
061                gNeedEscaping[i] = true;
062                gAfterEscaping1[i] = gHexChs[i >> 4];
063                gAfterEscaping2[i] = gHexChs[i & 0xf];
064            }
065            gNeedEscaping[0x7f] = true;
066            gAfterEscaping1[0x7f] = '7';
067            gAfterEscaping2[0x7f] = 'F';
068            char[] escChs = {' ', '<', '>', '#', '%', '"', '{', '}',
069                             '|', '\\', '^', '~', '[', ']', '`'};
070            int len = escChs.length;
071            char ch;
072            for (int i = 0; i < len; i++) {
073                ch = escChs[i];
074                gNeedEscaping[ch] = true;
075                gAfterEscaping1[ch] = gHexChs[ch >> 4];
076                gAfterEscaping2[ch] = gHexChs[ch & 0xf];
077            }
078        }
079        
080        private FileUtil() {
081            // Utils method
082        }
083        
084    
085        // To escape the "user.dir" system property, by using %HH to represent
086        // special ASCII characters: 0x00~0x1F, 0x7F, ' ', '<', '>', '#', '%'
087        // and '"'. It's a static method, so needs to be synchronized.
088        // this method looks heavy, but since the system property isn't expected
089        // to change often, so in most cases, we only need to return the URI
090        // that was escaped before.
091        // According to the URI spec, non-ASCII characters (whose value >= 128)
092        // need to be escaped too.
093        // REVISIT: don't know how to escape non-ASCII characters, especially
094        // which encoding to use. Leave them for now.
095        public static synchronized URI getUserDir() throws URISyntaxException {
096            // get the user.dir property
097            String userDir = "";
098            try {
099                userDir = System.getProperty("user.dir");
100            } catch (SecurityException se) {
101            }
102    
103            // return empty string if property value is empty string.
104            if (userDir.length() == 0) {
105                return new URI("file", "", "", null, null);
106            }
107            // compute the new escaped value if the new property value doesn't
108            // match the previous one
109            if (gUserDirURI != null && userDir.equals(gUserDir)) {
110                return gUserDirURI;
111            }
112    
113            // record the new value as the global property value
114            gUserDir = userDir;
115    
116            char separator = java.io.File.separatorChar;
117            userDir = userDir.replace(separator, '/');
118    
119            int len = userDir.length(); 
120            int ch;
121            StringBuffer buffer = new StringBuffer(len * 3);
122            // change C:/blah to /C:/blah
123            if (len >= 2 && userDir.charAt(1) == ':') {
124                ch = Character.toUpperCase(userDir.charAt(0));
125                if (ch >= 'A' && ch <= 'Z') {
126                    buffer.append('/');
127                }
128            }
129    
130            // for each character in the path
131            int i = 0;
132            for (; i < len; i++) {
133                ch = userDir.charAt(i);
134                // if it's not an ASCII character, break here, and use UTF-8 encoding
135                if (ch >= 128) {
136                    break;
137                }
138                if (gNeedEscaping[ch]) {
139                    buffer.append('%');
140                    buffer.append(gAfterEscaping1[ch]);
141                    buffer.append(gAfterEscaping2[ch]);
142                    // record the fact that it's escaped
143                } else {
144                    buffer.append((char)ch);
145                }
146            }
147    
148            // we saw some non-ascii character
149            if (i < len) {
150                // get UTF-8 bytes for the remaining sub-string
151                byte[] bytes = null;
152                byte b;
153                try {
154                    bytes = userDir.substring(i).getBytes("UTF-8");
155                } catch (java.io.UnsupportedEncodingException e) {
156                    // should never happen
157                    return new URI("file", "", userDir, null, null);
158                }
159                len = bytes.length;
160    
161                // for each byte
162                for (i = 0; i < len; i++) {
163                    b = bytes[i];
164                    // for non-ascii character: make it positive, then escape
165                    if (b < 0) {
166                        ch = b + 256;
167                        buffer.append('%');
168                        buffer.append(gHexChs[ch >> 4]);
169                        buffer.append(gHexChs[ch & 0xf]);
170                    } else if (gNeedEscaping[b]) {
171                        buffer.append('%');
172                        buffer.append(gAfterEscaping1[b]);
173                        buffer.append(gAfterEscaping2[b]);
174                    } else {
175                        buffer.append((char)b);
176                    }
177                }
178            }
179    
180            // change blah/blah to blah/blah/
181            if (!userDir.endsWith("/")) {
182                buffer.append('/');
183            }
184    
185            gUserDirURI = new URI("file", "", buffer.toString(), null, null);
186    
187            return gUserDirURI;
188        }
189    
190        /**
191         * Normalizes the path to cater for Windows and other platforms
192         */
193        public static String normalizePath(String path) {
194            if (path == null) {
195                return null;
196            }
197    
198            if (isWindows()) {
199                // special handling for Windows where we need to convert / to \\
200                return path.replace('/', '\\');
201            } else {
202                // for other systems make sure we use / as separators
203                return path.replace('\\', '/');
204            }
205        }
206        
207        public static boolean isWindows() {
208            String osName = System.getProperty("os.name").toLowerCase(Locale.US);
209            return osName.indexOf("windows") > -1;
210        }
211    
212        public static File createTempFile(String prefix, String suffix) throws IOException {
213            return createTempFile(prefix, suffix, null);
214        }
215    
216        public static File createTempFile(String prefix, String suffix, File parentDir) throws IOException {
217            File parent = (parentDir == null) ? getDefaultTempDir() : parentDir;
218                
219            if (suffix == null) {
220                suffix = ".tmp";
221            }
222            if (prefix == null) {
223                prefix = "camel";
224            } else if (prefix.length() < 3) {
225                prefix = prefix + "camel";
226            }
227    
228            // create parent folder
229            parent.mkdirs();
230    
231            return File.createTempFile(prefix, suffix, parent);
232        }
233    
234        /**
235         * Strip any leading separators
236         */
237        public static String stripLeadingSeparator(String name) {
238            if (name == null) {
239                return null;
240            }
241            while (name.startsWith("/") || name.startsWith(File.separator)) {
242                name = name.substring(1);
243            }
244            return name;
245        }
246    
247        /**
248         * Does the name start with a leading separator
249         */
250        public static boolean hasLeadingSeparator(String name) {
251            if (name == null) {
252                return false;
253            }
254            if (name.startsWith("/") || name.startsWith(File.separator)) {
255                return true;
256            }
257            return false;
258        }
259    
260        /**
261         * Strip first leading separator
262         */
263        public static String stripFirstLeadingSeparator(String name) {
264            if (name == null) {
265                return null;
266            }
267            if (name.startsWith("/") || name.startsWith(File.separator)) {
268                name = name.substring(1);
269            }
270            return name;
271        }
272    
273        /**
274         * Strip any trailing separators
275         */
276        public static String stripTrailingSeparator(String name) {
277            if (ObjectHelper.isEmpty(name)) {
278                return name;
279            }
280            
281            String s = name;
282            
283            // there must be some leading text, as we should only remove trailing separators 
284            while (s.endsWith("/") || s.endsWith(File.separator)) {
285                s = s.substring(0, s.length() - 1);
286            }
287            
288            // if the string is empty, that means there was only trailing slashes, and no leading text
289            // and so we should then return the original name as is
290            if (ObjectHelper.isEmpty(s)) {
291                return name;
292            } else {
293                // return without trailing slashes
294                return s;
295            }
296        }
297    
298        /**
299         * Strips any leading paths
300         */
301        public static String stripPath(String name) {
302            if (name == null) {
303                return null;
304            }
305            int posUnix = name.lastIndexOf('/');
306            int posWin = name.lastIndexOf('\\');
307            int pos = Math.max(posUnix, posWin);
308    
309            if (pos != -1) {
310                return name.substring(pos + 1);
311            }
312            return name;
313        }
314    
315        public static String stripExt(String name) {
316            if (name == null) {
317                return null;
318            }
319            int pos = name.lastIndexOf('.');
320            if (pos != -1) {
321                return name.substring(0, pos);
322            }
323            return name;
324        }
325    
326        /**
327         * Returns only the leading path (returns <tt>null</tt> if no path)
328         */
329        public static String onlyPath(String name) {
330            if (name == null) {
331                return null;
332            }
333    
334            int posUnix = name.lastIndexOf('/');
335            int posWin = name.lastIndexOf('\\');
336            int pos = Math.max(posUnix, posWin);
337    
338            if (pos > 0) {
339                return name.substring(0, pos);
340            } else if (pos == 0) {
341                // name is in the root path, so extract the path as the first char
342                return name.substring(0, 1);
343            }
344            // no path in name
345            return null;
346        }
347    
348        /**
349         * Compacts a path by stacking it and reducing <tt>..</tt>,
350         * and uses OS specific file separators (eg {@link java.io.File#separator}).
351         */
352        public static String compactPath(String path) {
353            return compactPath(path, File.separatorChar);
354        }
355    
356        /**
357         * Compacts a path by stacking it and reducing <tt>..</tt>,
358         * and uses the given separator.
359         */
360        public static String compactPath(String path, char separator) {
361            if (path == null) {
362                return null;
363            }
364            
365            // only normalize if contains a path separator
366            if (path.indexOf('/') == -1 && path.indexOf('\\') == -1)  {
367                return path;
368            }
369    
370            // need to normalize path before compacting
371            path = normalizePath(path);
372    
373            // preserve ending slash if given in input path
374            boolean endsWithSlash = path.endsWith("/") || path.endsWith("\\");
375    
376            // preserve starting slash if given in input path
377            boolean startsWithSlash = path.startsWith("/") || path.startsWith("\\");
378            
379            Stack<String> stack = new Stack<String>();
380    
381            // separator can either be windows or unix style
382            String separatorRegex = "\\\\|/";
383            String[] parts = path.split(separatorRegex);
384            for (String part : parts) {
385                if (part.equals("..") && !stack.isEmpty() && !"..".equals(stack.peek())) {
386                    // only pop if there is a previous path, which is not a ".." path either
387                    stack.pop();
388                } else if (part.equals(".") || part.isEmpty()) {
389                    // do nothing because we don't want a path like foo/./bar or foo//bar
390                } else {
391                    stack.push(part);
392                }
393            }
394    
395            // build path based on stack
396            StringBuilder sb = new StringBuilder();
397            
398            if (startsWithSlash) {
399                sb.append(separator);
400            }
401            
402            for (Iterator<String> it = stack.iterator(); it.hasNext();) {
403                sb.append(it.next());
404                if (it.hasNext()) {
405                    sb.append(separator);
406                }
407            }
408    
409            if (endsWithSlash) {
410                sb.append(separator);
411            }
412    
413            return sb.toString();
414        }
415    
416        private static synchronized File getDefaultTempDir() {
417            if (defaultTempDir != null && defaultTempDir.exists()) {
418                return defaultTempDir;
419            }
420    
421            String s = System.getProperty("java.io.tmpdir");
422            File checkExists = new File(s);
423            if (!checkExists.exists()) {
424                throw new RuntimeException("The directory "
425                                       + checkExists.getAbsolutePath()
426                                       + " does not exist, please set java.io.tempdir"
427                                       + " to an existing directory");
428            }
429    
430            // create a sub folder with a random number
431            Random ran = new Random();
432            int x = ran.nextInt(1000000);
433    
434            File f = new File(s, "camel-tmp-" + x);
435            while (!f.mkdir()) {
436                x = ran.nextInt(1000000);
437                f = new File(s, "camel-tmp-" + x);
438            }
439    
440            defaultTempDir = f;
441    
442            // create shutdown hook to remove the temp dir
443            Thread hook = new Thread() {
444                @Override
445                public void run() {
446                    removeDir(defaultTempDir);
447                }
448            };
449            Runtime.getRuntime().addShutdownHook(hook);
450    
451            return defaultTempDir;
452        }
453    
454        private static void removeDir(File d) {
455            String[] list = d.list();
456            if (list == null) {
457                list = new String[0];
458            }
459            for (String s : list) {
460                File f = new File(d, s);
461                if (f.isDirectory()) {
462                    removeDir(f);
463                } else {
464                    delete(f);
465                }
466            }
467            delete(d);
468        }
469    
470        private static void delete(File f) {
471            if (!f.delete()) {
472                if (isWindows()) {
473                    System.gc();
474                }
475                try {
476                    Thread.sleep(RETRY_SLEEP_MILLIS);
477                } catch (InterruptedException ex) {
478                    // Ignore Exception
479                }
480                if (!f.delete()) {
481                    f.deleteOnExit();
482                }
483            }
484        }
485    
486        /**
487         * Renames a file.
488         *
489         * @param from the from file
490         * @param to   the to file
491         * @param copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails
492         * @return <tt>true</tt> if the file was renamed, otherwise <tt>false</tt>
493         * @throws java.io.IOException is thrown if error renaming file
494         */
495        public static boolean renameFile(File from, File to, boolean copyAndDeleteOnRenameFail) throws IOException {
496            // do not try to rename non existing files
497            if (!from.exists()) {
498                return false;
499            }
500    
501            // some OS such as Windows can have problem doing rename IO operations so we may need to
502            // retry a couple of times to let it work
503            boolean renamed = false;
504            int count = 0;
505            while (!renamed && count < 3) {
506                if (LOG.isDebugEnabled() && count > 0) {
507                    LOG.debug("Retrying attempt {} to rename file from: {} to: {}", new Object[]{count, from, to});
508                }
509    
510                renamed = from.renameTo(to);
511                if (!renamed && count > 0) {
512                    try {
513                        Thread.sleep(1000);
514                    } catch (InterruptedException e) {
515                        // ignore
516                    }
517                }
518                count++;
519            }
520    
521            // we could not rename using renameTo, so lets fallback and do a copy/delete approach.
522            // for example if you move files between different file systems (linux -> windows etc.)
523            if (!renamed && copyAndDeleteOnRenameFail) {
524                // now do a copy and delete as all rename attempts failed
525                LOG.debug("Cannot rename file from: {} to: {}, will now use a copy/delete approach instead", from, to);
526                copyFile(from, to);
527                if (!deleteFile(from)) {
528                    throw new IOException("Renaming file from: " + from + " to: " + to + " failed due cannot delete from file: " + from + " after copy succeeded");
529                } else {
530                    renamed = true;
531                }
532            }
533    
534            if (LOG.isDebugEnabled() && count > 0) {
535                LOG.debug("Tried {} to rename file: {} to: {} with result: {}", new Object[]{count, from, to, renamed});
536            }
537            return renamed;
538        }
539    
540        @SuppressWarnings("resource")
541        public static void copyFile(File from, File to) throws IOException {
542            FileChannel in = new FileInputStream(from).getChannel();
543            FileChannel out = new FileOutputStream(to).getChannel();
544            try {
545                if (LOG.isTraceEnabled()) {
546                    LOG.trace("Using FileChannel to copy from: " + in + " to: " + out);
547                }
548    
549                long size = in.size();
550                long position = 0;
551                while (position < size) {
552                    position += in.transferTo(position, BUFFER_SIZE, out);
553                }
554            } finally {
555                IOHelper.close(in, from.getName(), LOG);
556                IOHelper.close(out, to.getName(), LOG);
557            }
558        }
559    
560        public static boolean deleteFile(File file) {
561            // do not try to delete non existing files
562            if (!file.exists()) {
563                return false;
564            }
565    
566            // some OS such as Windows can have problem doing delete IO operations so we may need to
567            // retry a couple of times to let it work
568            boolean deleted = false;
569            int count = 0;
570            while (!deleted && count < 3) {
571                LOG.debug("Retrying attempt {} to delete file: {}", count, file);
572    
573                deleted = file.delete();
574                if (!deleted && count > 0) {
575                    try {
576                        Thread.sleep(1000);
577                    } catch (InterruptedException e) {
578                        // ignore
579                    }
580                }
581                count++;
582            }
583    
584    
585            if (LOG.isDebugEnabled() && count > 0) {
586                LOG.debug("Tried {} to delete file: {} with result: {}", new Object[]{count, file, deleted});
587            }
588            return deleted;
589        }
590    
591        /**
592         * Is the given file an absolute file.
593         * <p/>
594         * Will also work around issue on Windows to consider files on Windows starting with a \
595         * as absolute files. This makes the logic consistent across all OS platforms.
596         *
597         * @param file  the file
598         * @return <tt>true</ff> if its an absolute path, <tt>false</tt> otherwise.
599         */
600        public static boolean isAbsolute(File file) {
601            if (isWindows()) {
602                // special for windows
603                String path = file.getPath();
604                if (path.startsWith(File.separator)) {
605                    return true;
606                }
607            }
608            return file.isAbsolute();
609        }
610    
611        /**
612         * Creates a new file.
613         *
614         * @param file the file
615         * @return <tt>true</tt> if created a new file, <tt>false</tt> otherwise
616         * @throws IOException is thrown if error creating the new file
617         */
618        public static boolean createNewFile(File file) throws IOException {
619            try {
620                return file.createNewFile();
621            } catch (IOException e) {
622                if (file.exists()) {
623                    return true;
624                } else {
625                    throw e;
626                }
627            }
628        }
629    
630    }