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        if (name == null) {
195            return null;
196        }
197        int pos = name.lastIndexOf('.');
198        if (pos != -1) {
199            return name.substring(0, pos);
200        }
201        return name;
202    }
203
204    /**
205     * Returns only the leading path (returns <tt>null</tt> if no path)
206     */
207    public static String onlyPath(String name) {
208        if (name == null) {
209            return null;
210        }
211
212        int posUnix = name.lastIndexOf('/');
213        int posWin = name.lastIndexOf('\\');
214        int pos = Math.max(posUnix, posWin);
215
216        if (pos > 0) {
217            return name.substring(0, pos);
218        } else if (pos == 0) {
219            // name is in the root path, so extract the path as the first char
220            return name.substring(0, 1);
221        }
222        // no path in name
223        return null;
224    }
225
226    /**
227     * Compacts a path by stacking it and reducing <tt>..</tt>,
228     * and uses OS specific file separators (eg {@link java.io.File#separator}).
229     */
230    public static String compactPath(String path) {
231        return compactPath(path, File.separatorChar);
232    }
233
234    /**
235     * Compacts a path by stacking it and reducing <tt>..</tt>,
236     * and uses the given separator.
237     */
238    public static String compactPath(String path, char separator) {
239        if (path == null) {
240            return null;
241        }
242        
243        // only normalize if contains a path separator
244        if (path.indexOf('/') == -1 && path.indexOf('\\') == -1)  {
245            return path;
246        }
247
248        // need to normalize path before compacting
249        path = normalizePath(path);
250
251        // preserve ending slash if given in input path
252        boolean endsWithSlash = path.endsWith("/") || path.endsWith("\\");
253
254        // preserve starting slash if given in input path
255        boolean startsWithSlash = path.startsWith("/") || path.startsWith("\\");
256        
257        Stack<String> stack = new Stack<String>();
258
259        // separator can either be windows or unix style
260        String separatorRegex = "\\\\|/";
261        String[] parts = path.split(separatorRegex);
262        for (String part : parts) {
263            if (part.equals("..") && !stack.isEmpty() && !"..".equals(stack.peek())) {
264                // only pop if there is a previous path, which is not a ".." path either
265                stack.pop();
266            } else if (part.equals(".") || part.isEmpty()) {
267                // do nothing because we don't want a path like foo/./bar or foo//bar
268            } else {
269                stack.push(part);
270            }
271        }
272
273        // build path based on stack
274        StringBuilder sb = new StringBuilder();
275        
276        if (startsWithSlash) {
277            sb.append(separator);
278        }
279        
280        for (Iterator<String> it = stack.iterator(); it.hasNext();) {
281            sb.append(it.next());
282            if (it.hasNext()) {
283                sb.append(separator);
284            }
285        }
286
287        if (endsWithSlash && stack.size() > 0) {
288            sb.append(separator);
289        }
290
291        return sb.toString();
292    }
293
294    @Deprecated
295    private static synchronized File getDefaultTempDir() {
296        if (defaultTempDir != null && defaultTempDir.exists()) {
297            return defaultTempDir;
298        }
299
300        defaultTempDir = createNewTempDir();
301
302        // create shutdown hook to remove the temp dir
303        shutdownHook = new Thread() {
304            @Override
305            public void run() {
306                removeDir(defaultTempDir);
307            }
308        };
309        Runtime.getRuntime().addShutdownHook(shutdownHook);
310
311        return defaultTempDir;
312    }
313
314    /**
315     * Creates a new temporary directory in the <tt>java.io.tmpdir</tt> directory.
316     */
317    @Deprecated
318    private static File createNewTempDir() {
319        String s = System.getProperty("java.io.tmpdir");
320        File checkExists = new File(s);
321        if (!checkExists.exists()) {
322            throw new RuntimeException("The directory "
323                                   + checkExists.getAbsolutePath()
324                                   + " does not exist, please set java.io.tempdir"
325                                   + " to an existing directory");
326        }
327        
328        if (!checkExists.canWrite()) {
329            throw new RuntimeException("The directory "
330                + checkExists.getAbsolutePath()
331                + " is not writable, please set java.io.tempdir"
332                + " to a writable directory");
333        }
334
335        // create a sub folder with a random number
336        Random ran = new Random();
337        int x = ran.nextInt(1000000);
338        File f = new File(s, "camel-tmp-" + x);
339        int count = 0;
340        // Let us just try 100 times to avoid the infinite loop
341        while (!f.mkdir()) {
342            count++;
343            if (count >= 100) {
344                throw new RuntimeException("Camel cannot a temp directory from"
345                    + checkExists.getAbsolutePath()
346                    + " 100 times , please set java.io.tempdir"
347                    + " to a writable directory");
348            }
349            x = ran.nextInt(1000000);
350            f = new File(s, "camel-tmp-" + x);
351        }
352
353        return f;
354    }
355
356    /**
357     * Shutdown and cleanup the temporary directory and removes any shutdown hooks in use.
358     */
359    @Deprecated
360    public static synchronized void shutdown() {
361        if (defaultTempDir != null && defaultTempDir.exists()) {
362            removeDir(defaultTempDir);
363        }
364
365        if (shutdownHook != null) {
366            Runtime.getRuntime().removeShutdownHook(shutdownHook);
367            shutdownHook = null;
368        }
369    }
370
371    public static void removeDir(File d) {
372        String[] list = d.list();
373        if (list == null) {
374            list = new String[0];
375        }
376        for (String s : list) {
377            File f = new File(d, s);
378            if (f.isDirectory()) {
379                removeDir(f);
380            } else {
381                delete(f);
382            }
383        }
384        delete(d);
385    }
386
387    private static void delete(File f) {
388        if (!f.delete()) {
389            if (isWindows()) {
390                System.gc();
391            }
392            try {
393                Thread.sleep(RETRY_SLEEP_MILLIS);
394            } catch (InterruptedException ex) {
395                // Ignore Exception
396            }
397            if (!f.delete()) {
398                f.deleteOnExit();
399            }
400        }
401    }
402
403    /**
404     * Renames a file.
405     *
406     * @param from the from file
407     * @param to   the to file
408     * @param copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails
409     * @return <tt>true</tt> if the file was renamed, otherwise <tt>false</tt>
410     * @throws java.io.IOException is thrown if error renaming file
411     */
412    public static boolean renameFile(File from, File to, boolean copyAndDeleteOnRenameFail) throws IOException {
413        // do not try to rename non existing files
414        if (!from.exists()) {
415            return false;
416        }
417
418        // some OS such as Windows can have problem doing rename IO operations so we may need to
419        // retry a couple of times to let it work
420        boolean renamed = false;
421        int count = 0;
422        while (!renamed && count < 3) {
423            if (LOG.isDebugEnabled() && count > 0) {
424                LOG.debug("Retrying attempt {} to rename file from: {} to: {}", new Object[]{count, from, to});
425            }
426
427            renamed = from.renameTo(to);
428            if (!renamed && count > 0) {
429                try {
430                    Thread.sleep(1000);
431                } catch (InterruptedException e) {
432                    // ignore
433                }
434            }
435            count++;
436        }
437
438        // we could not rename using renameTo, so lets fallback and do a copy/delete approach.
439        // for example if you move files between different file systems (linux -> windows etc.)
440        if (!renamed && copyAndDeleteOnRenameFail) {
441            // now do a copy and delete as all rename attempts failed
442            LOG.debug("Cannot rename file from: {} to: {}, will now use a copy/delete approach instead", from, to);
443            renamed = renameFileUsingCopy(from, to);
444        }
445
446        if (LOG.isDebugEnabled() && count > 0) {
447            LOG.debug("Tried {} to rename file: {} to: {} with result: {}", new Object[]{count, from, to, renamed});
448        }
449        return renamed;
450    }
451
452    /**
453     * Rename file using copy and delete strategy. This is primarily used in
454     * environments where the regular rename operation is unreliable.
455     * 
456     * @param from the file to be renamed
457     * @param to the new target file
458     * @return <tt>true</tt> if the file was renamed successfully, otherwise <tt>false</tt>
459     * @throws IOException If an I/O error occurs during copy or delete operations.
460     */
461    public static boolean renameFileUsingCopy(File from, File to) throws IOException {
462        // do not try to rename non existing files
463        if (!from.exists()) {
464            return false;
465        }
466
467        LOG.debug("Rename file '{}' to '{}' using copy/delete strategy.", from, to);
468
469        copyFile(from, to);
470        if (!deleteFile(from)) {
471            throw new IOException("Renaming file from '" + from + "' to '" + to + "' failed: Cannot delete file '" + from + "' after copy succeeded");
472        }
473
474        return true;
475    }
476
477    public static void copyFile(File from, File to) throws IOException {
478        FileChannel in = null;
479        FileChannel out = null;
480        try {
481            in = new FileInputStream(from).getChannel();
482            out = new FileOutputStream(to).getChannel();
483            if (LOG.isTraceEnabled()) {
484                LOG.trace("Using FileChannel to copy from: " + in + " to: " + out);
485            }
486
487            long size = in.size();
488            long position = 0;
489            while (position < size) {
490                position += in.transferTo(position, BUFFER_SIZE, out);
491            }
492        } finally {
493            IOHelper.close(in, from.getName(), LOG);
494            IOHelper.close(out, to.getName(), LOG);
495        }
496    }
497
498    public static boolean deleteFile(File file) {
499        // do not try to delete non existing files
500        if (!file.exists()) {
501            return false;
502        }
503
504        // some OS such as Windows can have problem doing delete IO operations so we may need to
505        // retry a couple of times to let it work
506        boolean deleted = false;
507        int count = 0;
508        while (!deleted && count < 3) {
509            LOG.debug("Retrying attempt {} to delete file: {}", count, file);
510
511            deleted = file.delete();
512            if (!deleted && count > 0) {
513                try {
514                    Thread.sleep(1000);
515                } catch (InterruptedException e) {
516                    // ignore
517                }
518            }
519            count++;
520        }
521
522
523        if (LOG.isDebugEnabled() && count > 0) {
524            LOG.debug("Tried {} to delete file: {} with result: {}", new Object[]{count, file, deleted});
525        }
526        return deleted;
527    }
528
529    /**
530     * Is the given file an absolute file.
531     * <p/>
532     * Will also work around issue on Windows to consider files on Windows starting with a \
533     * as absolute files. This makes the logic consistent across all OS platforms.
534     *
535     * @param file  the file
536     * @return <tt>true</ff> if its an absolute path, <tt>false</tt> otherwise.
537     */
538    public static boolean isAbsolute(File file) {
539        if (isWindows()) {
540            // special for windows
541            String path = file.getPath();
542            if (path.startsWith(File.separator)) {
543                return true;
544            }
545        }
546        return file.isAbsolute();
547    }
548
549    /**
550     * Creates a new file.
551     *
552     * @param file the file
553     * @return <tt>true</tt> if created a new file, <tt>false</tt> otherwise
554     * @throws IOException is thrown if error creating the new file
555     */
556    public static boolean createNewFile(File file) throws IOException {
557        try {
558            return file.createNewFile();
559        } catch (IOException e) {
560            if (file.exists()) {
561                return true;
562            } else {
563                throw e;
564            }
565        }
566    }
567
568}