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    import java.io.File;
020    import java.io.FileInputStream;
021    import java.io.FileOutputStream;
022    import java.io.IOException;
023    import java.nio.channels.FileChannel;
024    import java.util.Iterator;
025    import java.util.Locale;
026    import java.util.Random;
027    import java.util.Stack;
028    
029    import org.slf4j.Logger;
030    import org.slf4j.LoggerFactory;
031    
032    /**
033     * File utilities.
034     */
035    public 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.US);
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            // create a sub folder with a random number
329            Random ran = new Random();
330            int x = ran.nextInt(1000000);
331    
332            File f = new File(s, "camel-tmp-" + x);
333            while (!f.mkdir()) {
334                x = ran.nextInt(1000000);
335                f = new File(s, "camel-tmp-" + x);
336            }
337    
338            return f;
339        }
340    
341        /**
342         * Shutdown and cleanup the temporary directory and removes any shutdown hooks in use.
343         */
344        @Deprecated
345        public static synchronized void shutdown() {
346            if (defaultTempDir != null && defaultTempDir.exists()) {
347                removeDir(defaultTempDir);
348            }
349    
350            if (shutdownHook != null) {
351                Runtime.getRuntime().removeShutdownHook(shutdownHook);
352                shutdownHook = null;
353            }
354        }
355    
356        public static void removeDir(File d) {
357            String[] list = d.list();
358            if (list == null) {
359                list = new String[0];
360            }
361            for (String s : list) {
362                File f = new File(d, s);
363                if (f.isDirectory()) {
364                    removeDir(f);
365                } else {
366                    delete(f);
367                }
368            }
369            delete(d);
370        }
371    
372        private static void delete(File f) {
373            if (!f.delete()) {
374                if (isWindows()) {
375                    System.gc();
376                }
377                try {
378                    Thread.sleep(RETRY_SLEEP_MILLIS);
379                } catch (InterruptedException ex) {
380                    // Ignore Exception
381                }
382                if (!f.delete()) {
383                    f.deleteOnExit();
384                }
385            }
386        }
387    
388        /**
389         * Renames a file.
390         *
391         * @param from the from file
392         * @param to   the to file
393         * @param copyAndDeleteOnRenameFail whether to fallback and do copy and delete, if renameTo fails
394         * @return <tt>true</tt> if the file was renamed, otherwise <tt>false</tt>
395         * @throws java.io.IOException is thrown if error renaming file
396         */
397        public static boolean renameFile(File from, File to, boolean copyAndDeleteOnRenameFail) throws IOException {
398            // do not try to rename non existing files
399            if (!from.exists()) {
400                return false;
401            }
402    
403            // some OS such as Windows can have problem doing rename IO operations so we may need to
404            // retry a couple of times to let it work
405            boolean renamed = false;
406            int count = 0;
407            while (!renamed && count < 3) {
408                if (LOG.isDebugEnabled() && count > 0) {
409                    LOG.debug("Retrying attempt {} to rename file from: {} to: {}", new Object[]{count, from, to});
410                }
411    
412                renamed = from.renameTo(to);
413                if (!renamed && count > 0) {
414                    try {
415                        Thread.sleep(1000);
416                    } catch (InterruptedException e) {
417                        // ignore
418                    }
419                }
420                count++;
421            }
422    
423            // we could not rename using renameTo, so lets fallback and do a copy/delete approach.
424            // for example if you move files between different file systems (linux -> windows etc.)
425            if (!renamed && copyAndDeleteOnRenameFail) {
426                // now do a copy and delete as all rename attempts failed
427                LOG.debug("Cannot rename file from: {} to: {}, will now use a copy/delete approach instead", from, to);
428                copyFile(from, to);
429                if (!deleteFile(from)) {
430                    throw new IOException("Renaming file from: " + from + " to: " + to + " failed due cannot delete from file: " + from + " after copy succeeded");
431                } else {
432                    renamed = true;
433                }
434            }
435    
436            if (LOG.isDebugEnabled() && count > 0) {
437                LOG.debug("Tried {} to rename file: {} to: {} with result: {}", new Object[]{count, from, to, renamed});
438            }
439            return renamed;
440        }
441    
442        public static void copyFile(File from, File to) throws IOException {
443            FileChannel in = new FileInputStream(from).getChannel();
444            FileChannel out = new FileOutputStream(to).getChannel();
445            try {
446                if (LOG.isTraceEnabled()) {
447                    LOG.trace("Using FileChannel to copy from: " + in + " to: " + out);
448                }
449    
450                long size = in.size();
451                long position = 0;
452                while (position < size) {
453                    position += in.transferTo(position, BUFFER_SIZE, out);
454                }
455            } finally {
456                IOHelper.close(in, from.getName(), LOG);
457                IOHelper.close(out, to.getName(), LOG);
458            }
459        }
460    
461        public static boolean deleteFile(File file) {
462            // do not try to delete non existing files
463            if (!file.exists()) {
464                return false;
465            }
466    
467            // some OS such as Windows can have problem doing delete IO operations so we may need to
468            // retry a couple of times to let it work
469            boolean deleted = false;
470            int count = 0;
471            while (!deleted && count < 3) {
472                LOG.debug("Retrying attempt {} to delete file: {}", count, file);
473    
474                deleted = file.delete();
475                if (!deleted && count > 0) {
476                    try {
477                        Thread.sleep(1000);
478                    } catch (InterruptedException e) {
479                        // ignore
480                    }
481                }
482                count++;
483            }
484    
485    
486            if (LOG.isDebugEnabled() && count > 0) {
487                LOG.debug("Tried {} to delete file: {} with result: {}", new Object[]{count, file, deleted});
488            }
489            return deleted;
490        }
491    
492        /**
493         * Is the given file an absolute file.
494         * <p/>
495         * Will also work around issue on Windows to consider files on Windows starting with a \
496         * as absolute files. This makes the logic consistent across all OS platforms.
497         *
498         * @param file  the file
499         * @return <tt>true</ff> if its an absolute path, <tt>false</tt> otherwise.
500         */
501        public static boolean isAbsolute(File file) {
502            if (isWindows()) {
503                // special for windows
504                String path = file.getPath();
505                if (path.startsWith(File.separator)) {
506                    return true;
507                }
508            }
509            return file.isAbsolute();
510        }
511    
512        /**
513         * Creates a new file.
514         *
515         * @param file the file
516         * @return <tt>true</tt> if created a new file, <tt>false</tt> otherwise
517         * @throws IOException is thrown if error creating the new file
518         */
519        public static boolean createNewFile(File file) throws IOException {
520            try {
521                return file.createNewFile();
522            } catch (IOException e) {
523                if (file.exists()) {
524                    return true;
525                } else {
526                    throw e;
527                }
528            }
529        }
530    
531    }