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    public static String compactPath(String path, char separator) {
278        if (path == null) {
279            return null;
280        }
281        
282        // only normalize if contains a path separator
283        if (path.indexOf('/') == -1 && path.indexOf('\\') == -1)  {
284            return path;
285        }
286
287        // need to normalize path before compacting
288        path = normalizePath(path);
289
290        // preserve ending slash if given in input path
291        boolean endsWithSlash = path.endsWith("/") || path.endsWith("\\");
292
293        // preserve starting slash if given in input path
294        boolean startsWithSlash = path.startsWith("/") || path.startsWith("\\");
295        
296        Stack<String> stack = new Stack<String>();
297
298        // separator can either be windows or unix style
299        String separatorRegex = "\\\\|/";
300        String[] parts = path.split(separatorRegex);
301        for (String part : parts) {
302            if (part.equals("..") && !stack.isEmpty() && !"..".equals(stack.peek())) {
303                // only pop if there is a previous path, which is not a ".." path either
304                stack.pop();
305            } else if (part.equals(".") || part.isEmpty()) {
306                // do nothing because we don't want a path like foo/./bar or foo//bar
307            } else {
308                stack.push(part);
309            }
310        }
311
312        // build path based on stack
313        StringBuilder sb = new StringBuilder();
314        
315        if (startsWithSlash) {
316            sb.append(separator);
317        }
318        
319        for (Iterator<String> it = stack.iterator(); it.hasNext();) {
320            sb.append(it.next());
321            if (it.hasNext()) {
322                sb.append(separator);
323            }
324        }
325
326        if (endsWithSlash && stack.size() > 0) {
327            sb.append(separator);
328        }
329
330        return sb.toString();
331    }
332
333    @Deprecated
334    private static synchronized File getDefaultTempDir() {
335        if (defaultTempDir != null && defaultTempDir.exists()) {
336            return defaultTempDir;
337        }
338
339        defaultTempDir = createNewTempDir();
340
341        // create shutdown hook to remove the temp dir
342        shutdownHook = new Thread() {
343            @Override
344            public void run() {
345                removeDir(defaultTempDir);
346            }
347        };
348        Runtime.getRuntime().addShutdownHook(shutdownHook);
349
350        return defaultTempDir;
351    }
352
353    /**
354     * Creates a new temporary directory in the <tt>java.io.tmpdir</tt> directory.
355     */
356    @Deprecated
357    private static File createNewTempDir() {
358        String s = System.getProperty("java.io.tmpdir");
359        File checkExists = new File(s);
360        if (!checkExists.exists()) {
361            throw new RuntimeException("The directory "
362                                   + checkExists.getAbsolutePath()
363                                   + " does not exist, please set java.io.tempdir"
364                                   + " to an existing directory");
365        }
366        
367        if (!checkExists.canWrite()) {
368            throw new RuntimeException("The directory "
369                + checkExists.getAbsolutePath()
370                + " is not writable, please set java.io.tempdir"
371                + " to a writable directory");
372        }
373
374        // create a sub folder with a random number
375        Random ran = new Random();
376        int x = ran.nextInt(1000000);
377        File f = new File(s, "camel-tmp-" + x);
378        int count = 0;
379        // Let us just try 100 times to avoid the infinite loop
380        while (!f.mkdir()) {
381            count++;
382            if (count >= 100) {
383                throw new RuntimeException("Camel cannot a temp directory from"
384                    + checkExists.getAbsolutePath()
385                    + " 100 times , please set java.io.tempdir"
386                    + " to a writable directory");
387            }
388            x = ran.nextInt(1000000);
389            f = new File(s, "camel-tmp-" + x);
390        }
391
392        return f;
393    }
394
395    /**
396     * Shutdown and cleanup the temporary directory and removes any shutdown hooks in use.
397     */
398    @Deprecated
399    public static synchronized void shutdown() {
400        if (defaultTempDir != null && defaultTempDir.exists()) {
401            removeDir(defaultTempDir);
402        }
403
404        if (shutdownHook != null) {
405            Runtime.getRuntime().removeShutdownHook(shutdownHook);
406            shutdownHook = null;
407        }
408    }
409
410    public static void removeDir(File d) {
411        String[] list = d.list();
412        if (list == null) {
413            list = new String[0];
414        }
415        for (String s : list) {
416            File f = new File(d, s);
417            if (f.isDirectory()) {
418                removeDir(f);
419            } else {
420                delete(f);
421            }
422        }
423        delete(d);
424    }
425
426    private static void delete(File f) {
427        if (!f.delete()) {
428            if (isWindows()) {
429                System.gc();
430            }
431            try {
432                Thread.sleep(RETRY_SLEEP_MILLIS);
433            } catch (InterruptedException ex) {
434                // Ignore Exception
435            }
436            if (!f.delete()) {
437                f.deleteOnExit();
438            }
439        }
440    }
441
442    /**
443     * Renames a file.
444     *
445     * @param from the from file
446     * @param to   the to file
447     * @param copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails
448     * @return <tt>true</tt> if the file was renamed, otherwise <tt>false</tt>
449     * @throws java.io.IOException is thrown if error renaming file
450     */
451    public static boolean renameFile(File from, File to, boolean copyAndDeleteOnRenameFail) throws IOException {
452        // do not try to rename non existing files
453        if (!from.exists()) {
454            return false;
455        }
456
457        // some OS such as Windows can have problem doing rename IO operations so we may need to
458        // retry a couple of times to let it work
459        boolean renamed = false;
460        int count = 0;
461        while (!renamed && count < 3) {
462            if (LOG.isDebugEnabled() && count > 0) {
463                LOG.debug("Retrying attempt {} to rename file from: {} to: {}", new Object[]{count, from, to});
464            }
465
466            renamed = from.renameTo(to);
467            if (!renamed && count > 0) {
468                try {
469                    Thread.sleep(1000);
470                } catch (InterruptedException e) {
471                    // ignore
472                }
473            }
474            count++;
475        }
476
477        // we could not rename using renameTo, so lets fallback and do a copy/delete approach.
478        // for example if you move files between different file systems (linux -> windows etc.)
479        if (!renamed && copyAndDeleteOnRenameFail) {
480            // now do a copy and delete as all rename attempts failed
481            LOG.debug("Cannot rename file from: {} to: {}, will now use a copy/delete approach instead", from, to);
482            renamed = renameFileUsingCopy(from, to);
483        }
484
485        if (LOG.isDebugEnabled() && count > 0) {
486            LOG.debug("Tried {} to rename file: {} to: {} with result: {}", new Object[]{count, from, to, renamed});
487        }
488        return renamed;
489    }
490
491    /**
492     * Rename file using copy and delete strategy. This is primarily used in
493     * environments where the regular rename operation is unreliable.
494     * 
495     * @param from the file to be renamed
496     * @param to the new target file
497     * @return <tt>true</tt> if the file was renamed successfully, otherwise <tt>false</tt>
498     * @throws IOException If an I/O error occurs during copy or delete operations.
499     */
500    public static boolean renameFileUsingCopy(File from, File to) throws IOException {
501        // do not try to rename non existing files
502        if (!from.exists()) {
503            return false;
504        }
505
506        LOG.debug("Rename file '{}' to '{}' using copy/delete strategy.", from, to);
507
508        copyFile(from, to);
509        if (!deleteFile(from)) {
510            throw new IOException("Renaming file from '" + from + "' to '" + to + "' failed: Cannot delete file '" + from + "' after copy succeeded");
511        }
512
513        return true;
514    }
515
516    public static void copyFile(File from, File to) throws IOException {
517        FileChannel in = null;
518        FileChannel out = null;
519        try {
520            in = new FileInputStream(from).getChannel();
521            out = new FileOutputStream(to).getChannel();
522            if (LOG.isTraceEnabled()) {
523                LOG.trace("Using FileChannel to copy from: " + in + " to: " + out);
524            }
525
526            long size = in.size();
527            long position = 0;
528            while (position < size) {
529                position += in.transferTo(position, BUFFER_SIZE, out);
530            }
531        } finally {
532            IOHelper.close(in, from.getName(), LOG);
533            IOHelper.close(out, to.getName(), LOG);
534        }
535    }
536
537    public static boolean deleteFile(File file) {
538        // do not try to delete non existing files
539        if (!file.exists()) {
540            return false;
541        }
542
543        // some OS such as Windows can have problem doing delete IO operations so we may need to
544        // retry a couple of times to let it work
545        boolean deleted = false;
546        int count = 0;
547        while (!deleted && count < 3) {
548            LOG.debug("Retrying attempt {} to delete file: {}", count, file);
549
550            deleted = file.delete();
551            if (!deleted && count > 0) {
552                try {
553                    Thread.sleep(1000);
554                } catch (InterruptedException e) {
555                    // ignore
556                }
557            }
558            count++;
559        }
560
561
562        if (LOG.isDebugEnabled() && count > 0) {
563            LOG.debug("Tried {} to delete file: {} with result: {}", new Object[]{count, file, deleted});
564        }
565        return deleted;
566    }
567
568    /**
569     * Is the given file an absolute file.
570     * <p/>
571     * Will also work around issue on Windows to consider files on Windows starting with a \
572     * as absolute files. This makes the logic consistent across all OS platforms.
573     *
574     * @param file  the file
575     * @return <tt>true</ff> if its an absolute path, <tt>false</tt> otherwise.
576     */
577    public static boolean isAbsolute(File file) {
578        if (isWindows()) {
579            // special for windows
580            String path = file.getPath();
581            if (path.startsWith(File.separator)) {
582                return true;
583            }
584        }
585        return file.isAbsolute();
586    }
587
588    /**
589     * Creates a new file.
590     *
591     * @param file the file
592     * @return <tt>true</tt> if created a new file, <tt>false</tt> otherwise
593     * @throws IOException is thrown if error creating the new file
594     */
595    public static boolean createNewFile(File file) throws IOException {
596        // need to check first
597        if (file.exists()) {
598            return false;
599        }
600        try {
601            return file.createNewFile();
602        } catch (IOException e) {
603            // and check again if the file was created as createNewFile may create the file
604            // but throw a permission error afterwards when using some NAS
605            if (file.exists()) {
606                return true;
607            } else {
608                throw e;
609            }
610        }
611    }
612
613}