001    /*--------------------------------------------------------------------------+
002    $Id: FileSystemUtils.java 29722 2010-08-16 13:40:26Z deissenb $
003    |                                                                          |
004    | Copyright 2005-2010 Technische Universitaet Muenchen                     |
005    |                                                                          |
006    | Licensed under the Apache License, Version 2.0 (the "License");          |
007    | you may not use this file except in compliance with the License.         |
008    | You may obtain a copy of the License at                                  |
009    |                                                                          |
010    |    http://www.apache.org/licenses/LICENSE-2.0                            |
011    |                                                                          |
012    | Unless required by applicable law or agreed to in writing, software      |
013    | distributed under the License is distributed on an "AS IS" BASIS,        |
014    | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
015    | See the License for the specific language governing permissions and      |
016    | limitations under the License.                                           |
017    +--------------------------------------------------------------------------*/
018    package edu.tum.cs.commons.filesystem;
019    
020    import java.io.BufferedInputStream;
021    import java.io.ByteArrayInputStream;
022    import java.io.Closeable;
023    import java.io.EOFException;
024    import java.io.File;
025    import java.io.FileFilter;
026    import java.io.FileInputStream;
027    import java.io.FileOutputStream;
028    import java.io.IOException;
029    import java.io.InputStream;
030    import java.io.OutputStream;
031    import java.io.OutputStreamWriter;
032    import java.io.UnsupportedEncodingException;
033    import java.net.URL;
034    import java.nio.channels.FileChannel;
035    import java.nio.charset.Charset;
036    import java.util.ArrayList;
037    import java.util.Collection;
038    import java.util.Collections;
039    import java.util.Enumeration;
040    import java.util.HashSet;
041    import java.util.IllegalFormatException;
042    import java.util.List;
043    import java.util.Properties;
044    import java.util.Set;
045    import java.util.jar.JarEntry;
046    import java.util.jar.JarFile;
047    import java.util.jar.JarOutputStream;
048    import java.util.zip.GZIPInputStream;
049    import java.util.zip.ZipEntry;
050    
051    import edu.tum.cs.commons.assertion.CCSMAssert;
052    import edu.tum.cs.commons.assertion.CCSMPre;
053    import edu.tum.cs.commons.assertion.PreconditionException;
054    import edu.tum.cs.commons.collections.CollectionUtils;
055    import edu.tum.cs.commons.logging.ILogger;
056    import edu.tum.cs.commons.string.StringUtils;
057    
058    /**
059     * File system utilities.
060     * 
061     * @author Florian Deissenboeck
062     * @author Benjamin Hummel
063     * @author $Author: deissenb $
064     * @version $Rev: 29722 $
065     * @levd.rating GREEN Hash: C0A48F2C854364D0F04BB664D5A318A6
066     */
067    public class FileSystemUtils {
068    
069            /** Encoding for UTF-8. */
070            public static final String UTF8_ENCODING = "UTF-8";
071    
072            /** Charset for UTF-8. */
073            public static final Charset UTF8_CHARSET = Charset.forName(UTF8_ENCODING);
074    
075            /**
076             * Copy an input stream to an output stream. This does <em>not</em> close
077             * the streams.
078             * 
079             * @param input
080             *            input stream
081             * @param output
082             *            output stream
083             * @return number of bytes copied
084             * @throws IOException
085             *             if an IO exception occurs.
086             */
087            public static int copy(InputStream input, OutputStream output)
088                            throws IOException {
089                    byte[] buffer = new byte[1024];
090                    int size = 0;
091                    int len;
092                    while ((len = input.read(buffer)) > 0) {
093                            output.write(buffer, 0, len);
094                            size += len;
095                    }
096                    return size;
097            }
098    
099            /**
100             * Copy a file. This creates all necessary directories.
101             */
102            public static void copyFile(File sourceFile, File targetFile)
103                            throws IOException {
104    
105                    ensureParentDirectoryExists(targetFile);
106    
107                    FileChannel sourceChannel = new FileInputStream(sourceFile)
108                                    .getChannel();
109                    FileChannel targetChannel = new FileOutputStream(targetFile)
110                                    .getChannel();
111                    sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
112                    sourceChannel.close();
113                    targetChannel.close();
114            }
115    
116            /**
117             * Copy a file. This creates all necessary directories.
118             */
119            public static void copyFile(String sourceFilename, String targetFilename)
120                            throws IOException {
121                    copyFile(new File(sourceFilename), new File(targetFilename));
122            }
123    
124            /**
125             * Copy all files specified by a file filter from one directory to another.
126             * This automatically creates all necessary directories.
127             * 
128             * @param sourceDirectory
129             *            source directory
130             * @param targetDirectory
131             *            target directory
132             * @param fileFilter
133             *            filter to specify file types. If all files should be copied,
134             *            use {@link FileOnlyFilter}.
135             * @throws IOException
136             *             if an exception occurs.
137             * @return number of files copied
138             */
139            public static int copyFiles(File sourceDirectory, File targetDirectory,
140                            FileFilter fileFilter) throws IOException {
141                    List<File> files = FileSystemUtils.listFilesRecursively(
142                                    sourceDirectory, fileFilter);
143    
144                    int fileCount = 0;
145                    for (File sourceFile : files) {
146                            if (sourceFile.isFile()) {
147                                    String path = sourceFile.getAbsolutePath();
148                                    int index = sourceDirectory.getAbsolutePath().length();
149                                    String newPath = path.substring(index);
150                                    File targetFile = new File(targetDirectory, newPath);
151                                    copyFile(sourceFile, targetFile);
152                                    fileCount++;
153                            }
154                    }
155                    return fileCount;
156            }
157    
158            /**
159             * Create jar file from all files in a directory.
160             * 
161             * @param jarFile
162             *            jar file to create.
163             * @param directory
164             *            source directory.
165             * @param filter
166             *            filter to specify file types. If all files should be copied,
167             *            use {@link FileOnlyFilter}.
168             * @return number of files added to the jar file
169             * @throws IOException
170             *             if an exception occurs.
171             */
172            public static int createJARFile(File jarFile, File directory,
173                            FileFilter filter) throws IOException {
174                    JarOutputStream out = new JarOutputStream(new FileOutputStream(jarFile));
175                    List<File> files = FileSystemUtils.listFilesRecursively(directory,
176                                    filter);
177                    int fileCount = 0;
178                    for (File file : files) {
179                            if (file.isFile()) {
180                                    FileInputStream in = new FileInputStream(file);
181    
182                                    String entryName = file.getAbsolutePath().substring(
183                                                    directory.getAbsolutePath().length() + 1);
184    
185                                    // works for forward slashes only
186                                    entryName = entryName.replace(File.separatorChar, '/');
187    
188                                    out.putNextEntry(new ZipEntry(entryName));
189    
190                                    copy(in, out);
191    
192                                    out.closeEntry();
193                                    in.close();
194                                    fileCount++;
195                            }
196                    }
197                    out.close();
198                    return fileCount;
199            }
200    
201            /**
202             * Returns a string describing the relative path to the given directory. If
203             * there is no relative path, as the directories do not share a common
204             * parent, the absolute path is returned.
205             * 
206             * @param path
207             *            the path to convert to a relative path (must describe an
208             *            existing directory)
209             * @param relativeTo
210             *            the anchor (must describe an existing directory)
211             * @return a relative path
212             * @throws IOException
213             *             if creation of canonical pathes fails.
214             */
215            public static String createRelativePath(File path, File relativeTo)
216                            throws IOException {
217                    if (!path.isDirectory() || !relativeTo.isDirectory()) {
218                            throw new IllegalArgumentException(
219                                            "Both arguments must be existing directories!");
220                    }
221                    path = path.getCanonicalFile();
222                    relativeTo = relativeTo.getCanonicalFile();
223    
224                    Set<File> parents = new HashSet<File>();
225                    File f = path;
226                    while (f != null) {
227                            parents.add(f);
228                            f = f.getParentFile();
229                    }
230    
231                    File root = relativeTo;
232                    while (root != null && !parents.contains(root)) {
233                            root = root.getParentFile();
234                    }
235    
236                    if (root == null) {
237                            // no common root, so use full path
238                            return path.getAbsolutePath();
239                    }
240    
241                    String result = "";
242                    while (!path.equals(root)) {
243                            result = path.getName() + "/" + result;
244                            path = path.getParentFile();
245                    }
246                    while (!relativeTo.equals(root)) {
247                            result = "../" + result;
248                            relativeTo = relativeTo.getParentFile();
249                    }
250    
251                    return result;
252            }
253    
254            /**
255             * Recursively delete directories and files. This method ignores the return
256             * value of delete(), i.e. if anything fails, some files might still exist.
257             */
258            public static void deleteRecursively(File directory) {
259    
260                    if (directory == null) {
261                            throw new IllegalArgumentException("Directory may not be null.");
262                    } else if (directory.listFiles() == null) {
263                            throw new IllegalArgumentException(
264                                            "Argument is not a valid directory.");
265                    }
266    
267                    for (File entry : directory.listFiles()) {
268                            if (entry.isDirectory()) {
269                                    deleteRecursively(entry);
270                            }
271                            entry.delete();
272                    }
273                    directory.delete();
274            }
275    
276            /**
277             * Deletes the given file and throws an exception if this fails.
278             * 
279             * @see File#delete()
280             */
281            public static void deleteFile(File file) throws IOException {
282                    if (file.exists() && !file.delete()) {
283                            throw new IOException("Could not delete " + file);
284                    }
285            }
286    
287            /**
288             * Renames the given file and throws an exception if this fails.
289             * 
290             * @see File#renameTo(File)
291             */
292            public static void renameFileTo(File file, File dest) throws IOException {
293                    if (!file.renameTo(dest)) {
294                            throw new IOException("Could not rename " + file + " to " + dest);
295                    }
296            }
297    
298            /**
299             * Creates a directory and throws an exception if this fails.
300             * 
301             * @see File#mkdir()
302             */
303            public static void mkdir(File dir) throws IOException {
304                    if (!dir.mkdir()) {
305                            throw new IOException("Could not create directory " + dir);
306                    }
307            }
308    
309            /**
310             * Creates a directory and all required parent directories. Throws an
311             * exception if this fails.
312             * 
313             * @see File#mkdirs()
314             */
315            public static void mkdirs(File dir) throws IOException {
316                    if (!dir.mkdirs()) {
317                            throw new IOException("Could not create directory " + dir);
318                    }
319            }
320    
321            /**
322             * Checks if a directory exists. If not it creates the directory and all
323             * necessary parent directories.
324             * 
325             * @param file
326             *            the directory
327             * @throws IOException
328             *             if directories couldn't be created.
329             */
330            public static void ensureDirectoryExists(File file) throws IOException {
331                    if (!file.exists()) {
332                            if (!file.mkdirs()) {
333                                    throw new IOException("Couldn't create directory: " + file);
334                            }
335                    }
336            }
337    
338            /**
339             * Checks if the parent directory of a file exists. If not it creates the
340             * directory and all necessary parent directories.
341             * 
342             * @param file
343             *            the file
344             * @throws IOException
345             *             if directories couldn't be created.
346             */
347            public static void ensureParentDirectoryExists(File file)
348                            throws IOException {
349                    ensureDirectoryExists(file.getCanonicalFile().getParentFile());
350            }
351    
352            /**
353             * Returns a list of all files and directories contained in the given
354             * directory and all subdirectories. The given directory itself is not
355             * included in the result.
356             * <p>
357             * This method knows nothing about (symbolic and hard) links, so care should
358             * be taken when traversing directories containing recursive links.
359             * 
360             * @param directory
361             *            the directory to start the search from.
362             * @return the list of files found (the order is determined by the file
363             *         system).
364             */
365            public static List<File> listFilesRecursively(File directory) {
366                    return listFilesRecursively(directory, null);
367            }
368    
369            /**
370             * Returns a list of all files and directories contained in the given
371             * directory and all subdirectories matching the filter provided. The given
372             * directory itself is not included in the result.
373             * <p>
374             * The file filter may or may not exclude directories.
375             * <p>
376             * This method knows nothing about (symbolic and hard) links, so care should
377             * be taken when traversing directories containing recursive links.
378             * 
379             * @param directory
380             *            the directory to start the search from. If this is null or the
381             *            directory does not exists, an empty list is returned.
382             * @param filter
383             *            the filter used to determine whether the result should be
384             *            included. If the filter is null, all files and directories are
385             *            included.
386             * @return the list of files found (the order is determined by the file
387             *         system).
388             */
389            public static List<File> listFilesRecursively(File directory,
390                            FileFilter filter) {
391                    if (directory == null || !directory.isDirectory()) {
392                            return CollectionUtils.emptyList();
393                    }
394                    List<File> result = new ArrayList<File>();
395                    listFilesRecursively(directory, result, filter);
396                    return result;
397            }
398    
399            /**
400             * Returns the extension of the file.
401             * 
402             * @return File extension, i.e. "java" for "FileSystemUtils.java", or
403             *         <code>null</code>, if the file has no extension (i.e. if a
404             *         filename contains no '.'), returns the empty string if the '.' is
405             *         the filename's last character.
406             */
407            public static String getFileExtension(File file) {
408                    String name = file.getName();
409                    int posLastDot = name.lastIndexOf('.');
410                    if (posLastDot < 0) {
411                            return null;
412                    }
413                    return name.substring(posLastDot + 1);
414            }
415    
416            /**
417             * This method is similar to the constructor {@link File#File(File, String)}
418             * but allows to define multiple child levels.
419             * 
420             * @param parent
421             *            parent file
422             * @param elements
423             *            list of elements. If this is empty, the parent is returned.
424             * @return the new file.
425             */
426            public static File newFile(File parent, String... elements) {
427                    if (elements.length == 0) {
428                            return parent;
429                    }
430    
431                    File child = new File(parent, elements[0]);
432    
433                    String[] remainingElements = new String[elements.length - 1];
434    
435                    System
436                                    .arraycopy(elements, 1, remainingElements, 0,
437                                                    elements.length - 1);
438    
439                    return newFile(child, remainingElements);
440            }
441    
442            /**
443             * Read file content into a string using the default encoding for the
444             * platform. If the file starts with a UTF byte order mark (BOM), the
445             * encoding is ignored and the correct encoding based on this BOM is used
446             * for reading the file.
447             * 
448             * @see EByteOrderMark
449             */
450            public static String readFile(File file) throws IOException {
451                    return readFile(file, Charset.defaultCharset().name());
452            }
453    
454            /**
455             * Read file content into a string using UTF-8 encoding. If the file starts
456             * with a UTF byte order mark (BOM), the encoding is ignored and the correct
457             * encoding based on this BOM is used for reading the file.
458             * 
459             * @see EByteOrderMark
460             */
461            public static String readFileUTF8(File file) throws IOException {
462                    return readFile(file, UTF8_ENCODING);
463            }
464    
465            /**
466             * Read file content into a string using the given encoding. If the file
467             * starts with a UTF byte order mark (BOM), the encoding is ignored and the
468             * correct encoding based on this BOM is used for reading the file.
469             * 
470             * @see EByteOrderMark
471             */
472            public static String readFile(File file, String encoding)
473                            throws IOException, UnsupportedEncodingException {
474                    FileInputStream in = new FileInputStream(file);
475                    byte[] buffer = new byte[(int) file.length()];
476                    in.read(buffer);
477                    in.close();
478    
479                    EByteOrderMark bom = EByteOrderMark.determineBOM(buffer);
480                    if (bom != null) {
481                            return new String(buffer, bom.getBOMLength(), buffer.length
482                                            - bom.getBOMLength(), bom.getEncoding());
483                    }
484    
485                    return new String(buffer, encoding);
486            }
487    
488            /**
489             * Extract a JAR file to a directory.
490             * 
491             * @param jarFile
492             *            jar file to extract
493             * @param targetDirectory
494             *            target directory.
495             * @throws IOException
496             *             if an exception occurs.
497             */
498            public static void unjar(File jarFile, File targetDirectory)
499                            throws IOException {
500                    JarFile jar = new JarFile(jarFile);
501                    Enumeration<JarEntry> entries = jar.entries();
502    
503                    while (entries.hasMoreElements()) {
504                            JarEntry entry = entries.nextElement();
505                            if (!entry.isDirectory()) {
506                                    InputStream entryStream = jar.getInputStream(entry);
507                                    File file = new File(targetDirectory, entry.getName());
508                                    FileSystemUtils.ensureParentDirectoryExists(file);
509                                    FileOutputStream outputStream = new FileOutputStream(file);
510                                    copy(entryStream, outputStream);
511                                    entryStream.close();
512                                    outputStream.close();
513                            }
514                    }
515    
516                    jar.close();
517            }
518    
519            /**
520             * Write string to a file with the default encoding. This ensures all
521             * directories exist.
522             */
523            public static void writeFile(File file, String content) throws IOException {
524                    writeFile(file, content, Charset.defaultCharset().name());
525            }
526    
527            /**
528             * Write string to a file with UTF8 encoding. This ensures all directories
529             * exist.
530             */
531            public static void writeFileUTF8(File file, String content)
532                            throws IOException {
533                    writeFile(file, content, UTF8_ENCODING);
534            }
535    
536            /** Write string to a file. This ensures all directories exist. */
537            public static void writeFile(File file, String content, String encoding)
538                            throws IOException {
539                    ensureParentDirectoryExists(file);
540                    OutputStreamWriter writer = null;
541                    try {
542                            writer = new OutputStreamWriter(new FileOutputStream(file),
543                                            encoding);
544                            writer.write(content);
545                    } finally {
546                            FileSystemUtils.close(writer);
547                    }
548            }
549    
550            /**
551             * Write string to a file using a UTF encoding. The file will be prefixed
552             * with a byte-order mark. This ensures all directories exist.
553             */
554            public static void writeFileWithBOM(File file, String content,
555                            EByteOrderMark bom) throws IOException {
556                    ensureParentDirectoryExists(file);
557                    FileOutputStream out = null;
558                    try {
559                            out = new FileOutputStream(file);
560                            out.write(bom.getBOM());
561    
562                            OutputStreamWriter writer = new OutputStreamWriter(out, bom
563                                            .getEncoding());
564                            writer.write(content);
565                            writer.flush();
566                    } finally {
567                            FileSystemUtils.close(out);
568                    }
569            }
570    
571            /**
572             * Finds all files and directories contained in the given directory and all
573             * subdirectories matching the filter provided and put them into the result
574             * collection. The given directory itself is not included in the result.
575             * <p>
576             * This method knows nothing about (symbolic and hard) links, so care should
577             * be taken when traversing directories containing recursive links.
578             * 
579             * @param directory
580             *            the directory to start the search from.
581             * @param result
582             *            the collection to add to all files found.
583             * @param filter
584             *            the filter used to determine whether the result should be
585             *            included. If the filter is null, all files and directories are
586             *            included.
587             */
588            private static void listFilesRecursively(File directory,
589                            Collection<File> result, FileFilter filter) {
590                    for (File f : directory.listFiles()) {
591                            if (f.isDirectory()) {
592                                    listFilesRecursively(f, result, filter);
593                            }
594                            if (filter == null || filter.accept(f)) {
595                                    result.add(f);
596                            }
597                    }
598            }
599    
600            /**
601             * Loads template file with a <a href=
602             * "http://java.sun.com/javase/6/docs/api/java/util/Formatter.html#syntax"
603             * >Format string</a>, formats it and writes result to file.
604             * 
605             * @param templateFile
606             *            the template file with the format string
607             * @param outFile
608             *            the target file, parent directories are created automatically.
609             * @param arguments
610             *            the formatting arguments.
611             * @throws IOException
612             *             if an IO exception occurs or the template file defines an
613             *             illegal format.
614             */
615            public static void mergeTemplate(File templateFile, File outFile,
616                            Object... arguments) throws IOException {
617                    String template = readFile(templateFile);
618                    String output;
619                    try {
620                            output = String.format(template, arguments);
621                    } catch (IllegalFormatException e) {
622                            // We do not pass the cause to the constructor as the required
623                            // constructor is only defined in 1.6
624                            throw new IOException("Illegal format: " + e.getMessage());
625                    }
626                    writeFile(outFile, output);
627            }
628    
629            /**
630             * Loads template file with a <a href=
631             * "http://java.sun.com/javase/6/docs/api/java/util/Formatter.html#syntax"
632             * >Format string</a>, formats it and provides result as stream. No streams
633             * are closed by this method.
634             * 
635             * @param inStream
636             *            stream that provides the template format string
637             * @param arguments
638             *            the formatting arguments.
639             * @throws IOException
640             *             if an IOException occurs or the template file defines an
641             *             illegal format.
642             */
643            public static InputStream mergeTemplate(InputStream inStream,
644                            Object... arguments) throws IOException {
645                    String template = readStream(inStream);
646                    String output;
647                    try {
648                            output = String.format(template, arguments);
649                    } catch (IllegalFormatException e) {
650                            // We do not pass the cause to the constructor as the required
651                            // constructor is only defined in 1.6
652                            throw new IOException("Illegal format: " + e.getMessage());
653                    }
654                    return new ByteArrayInputStream(output.getBytes());
655            }
656    
657            /** Read input stream into string. */
658            public static String readStream(InputStream input) throws IOException {
659                    return readStream(input, Charset.defaultCharset().name());
660            }
661    
662            /** Read input stream into string. */
663            public static String readStreamUTF8(InputStream input) throws IOException {
664                    return readStream(input, UTF8_ENCODING);
665            }
666    
667            /** Read input stream into string. */
668            public static String readStream(InputStream input, String encoding)
669                            throws IOException {
670                    StringBuilder out = new StringBuilder();
671                    byte[] b = new byte[4096];
672                    for (int n; (n = input.read(b)) != -1;) {
673                            out.append(new String(b, 0, n, encoding));
674                    }
675                    return out.toString();
676            }
677    
678            /** Reads properties from a properties file. */
679            public static Properties readPropertiesFile(File propertiesFile)
680                            throws IOException {
681                    Properties properties = new Properties();
682                    InputStream inputStream = new FileInputStream(propertiesFile);
683                    try {
684                            properties.load(inputStream);
685                    } finally {
686                            inputStream.close();
687                    }
688                    return properties;
689            }
690    
691            /**
692             * Determines the root directory from a collection of files. The root
693             * directory is the lowest common ancestor directory of the files in the
694             * directory tree.
695             * <p>
696             * This method does not require the input files to exist.
697             * 
698             * @param files
699             *            Collection of files for which root directory gets determined.
700             *            This collection is required to contain at least 2 files. If it
701             *            does not, an AssertionError is thrown.
702             * 
703             * @throws PreconditionException
704             *             If less than two different files are provided whereas fully
705             *             qualified canonical names are used for comparison.
706             * 
707             * @throws IOException
708             *             Since canonical paths are used for determination of the
709             *             common root, and {@link File#getCanonicalPath()} can throw
710             *             {@link IOException}s.
711             * 
712             * @return Root directory, or null, if the files do not have a common root
713             *         directory.
714             */
715            public static File commonRoot(Iterable<? extends File> files)
716                            throws IOException {
717                    // determine longest common prefix on canonical absolute paths
718                    Set<String> absolutePaths = new HashSet<String>();
719                    for (File file : files) {
720                            absolutePaths.add(file.getCanonicalPath());
721                    }
722    
723                    CCSMPre.isTrue(absolutePaths.size() >= 2,
724                                    "Expected are at least 2 files");
725    
726                    String longestCommonPrefix = StringUtils
727                                    .longestCommonPrefix(absolutePaths);
728    
729                    // trim to name of root directory (remove possible equal filename
730                    // prefixes.)
731                    int lastSeparator = longestCommonPrefix.lastIndexOf(File.separator);
732                    if (lastSeparator > -1) {
733                            longestCommonPrefix = longestCommonPrefix.substring(0,
734                                            lastSeparator);
735                    }
736    
737                    if (StringUtils.isEmpty(longestCommonPrefix)) {
738                            return null;
739                    }
740    
741                    return new File(longestCommonPrefix);
742            }
743    
744            /** See {@link #canonize(File)} */
745            @Deprecated
746            public static String canonize(String path) {
747                    return canonize(new File(path)).getPath();
748            }
749    
750            /**
751             * Creates a canonical path from a file path. This method wraps
752             * {@link File#getCanonicalPath()} and wraps {@link IOException}s thrown by
753             * it into AssertionErrors, since we expect this method not to throw
754             * IOExceptions.
755             */
756            @Deprecated
757            public static File canonize(File file) {
758                    try {
759                            return file.getCanonicalFile();
760                    } catch (IOException e) {
761                            throw new AssertionError("Problems creating canonical path for "
762                                            + file);
763                    }
764            }
765    
766            /**
767             * Transparently creates a stream for decompression if the provided stream
768             * is compressed. Otherwise the stream is just handed through. Currently the
769             * following compression methods are supported:
770             * <ul>
771             * <li>GZIP via {@link GZIPInputStream}</li>
772             * </ul>
773             */
774            public static InputStream autoDecompressStream(InputStream in)
775                            throws IOException {
776                    if (!in.markSupported()) {
777                            in = new BufferedInputStream(in);
778                    }
779                    in.mark(2);
780                    // check first two bytes for GZIP header
781                    boolean isGZIP = (in.read() & 0xff | (in.read() & 0xff) << 8) == GZIPInputStream.GZIP_MAGIC;
782                    in.reset();
783                    if (isGZIP) {
784                            return new GZIPInputStream(in);
785                    }
786                    return in;
787            }
788    
789            /**
790             * Convenience method for calling {@link #close(Closeable, ILogger)} with a
791             * <code>null</code>-logger.
792             */
793            public static void close(Closeable closeable) {
794                    close(closeable, null);
795            }
796    
797            /**
798             * This method can be used to simplify the typical <code>finally</code>
799             * -block of code dealing with streams and readers/writers. It checks if the
800             * provided closeable is <code>null</code>. If not it closes it. An
801             * exception thrown during the close operation is logged with the provided
802             * logger with level <i>warn</i>. If the provided logger is
803             * <code>null</code>, no logging is performed. If no logging is required,
804             * method {@link #close(Closeable)} may also be used.
805             */
806            public static void close(Closeable closeable, ILogger logger) {
807                    if (closeable == null) {
808                            return;
809                    }
810    
811                    try {
812                            closeable.close();
813                    } catch (IOException e) {
814                            if (logger != null) {
815                                    logger.warn("Trouble closing: " + e.getMessage());
816                            }
817                    }
818            }
819    
820            /**
821             * Compares files based on the lexical order of their fully qualified names.
822             * Files must not null.
823             */
824            public static void sort(List<File> files) {
825                    Collections.sort(files, new FilenameComparator());
826            }
827    
828            /**
829             * Replace platform dependent separator char with forward slashes to create
830             * system-independent paths.
831             */
832            public static String normalizeSeparators(String path) {
833                    return path.replace(File.separatorChar, '/');
834            }
835    
836            /**
837             * Returns the JAR file for an URL with protocol 'jar'. If the protocol is
838             * not 'jar' an assertion error will be caused!
839             */
840            public static File extractJarFileFromJarURL(URL url) {
841                    CCSMPre.isTrue("jar".equals(url.getProtocol()),
842                                    "May only be used with 'jar' URLs!");
843    
844                    String path = url.getPath();
845                    path = StringUtils.stripPrefix("file:", path);
846    
847                    // the exclamation mark is the separator between jar file and path
848                    // within the file
849                    int index = path.indexOf('!');
850                    CCSMAssert.isTrue(index >= 0, "Unknown format for jar URLs");
851                    path = path.substring(0, index);
852                    return new File(path);
853            }
854    
855            /**
856             * Returns whether a filename represents an absolute path.
857             * 
858             * This method returns the same result, independent on which operating
859             * system it gets executed. In contrast, the behavior of
860             * {@link File#isAbsolute()} is operating system specific.
861             */
862            public static boolean isAbsolutePath(String filename) {
863                    // Unix and MacOS: absolute path starts with slash
864                    if (filename.startsWith("/") || filename.startsWith("~/")) {
865                            return true;
866                    }
867                    // Windows and OS/2: absolute path start with letter and colon
868                    if (filename.length() > 2 && Character.isLetter(filename.charAt(0))
869                                    && filename.charAt(1) == ':') {
870                            return true;
871                    }
872                    // UNC paths (aka network shares): start with double backslash
873                    if (filename.startsWith("\\\\")) {
874                            return true;
875                    }
876    
877                    return false;
878            }
879    
880            /**
881             * Reads bytes of data from the input stream into an array of bytes until
882             * the array is full. This method blocks until input data is available, end
883             * of file is detected, or an exception is thrown.
884             * 
885             * The reason for this method is that {@link InputStream#read(byte[])} may
886             * read less than the requested number of bytes, while this method ensures
887             * the data is complete.
888             * 
889             * @param in
890             *            the stream to read from.
891             * @param data
892             *            the stream to read from.
893             * @throws IOException
894             *             if reading the underlying stream causes an exception.
895             * @throws EOFException
896             *             if the end of file was reached before the requested data was
897             *             read.
898             */
899            public static void safeRead(InputStream in, byte[] data)
900                            throws IOException, EOFException {
901                    safeRead(in, data, 0, data.length);
902            }
903    
904            /**
905             * Reads <code>length</code> bytes of data from the input stream into an
906             * array of bytes and stores it at position <code>offset</code>. This method
907             * blocks until input data is available, end of file is detected, or an
908             * exception is thrown.
909             * 
910             * The reason for this method is that
911             * {@link InputStream#read(byte[], int, int)} may read less than the
912             * requested number of bytes, while this method ensures the data is
913             * complete.
914             * 
915             * @param in
916             *            the stream to read from.
917             * @param data
918             *            the stream to read from.
919             * @param offset
920             *            the offset in the array where the first read byte is stored.
921             * @param length
922             *            the length of data read.
923             * @throws IOException
924             *             if reading the underlying stream causes an exception.
925             * @throws EOFException
926             *             if the end of file was reached before the requested data was
927             *             read.
928             */
929            public static void safeRead(InputStream in, byte[] data, int offset,
930                            int length) throws IOException, EOFException {
931                    while (length > 0) {
932                            int read = in.read(data, offset, length);
933                            if (read < 0) {
934                                    throw new EOFException(
935                                                    "Reached end of file before completing read.");
936                            }
937                            offset += read;
938                            length -= read;
939                    }
940            }
941    }