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 */
017package org.apache.camel.util;
018
019import java.io.File;
020import java.io.FileInputStream;
021import java.io.FileOutputStream;
022import java.io.IOException;
023import java.nio.channels.FileChannel;
024import java.util.Iterator;
025import java.util.Locale;
026import java.util.Random;
027import java.util.Stack;
028
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032/**
033 * File utilities.
034 */
035public final class FileUtil {
036    
037    public static final int BUFFER_SIZE = 128 * 1024;
038
039    private static final Logger LOG = LoggerFactory.getLogger(FileUtil.class);
040    private static final int RETRY_SLEEP_MILLIS = 10;
041    /**
042     * The System property key for the user directory.
043     */
044    private static final String USER_DIR_KEY = "user.dir";
045    private static final File USER_DIR = new File(System.getProperty(USER_DIR_KEY));
046    private static File defaultTempDir;
047    private static Thread shutdownHook;
048    private static boolean windowsOs = initWindowsOs();
049
050    private FileUtil() {
051        // Utils method
052    }
053
054    private static boolean initWindowsOs() {
055        // initialize once as System.getProperty is not fast
056        String osName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
057        return osName.contains("windows");
058    }
059
060    public static File getUserDir() {
061        return USER_DIR;
062    }
063
064    /**
065     * Normalizes the path to cater for Windows and other platforms
066     */
067    public static String normalizePath(String path) {
068        if (path == null) {
069            return null;
070        }
071
072        if (isWindows()) {
073            // special handling for Windows where we need to convert / to \\
074            return path.replace('/', '\\');
075        } else {
076            // for other systems make sure we use / as separators
077            return path.replace('\\', '/');
078        }
079    }
080
081    /**
082     * Returns true, if the OS is windows
083     */
084    public static boolean isWindows() {
085        return windowsOs;
086    }
087
088    @Deprecated
089    public static File createTempFile(String prefix, String suffix) throws IOException {
090        return createTempFile(prefix, suffix, null);
091    }
092
093    public static File createTempFile(String prefix, String suffix, File parentDir) throws IOException {
094        // TODO: parentDir should be mandatory
095        File parent = (parentDir == null) ? getDefaultTempDir() : parentDir;
096            
097        if (suffix == null) {
098            suffix = ".tmp";
099        }
100        if (prefix == null) {
101            prefix = "camel";
102        } else if (prefix.length() < 3) {
103            prefix = prefix + "camel";
104        }
105
106        // create parent folder
107        parent.mkdirs();
108
109        return File.createTempFile(prefix, suffix, parent);
110    }
111
112    /**
113     * Strip any leading separators
114     */
115    public static String stripLeadingSeparator(String name) {
116        if (name == null) {
117            return null;
118        }
119        while (name.startsWith("/") || name.startsWith(File.separator)) {
120            name = name.substring(1);
121        }
122        return name;
123    }
124
125    /**
126     * Does the name start with a leading separator
127     */
128    public static boolean hasLeadingSeparator(String name) {
129        if (name == null) {
130            return false;
131        }
132        if (name.startsWith("/") || name.startsWith(File.separator)) {
133            return true;
134        }
135        return false;
136    }
137
138    /**
139     * Strip first leading separator
140     */
141    public static String stripFirstLeadingSeparator(String name) {
142        if (name == null) {
143            return null;
144        }
145        if (name.startsWith("/") || name.startsWith(File.separator)) {
146            name = name.substring(1);
147        }
148        return name;
149    }
150
151    /**
152     * Strip any trailing separators
153     */
154    public static String stripTrailingSeparator(String name) {
155        if (ObjectHelper.isEmpty(name)) {
156            return name;
157        }
158        
159        String s = name;
160        
161        // there must be some leading text, as we should only remove trailing separators 
162        while (s.endsWith("/") || s.endsWith(File.separator)) {
163            s = s.substring(0, s.length() - 1);
164        }
165        
166        // if the string is empty, that means there was only trailing slashes, and no leading text
167        // and so we should then return the original name as is
168        if (ObjectHelper.isEmpty(s)) {
169            return name;
170        } else {
171            // return without trailing slashes
172            return s;
173        }
174    }
175
176    /**
177     * Strips any leading paths
178     */
179    public static String stripPath(String name) {
180        if (name == null) {
181            return null;
182        }
183        int posUnix = name.lastIndexOf('/');
184        int posWin = name.lastIndexOf('\\');
185        int pos = Math.max(posUnix, posWin);
186
187        if (pos != -1) {
188            return name.substring(pos + 1);
189        }
190        return name;
191    }
192
193    public static String stripExt(String name) {
194        return stripExt(name, false);
195    }
196
197    public static String stripExt(String name, boolean singleMode) {
198        if (name == null) {
199            return null;
200        }
201
202        // the name may have a leading path
203        int posUnix = name.lastIndexOf('/');
204        int posWin = name.lastIndexOf('\\');
205        int pos = Math.max(posUnix, posWin);
206
207        if (pos > 0) {
208            String onlyName = name.substring(pos + 1);
209            int pos2 = singleMode ? onlyName.lastIndexOf('.') : onlyName.indexOf('.');
210            if (pos2 > 0) {
211                return name.substring(0, pos + pos2 + 1);
212            }
213        } else {
214            // if single ext mode, then only return last extension
215            int pos2 = singleMode ? name.lastIndexOf('.') : name.indexOf('.');
216            if (pos2 > 0) {
217                return name.substring(0, pos2);
218            }
219        }
220
221        return name;
222    }
223
224    public static String onlyExt(String name) {
225        return onlyExt(name, false);
226    }
227
228    public static String onlyExt(String name, boolean singleMode) {
229        if (name == null) {
230            return null;
231        }
232        name = stripPath(name);
233
234        // extension is the first dot, as a file may have double extension such as .tar.gz
235        // if single ext mode, then only return last extension
236        int pos = singleMode ? name.lastIndexOf('.') : name.indexOf('.');
237        if (pos != -1) {
238            return name.substring(pos + 1);
239        }
240        return null;
241    }
242
243    /**
244     * Returns only the leading path (returns <tt>null</tt> if no path)
245     */
246    public static String onlyPath(String name) {
247        if (name == null) {
248            return null;
249        }
250
251        int posUnix = name.lastIndexOf('/');
252        int posWin = name.lastIndexOf('\\');
253        int pos = Math.max(posUnix, posWin);
254
255        if (pos > 0) {
256            return name.substring(0, pos);
257        } else if (pos == 0) {
258            // name is in the root path, so extract the path as the first char
259            return name.substring(0, 1);
260        }
261        // no path in name
262        return null;
263    }
264
265    /**
266     * Compacts a path by stacking it and reducing <tt>..</tt>,
267     * and uses OS specific file separators (eg {@link java.io.File#separator}).
268     */
269    public static String compactPath(String path) {
270        return compactPath(path, "" + File.separatorChar);
271    }
272
273    /**
274     * Compacts a path by stacking it and reducing <tt>..</tt>,
275     * and uses the given separator.
276     *
277     */
278    public static String compactPath(String path, char separator) {
279        return compactPath(path, "" + separator);
280    }
281
282    /**
283     * Compacts a path by stacking it and reducing <tt>..</tt>,
284     * and uses the given separator.
285     */
286    public static String compactPath(String path, String separator) {
287        if (path == null) {
288            return null;
289        }
290        
291        // only normalize if contains a path separator
292        if (path.indexOf('/') == -1 && path.indexOf('\\') == -1)  {
293            return path;
294        }
295
296        // need to normalize path before compacting
297        path = normalizePath(path);
298
299        // preserve ending slash if given in input path
300        boolean endsWithSlash = path.endsWith("/") || path.endsWith("\\");
301
302        // preserve starting slash if given in input path
303        boolean startsWithSlash = path.startsWith("/") || path.startsWith("\\");
304        
305        Stack<String> stack = new Stack<String>();
306
307        // separator can either be windows or unix style
308        String separatorRegex = "\\\\|/";
309        String[] parts = path.split(separatorRegex);
310        for (String part : parts) {
311            if (part.equals("..") && !stack.isEmpty() && !"..".equals(stack.peek())) {
312                // only pop if there is a previous path, which is not a ".." path either
313                stack.pop();
314            } else if (part.equals(".") || part.isEmpty()) {
315                // do nothing because we don't want a path like foo/./bar or foo//bar
316            } else {
317                stack.push(part);
318            }
319        }
320
321        // build path based on stack
322        StringBuilder sb = new StringBuilder();
323        
324        if (startsWithSlash) {
325            sb.append(separator);
326        }
327        
328        for (Iterator<String> it = stack.iterator(); it.hasNext();) {
329            sb.append(it.next());
330            if (it.hasNext()) {
331                sb.append(separator);
332            }
333        }
334
335        if (endsWithSlash && stack.size() > 0) {
336            sb.append(separator);
337        }
338
339        return sb.toString();
340    }
341
342    @Deprecated
343    private static synchronized File getDefaultTempDir() {
344        if (defaultTempDir != null && defaultTempDir.exists()) {
345            return defaultTempDir;
346        }
347
348        defaultTempDir = createNewTempDir();
349
350        // create shutdown hook to remove the temp dir
351        shutdownHook = new Thread() {
352            @Override
353            public void run() {
354                removeDir(defaultTempDir);
355            }
356        };
357        Runtime.getRuntime().addShutdownHook(shutdownHook);
358
359        return defaultTempDir;
360    }
361
362    /**
363     * Creates a new temporary directory in the <tt>java.io.tmpdir</tt> directory.
364     */
365    @Deprecated
366    private static File createNewTempDir() {
367        String s = System.getProperty("java.io.tmpdir");
368        File checkExists = new File(s);
369        if (!checkExists.exists()) {
370            throw new RuntimeException("The directory "
371                                   + checkExists.getAbsolutePath()
372                                   + " does not exist, please set java.io.tempdir"
373                                   + " to an existing directory");
374        }
375        
376        if (!checkExists.canWrite()) {
377            throw new RuntimeException("The directory "
378                + checkExists.getAbsolutePath()
379                + " is not writable, please set java.io.tempdir"
380                + " to a writable directory");
381        }
382
383        // create a sub folder with a random number
384        Random ran = new Random();
385        int x = ran.nextInt(1000000);
386        File f = new File(s, "camel-tmp-" + x);
387        int count = 0;
388        // Let us just try 100 times to avoid the infinite loop
389        while (!f.mkdir()) {
390            count++;
391            if (count >= 100) {
392                throw new RuntimeException("Camel cannot a temp directory from"
393                    + checkExists.getAbsolutePath()
394                    + " 100 times , please set java.io.tempdir"
395                    + " to a writable directory");
396            }
397            x = ran.nextInt(1000000);
398            f = new File(s, "camel-tmp-" + x);
399        }
400
401        return f;
402    }
403
404    /**
405     * Shutdown and cleanup the temporary directory and removes any shutdown hooks in use.
406     */
407    @Deprecated
408    public static synchronized void shutdown() {
409        if (defaultTempDir != null && defaultTempDir.exists()) {
410            removeDir(defaultTempDir);
411        }
412
413        if (shutdownHook != null) {
414            Runtime.getRuntime().removeShutdownHook(shutdownHook);
415            shutdownHook = null;
416        }
417    }
418
419    public static void removeDir(File d) {
420        String[] list = d.list();
421        if (list == null) {
422            list = new String[0];
423        }
424        for (String s : list) {
425            File f = new File(d, s);
426            if (f.isDirectory()) {
427                removeDir(f);
428            } else {
429                delete(f);
430            }
431        }
432        delete(d);
433    }
434
435    private static void delete(File f) {
436        if (!f.delete()) {
437            if (isWindows()) {
438                System.gc();
439            }
440            try {
441                Thread.sleep(RETRY_SLEEP_MILLIS);
442            } catch (InterruptedException ex) {
443                // Ignore Exception
444            }
445            if (!f.delete()) {
446                f.deleteOnExit();
447            }
448        }
449    }
450
451    /**
452     * Renames a file.
453     *
454     * @param from the from file
455     * @param to   the to file
456     * @param copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails
457     * @return <tt>true</tt> if the file was renamed, otherwise <tt>false</tt>
458     * @throws java.io.IOException is thrown if error renaming file
459     */
460    public static boolean renameFile(File from, File to, boolean copyAndDeleteOnRenameFail) throws IOException {
461        // do not try to rename non existing files
462        if (!from.exists()) {
463            return false;
464        }
465
466        // some OS such as Windows can have problem doing rename IO operations so we may need to
467        // retry a couple of times to let it work
468        boolean renamed = false;
469        int count = 0;
470        while (!renamed && count < 3) {
471            if (LOG.isDebugEnabled() && count > 0) {
472                LOG.debug("Retrying attempt {} to rename file from: {} to: {}", new Object[]{count, from, to});
473            }
474
475            renamed = from.renameTo(to);
476            if (!renamed && count > 0) {
477                try {
478                    Thread.sleep(1000);
479                } catch (InterruptedException e) {
480                    // ignore
481                }
482            }
483            count++;
484        }
485
486        // we could not rename using renameTo, so lets fallback and do a copy/delete approach.
487        // for example if you move files between different file systems (linux -> windows etc.)
488        if (!renamed && copyAndDeleteOnRenameFail) {
489            // now do a copy and delete as all rename attempts failed
490            LOG.debug("Cannot rename file from: {} to: {}, will now use a copy/delete approach instead", from, to);
491            renamed = renameFileUsingCopy(from, to);
492        }
493
494        if (LOG.isDebugEnabled() && count > 0) {
495            LOG.debug("Tried {} to rename file: {} to: {} with result: {}", new Object[]{count, from, to, renamed});
496        }
497        return renamed;
498    }
499
500    /**
501     * Rename file using copy and delete strategy. This is primarily used in
502     * environments where the regular rename operation is unreliable.
503     * 
504     * @param from the file to be renamed
505     * @param to the new target file
506     * @return <tt>true</tt> if the file was renamed successfully, otherwise <tt>false</tt>
507     * @throws IOException If an I/O error occurs during copy or delete operations.
508     */
509    public static boolean renameFileUsingCopy(File from, File to) throws IOException {
510        // do not try to rename non existing files
511        if (!from.exists()) {
512            return false;
513        }
514
515        LOG.debug("Rename file '{}' to '{}' using copy/delete strategy.", from, to);
516
517        copyFile(from, to);
518        if (!deleteFile(from)) {
519            throw new IOException("Renaming file from '" + from + "' to '" + to + "' failed: Cannot delete file '" + from + "' after copy succeeded");
520        }
521
522        return true;
523    }
524
525    /**
526     * Copies the file
527     *
528     * @param from  the source file
529     * @param to    the destination file
530     * @throws IOException If an I/O error occurs during copy operation
531     */
532    public static void copyFile(File from, File to) throws IOException {
533        FileChannel in = null;
534        FileChannel out = null;
535        try {
536            in = new FileInputStream(from).getChannel();
537            out = new FileOutputStream(to).getChannel();
538            if (LOG.isTraceEnabled()) {
539                LOG.trace("Using FileChannel to copy from: " + in + " to: " + out);
540            }
541
542            long size = in.size();
543            long position = 0;
544            while (position < size) {
545                position += in.transferTo(position, BUFFER_SIZE, out);
546            }
547        } finally {
548            IOHelper.close(in, from.getName(), LOG);
549            IOHelper.close(out, to.getName(), LOG);
550        }
551    }
552
553    /**
554     * Deletes the file.
555     * <p/>
556     * This implementation will attempt to delete the file up till three times with one second delay, which
557     * can mitigate problems on deleting files on some platforms such as Windows.
558     *
559     * @param file  the file to delete
560     */
561    public static boolean deleteFile(File file) {
562        // do not try to delete non existing files
563        if (!file.exists()) {
564            return false;
565        }
566
567        // some OS such as Windows can have problem doing delete IO operations so we may need to
568        // retry a couple of times to let it work
569        boolean deleted = false;
570        int count = 0;
571        while (!deleted && count < 3) {
572            LOG.debug("Retrying attempt {} to delete file: {}", count, file);
573
574            deleted = file.delete();
575            if (!deleted && count > 0) {
576                try {
577                    Thread.sleep(1000);
578                } catch (InterruptedException e) {
579                    // ignore
580                }
581            }
582            count++;
583        }
584
585
586        if (LOG.isDebugEnabled() && count > 0) {
587            LOG.debug("Tried {} to delete file: {} with result: {}", new Object[]{count, file, deleted});
588        }
589        return deleted;
590    }
591
592    /**
593     * Is the given file an absolute file.
594     * <p/>
595     * Will also work around issue on Windows to consider files on Windows starting with a \
596     * as absolute files. This makes the logic consistent across all OS platforms.
597     *
598     * @param file  the file
599     * @return <tt>true</ff> if its an absolute path, <tt>false</tt> otherwise.
600     */
601    public static boolean isAbsolute(File file) {
602        if (isWindows()) {
603            // special for windows
604            String path = file.getPath();
605            if (path.startsWith(File.separator)) {
606                return true;
607            }
608        }
609        return file.isAbsolute();
610    }
611
612    /**
613     * Creates a new file.
614     *
615     * @param file the file
616     * @return <tt>true</tt> if created a new file, <tt>false</tt> otherwise
617     * @throws IOException is thrown if error creating the new file
618     */
619    public static boolean createNewFile(File file) throws IOException {
620        // need to check first
621        if (file.exists()) {
622            return false;
623        }
624        try {
625            return file.createNewFile();
626        } catch (IOException e) {
627            // and check again if the file was created as createNewFile may create the file
628            // but throw a permission error afterwards when using some NAS
629            if (file.exists()) {
630                return true;
631            } else {
632                throw e;
633            }
634        }
635    }
636
637}