From fcac4cd906c1d16e600db84217f8c4310ee2ce44 Mon Sep 17 00:00:00 2001 From: Gianpaolo Coro Date: Thu, 18 Feb 2016 20:11:40 +0000 Subject: [PATCH] git-svn-id: https://svn.d4science.research-infrastructures.eu/gcube/trunk/data-analysis/EcologicalEngineSmartExecutor@124322 82a268e6-3cf1-43bd-a215-b396298e98cf --- .../executor/rscripts/generic/FileUtils.java | 2945 +++++++++++++++++ .../rscripts/generic/FilenameUtils.java | 1367 ++++++++ .../rscripts/generic/GenericRScript.java | 2 +- .../executor/rscripts/generic/IOUtils.java | 2547 ++++++++++++++ 4 files changed, 6860 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/FileUtils.java create mode 100644 src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/FilenameUtils.java create mode 100644 src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/IOUtils.java diff --git a/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/FileUtils.java b/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/FileUtils.java new file mode 100644 index 0000000..16cb809 --- /dev/null +++ b/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/FileUtils.java @@ -0,0 +1,2945 @@ +package org.gcube.dataanalysis.executor.rscripts.generic; +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.net.URL; +import java.net.URLConnection; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.zip.CRC32; +import java.util.zip.CheckedInputStream; +import java.util.zip.Checksum; + +import org.apache.commons.codec.Charsets; +import org.apache.commons.io.FileExistsException; +import org.apache.commons.io.LineIterator; +import org.apache.commons.io.filefilter.DirectoryFileFilter; +import org.apache.commons.io.filefilter.FalseFileFilter; +import org.apache.commons.io.filefilter.FileFilterUtils; +import org.apache.commons.io.filefilter.IOFileFilter; +import org.apache.commons.io.filefilter.SuffixFileFilter; +import org.apache.commons.io.filefilter.TrueFileFilter; +import org.apache.commons.io.output.NullOutputStream; + +/** + * General file manipulation utilities. + *

+ * Facilities are provided in the following areas: + *

+ *

+ * Origin of code: Excalibur, Alexandria, Commons-Utils + * + * @version $Id: FileUtils.java 1349509 2012-06-12 20:39:23Z ggregory $ + */ +public class FileUtils { + + /** + * Instances should NOT be constructed in standard programming. + */ + public FileUtils() { + super(); + } + + /** + * The number of bytes in a kilobyte. + */ + public static final long ONE_KB = 1024; + + /** + * The number of bytes in a kilobyte. + * + * @since 2.4 + */ + public static final BigInteger ONE_KB_BI = BigInteger.valueOf(ONE_KB); + + /** + * The number of bytes in a megabyte. + */ + public static final long ONE_MB = ONE_KB * ONE_KB; + + /** + * The number of bytes in a megabyte. + * + * @since 2.4 + */ + public static final BigInteger ONE_MB_BI = ONE_KB_BI.multiply(ONE_KB_BI); + + /** + * The file copy buffer size (30 MB) + */ + private static final long FILE_COPY_BUFFER_SIZE = ONE_MB * 30; + + /** + * The number of bytes in a gigabyte. + */ + public static final long ONE_GB = ONE_KB * ONE_MB; + + /** + * The number of bytes in a gigabyte. + * + * @since 2.4 + */ + public static final BigInteger ONE_GB_BI = ONE_KB_BI.multiply(ONE_MB_BI); + + /** + * The number of bytes in a terabyte. + */ + public static final long ONE_TB = ONE_KB * ONE_GB; + + /** + * The number of bytes in a terabyte. + * + * @since 2.4 + */ + public static final BigInteger ONE_TB_BI = ONE_KB_BI.multiply(ONE_GB_BI); + + /** + * The number of bytes in a petabyte. + */ + public static final long ONE_PB = ONE_KB * ONE_TB; + + /** + * The number of bytes in a petabyte. + * + * @since 2.4 + */ + public static final BigInteger ONE_PB_BI = ONE_KB_BI.multiply(ONE_TB_BI); + + /** + * The number of bytes in an exabyte. + */ + public static final long ONE_EB = ONE_KB * ONE_PB; + + /** + * The number of bytes in an exabyte. + * + * @since 2.4 + */ + public static final BigInteger ONE_EB_BI = ONE_KB_BI.multiply(ONE_PB_BI); + + /** + * The number of bytes in a zettabyte. + */ + public static final BigInteger ONE_ZB = BigInteger.valueOf(ONE_KB).multiply(BigInteger.valueOf(ONE_EB)); + + /** + * The number of bytes in a yottabyte. + */ + public static final BigInteger ONE_YB = ONE_KB_BI.multiply(ONE_ZB); + + /** + * An empty array of type File. + */ + public static final File[] EMPTY_FILE_ARRAY = new File[0]; + + /** + * The UTF-8 character set, used to decode octets in URLs. + */ + private static final Charset UTF8 = Charset.forName("UTF-8"); + + //----------------------------------------------------------------------- + /** + * Construct a file from the set of name elements. + * + * @param directory the parent directory + * @param names the name elements + * @return the file + * @since 2.1 + */ + public static File getFile(File directory, String... names) { + if (directory == null) { + throw new NullPointerException("directorydirectory must not be null"); + } + if (names == null) { + throw new NullPointerException("names must not be null"); + } + File file = directory; + for (String name : names) { + file = new File(file, name); + } + return file; + } + + /** + * Construct a file from the set of name elements. + * + * @param names the name elements + * @return the file + * @since 2.1 + */ + public static File getFile(String... names) { + if (names == null) { + throw new NullPointerException("names must not be null"); + } + File file = null; + for (String name : names) { + if (file == null) { + file = new File(name); + } else { + file = new File(file, name); + } + } + return file; + } + + /** + * Returns the path to the system temporary directory. + * + * @return the path to the system temporary directory. + * + * @since 2.0 + */ + public static String getTempDirectoryPath() { + return System.getProperty("java.io.tmpdir"); + } + + /** + * Returns a {@link File} representing the system temporary directory. + * + * @return the system temporary directory. + * + * @since 2.0 + */ + public static File getTempDirectory() { + return new File(getTempDirectoryPath()); + } + + /** + * Returns the path to the user's home directory. + * + * @return the path to the user's home directory. + * + * @since 2.0 + */ + public static String getUserDirectoryPath() { + return System.getProperty("user.home"); + } + + /** + * Returns a {@link File} representing the user's home directory. + * + * @return the user's home directory. + * + * @since 2.0 + */ + public static File getUserDirectory() { + return new File(getUserDirectoryPath()); + } + + //----------------------------------------------------------------------- + /** + * Opens a {@link FileInputStream} for the specified file, providing better + * error messages than simply calling new FileInputStream(file). + *

+ * At the end of the method either the stream will be successfully opened, + * or an exception will have been thrown. + *

+ * An exception is thrown if the file does not exist. + * An exception is thrown if the file object exists but is a directory. + * An exception is thrown if the file exists but cannot be read. + * + * @param file the file to open for input, must not be {@code null} + * @return a new {@link FileInputStream} for the specified file + * @throws FileNotFoundException if the file does not exist + * @throws IOException if the file object is a directory + * @throws IOException if the file cannot be read + * @since 1.3 + */ + public static FileInputStream openInputStream(File file) throws IOException { + if (file.exists()) { + if (file.isDirectory()) { + throw new IOException("File '" + file + "' exists but is a directory"); + } + if (file.canRead() == false) { + throw new IOException("File '" + file + "' cannot be read"); + } + } else { + throw new FileNotFoundException("File '" + file + "' does not exist"); + } + return new FileInputStream(file); + } + + //----------------------------------------------------------------------- + /** + * Opens a {@link FileOutputStream} for the specified file, checking and + * creating the parent directory if it does not exist. + *

+ * At the end of the method either the stream will be successfully opened, + * or an exception will have been thrown. + *

+ * The parent directory will be created if it does not exist. + * The file will be created if it does not exist. + * An exception is thrown if the file object exists but is a directory. + * An exception is thrown if the file exists but cannot be written to. + * An exception is thrown if the parent directory cannot be created. + * + * @param file the file to open for output, must not be {@code null} + * @return a new {@link FileOutputStream} for the specified file + * @throws IOException if the file object is a directory + * @throws IOException if the file cannot be written to + * @throws IOException if a parent directory needs creating but that fails + * @since 1.3 + */ + public static FileOutputStream openOutputStream(File file) throws IOException { + return openOutputStream(file, false); + } + + /** + * Opens a {@link FileOutputStream} for the specified file, checking and + * creating the parent directory if it does not exist. + *

+ * At the end of the method either the stream will be successfully opened, + * or an exception will have been thrown. + *

+ * The parent directory will be created if it does not exist. + * The file will be created if it does not exist. + * An exception is thrown if the file object exists but is a directory. + * An exception is thrown if the file exists but cannot be written to. + * An exception is thrown if the parent directory cannot be created. + * + * @param file the file to open for output, must not be {@code null} + * @param append if {@code true}, then bytes will be added to the + * end of the file rather than overwriting + * @return a new {@link FileOutputStream} for the specified file + * @throws IOException if the file object is a directory + * @throws IOException if the file cannot be written to + * @throws IOException if a parent directory needs creating but that fails + * @since 2.1 + */ + public static FileOutputStream openOutputStream(File file, boolean append) throws IOException { + if (file.exists()) { + if (file.isDirectory()) { + throw new IOException("File '" + file + "' exists but is a directory"); + } + if (file.canWrite() == false) { + throw new IOException("File '" + file + "' cannot be written to"); + } + } else { + File parent = file.getParentFile(); + if (parent != null) { + if (!parent.mkdirs() && !parent.isDirectory()) { + throw new IOException("Directory '" + parent + "' could not be created"); + } + } + } + return new FileOutputStream(file, append); + } + + //----------------------------------------------------------------------- + /** + * Returns a human-readable version of the file size, where the input represents a specific number of bytes. + *

+ * If the size is over 1GB, the size is returned as the number of whole GB, i.e. the size is rounded down to the + * nearest GB boundary. + *

+ *

+ * Similarly for the 1MB and 1KB boundaries. + *

+ * + * @param size + * the number of bytes + * @return a human-readable display value (includes units - EB, PB, TB, GB, MB, KB or bytes) + * @see IO-226 - should the rounding be changed? + * @since 2.4 + */ + // See https://issues.apache.org/jira/browse/IO-226 - should the rounding be changed? + public static String byteCountToDisplaySize(BigInteger size) { + String displaySize; + + if (size.divide(ONE_EB_BI).compareTo(BigInteger.ZERO) > 0) { + displaySize = String.valueOf(size.divide(ONE_EB_BI)) + " EB"; + } else if (size.divide(ONE_PB_BI).compareTo(BigInteger.ZERO) > 0) { + displaySize = String.valueOf(size.divide(ONE_PB_BI)) + " PB"; + } else if (size.divide(ONE_TB_BI).compareTo(BigInteger.ZERO) > 0) { + displaySize = String.valueOf(size.divide(ONE_TB_BI)) + " TB"; + } else if (size.divide(ONE_GB_BI).compareTo(BigInteger.ZERO) > 0) { + displaySize = String.valueOf(size.divide(ONE_GB_BI)) + " GB"; + } else if (size.divide(ONE_MB_BI).compareTo(BigInteger.ZERO) > 0) { + displaySize = String.valueOf(size.divide(ONE_MB_BI)) + " MB"; + } else if (size.divide(ONE_KB_BI).compareTo(BigInteger.ZERO) > 0) { + displaySize = String.valueOf(size.divide(ONE_KB_BI)) + " KB"; + } else { + displaySize = String.valueOf(size) + " bytes"; + } + return displaySize; + } + + /** + * Returns a human-readable version of the file size, where the input represents a specific number of bytes. + *

+ * If the size is over 1GB, the size is returned as the number of whole GB, i.e. the size is rounded down to the + * nearest GB boundary. + *

+ *

+ * Similarly for the 1MB and 1KB boundaries. + *

+ * + * @param size + * the number of bytes + * @return a human-readable display value (includes units - EB, PB, TB, GB, MB, KB or bytes) + * @see IO-226 - should the rounding be changed? + */ + // See https://issues.apache.org/jira/browse/IO-226 - should the rounding be changed? + public static String byteCountToDisplaySize(long size) { + return byteCountToDisplaySize(BigInteger.valueOf(size)); + } + + //----------------------------------------------------------------------- + /** + * Implements the same behaviour as the "touch" utility on Unix. It creates + * a new file with size 0 or, if the file exists already, it is opened and + * closed without modifying it, but updating the file date and time. + *

+ * NOTE: As from v1.3, this method throws an IOException if the last + * modified date of the file cannot be set. Also, as from v1.3 this method + * creates parent directories if they do not exist. + * + * @param file the File to touch + * @throws IOException If an I/O problem occurs + */ + public static void touch(File file) throws IOException { + if (!file.exists()) { + OutputStream out = openOutputStream(file); + IOUtils.closeQuietly(out); + } + boolean success = file.setLastModified(System.currentTimeMillis()); + if (!success) { + throw new IOException("Unable to set the last modification time for " + file); + } + } + + //----------------------------------------------------------------------- + /** + * Converts a Collection containing java.io.File instanced into array + * representation. This is to account for the difference between + * File.listFiles() and FileUtils.listFiles(). + * + * @param files a Collection containing java.io.File instances + * @return an array of java.io.File + */ + public static File[] convertFileCollectionToFileArray(Collection files) { + return files.toArray(new File[files.size()]); + } + + //----------------------------------------------------------------------- + /** + * Finds files within a given directory (and optionally its + * subdirectories). All files found are filtered by an IOFileFilter. + * + * @param files the collection of files found. + * @param directory the directory to search in. + * @param filter the filter to apply to files and directories. + * @param includeSubDirectories indicates if will include the subdirectories themselves + */ + private static void innerListFiles(Collection files, File directory, + IOFileFilter filter, boolean includeSubDirectories) { + File[] found = directory.listFiles((FileFilter) filter); + + if (found != null) { + for (File file : found) { + if (file.isDirectory()) { + if (includeSubDirectories) { + files.add(file); + } + innerListFiles(files, file, filter, includeSubDirectories); + } else { + files.add(file); + } + } + } + } + + /** + * Finds files within a given directory (and optionally its + * subdirectories). All files found are filtered by an IOFileFilter. + *

+ * If your search should recurse into subdirectories you can pass in + * an IOFileFilter for directories. You don't need to bind a + * DirectoryFileFilter (via logical AND) to this filter. This method does + * that for you. + *

+ * An example: If you want to search through all directories called + * "temp" you pass in FileFilterUtils.NameFileFilter("temp") + *

+ * Another common usage of this method is find files in a directory + * tree but ignoring the directories generated CVS. You can simply pass + * in FileFilterUtils.makeCVSAware(null). + * + * @param directory the directory to search in + * @param fileFilter filter to apply when finding files. + * @param dirFilter optional filter to apply when finding subdirectories. + * If this parameter is {@code null}, subdirectories will not be included in the + * search. Use TrueFileFilter.INSTANCE to match all directories. + * @return an collection of java.io.File with the matching files + * @see org.apache.commons.io.filefilter.FileFilterUtils + * @see org.apache.commons.io.filefilter.NameFileFilter + */ + public static Collection listFiles( + File directory, IOFileFilter fileFilter, IOFileFilter dirFilter) { + validateListFilesParameters(directory, fileFilter); + + IOFileFilter effFileFilter = setUpEffectiveFileFilter(fileFilter); + IOFileFilter effDirFilter = setUpEffectiveDirFilter(dirFilter); + + //Find files + Collection files = new java.util.LinkedList(); + innerListFiles(files, directory, + FileFilterUtils.or(effFileFilter, effDirFilter), false); + return files; + } + + /** + * Validates the given arguments. + *

+ * + * @param directory The File to test + * @param fileFilter The IOFileFilter to test + */ + private static void validateListFilesParameters(File directory, IOFileFilter fileFilter) { + if (!directory.isDirectory()) { + throw new IllegalArgumentException("Parameter 'directory' is not a directory"); + } + if (fileFilter == null) { + throw new NullPointerException("Parameter 'fileFilter' is null"); + } + } + + /** + * Returns a filter that accepts files in addition to the {@link File} objects accepted by the given filter. + * + * @param fileFilter a base filter to add to + * @return a filter that accepts files + */ + private static IOFileFilter setUpEffectiveFileFilter(IOFileFilter fileFilter) { + return FileFilterUtils.and(fileFilter, FileFilterUtils.notFileFilter(DirectoryFileFilter.INSTANCE)); + } + + /** + * Returns a filter that accepts directories in addition to the {@link File} objects accepted by the given filter. + * + * @param dirFilter a base filter to add to + * @return a filter that accepts directories + */ + private static IOFileFilter setUpEffectiveDirFilter(IOFileFilter dirFilter) { + return dirFilter == null ? FalseFileFilter.INSTANCE : FileFilterUtils.and(dirFilter, + DirectoryFileFilter.INSTANCE); + } + + /** + * Finds files within a given directory (and optionally its + * subdirectories). All files found are filtered by an IOFileFilter. + *

+ * The resulting collection includes the subdirectories themselves. + *

+ * @see org.apache.commons.io.FileUtils#listFiles + * + * @param directory the directory to search in + * @param fileFilter filter to apply when finding files. + * @param dirFilter optional filter to apply when finding subdirectories. + * If this parameter is {@code null}, subdirectories will not be included in the + * search. Use TrueFileFilter.INSTANCE to match all directories. + * @return an collection of java.io.File with the matching files + * @see org.apache.commons.io.filefilter.FileFilterUtils + * @see org.apache.commons.io.filefilter.NameFileFilter + * @since 2.2 + */ + public static Collection listFilesAndDirs( + File directory, IOFileFilter fileFilter, IOFileFilter dirFilter) { + validateListFilesParameters(directory, fileFilter); + + IOFileFilter effFileFilter = setUpEffectiveFileFilter(fileFilter); + IOFileFilter effDirFilter = setUpEffectiveDirFilter(dirFilter); + + //Find files + Collection files = new java.util.LinkedList(); + if (directory.isDirectory()) { + files.add(directory); + } + innerListFiles(files, directory, + FileFilterUtils.or(effFileFilter, effDirFilter), true); + return files; + } + + /** + * Allows iteration over the files in given directory (and optionally + * its subdirectories). + *

+ * All files found are filtered by an IOFileFilter. This method is + * based on {@link #listFiles(File, IOFileFilter, IOFileFilter)}, + * which supports Iterable ('foreach' loop). + *

+ * @param directory the directory to search in + * @param fileFilter filter to apply when finding files. + * @param dirFilter optional filter to apply when finding subdirectories. + * If this parameter is {@code null}, subdirectories will not be included in the + * search. Use TrueFileFilter.INSTANCE to match all directories. + * @return an iterator of java.io.File for the matching files + * @see org.apache.commons.io.filefilter.FileFilterUtils + * @see org.apache.commons.io.filefilter.NameFileFilter + * @since 1.2 + */ + public static Iterator iterateFiles( + File directory, IOFileFilter fileFilter, IOFileFilter dirFilter) { + return listFiles(directory, fileFilter, dirFilter).iterator(); + } + + /** + * Allows iteration over the files in given directory (and optionally + * its subdirectories). + *

+ * All files found are filtered by an IOFileFilter. This method is + * based on {@link #listFilesAndDirs(File, IOFileFilter, IOFileFilter)}, + * which supports Iterable ('foreach' loop). + *

+ * The resulting iterator includes the subdirectories themselves. + * + * @param directory the directory to search in + * @param fileFilter filter to apply when finding files. + * @param dirFilter optional filter to apply when finding subdirectories. + * If this parameter is {@code null}, subdirectories will not be included in the + * search. Use TrueFileFilter.INSTANCE to match all directories. + * @return an iterator of java.io.File for the matching files + * @see org.apache.commons.io.filefilter.FileFilterUtils + * @see org.apache.commons.io.filefilter.NameFileFilter + * @since 2.2 + */ + public static Iterator iterateFilesAndDirs(File directory, IOFileFilter fileFilter, IOFileFilter dirFilter) { + return listFilesAndDirs(directory, fileFilter, dirFilter).iterator(); + } + + //----------------------------------------------------------------------- + /** + * Converts an array of file extensions to suffixes for use + * with IOFileFilters. + * + * @param extensions an array of extensions. Format: {"java", "xml"} + * @return an array of suffixes. Format: {".java", ".xml"} + */ + private static String[] toSuffixes(String[] extensions) { + String[] suffixes = new String[extensions.length]; + for (int i = 0; i < extensions.length; i++) { + suffixes[i] = "." + extensions[i]; + } + return suffixes; + } + + + /** + * Finds files within a given directory (and optionally its subdirectories) + * which match an array of extensions. + * + * @param directory the directory to search in + * @param extensions an array of extensions, ex. {"java","xml"}. If this + * parameter is {@code null}, all files are returned. + * @param recursive if true all subdirectories are searched as well + * @return an collection of java.io.File with the matching files + */ + public static Collection listFiles( + File directory, String[] extensions, boolean recursive) { + IOFileFilter filter; + if (extensions == null) { + filter = TrueFileFilter.INSTANCE; + } else { + String[] suffixes = toSuffixes(extensions); + filter = new SuffixFileFilter(suffixes); + } + return listFiles(directory, filter, + recursive ? TrueFileFilter.INSTANCE : FalseFileFilter.INSTANCE); + } + + /** + * Allows iteration over the files in a given directory (and optionally + * its subdirectories) which match an array of extensions. This method + * is based on {@link #listFiles(File, String[], boolean)}, + * which supports Iterable ('foreach' loop). + * + * @param directory the directory to search in + * @param extensions an array of extensions, ex. {"java","xml"}. If this + * parameter is {@code null}, all files are returned. + * @param recursive if true all subdirectories are searched as well + * @return an iterator of java.io.File with the matching files + * @since 1.2 + */ + public static Iterator iterateFiles( + File directory, String[] extensions, boolean recursive) { + return listFiles(directory, extensions, recursive).iterator(); + } + + //----------------------------------------------------------------------- + /** + * Compares the contents of two files to determine if they are equal or not. + *

+ * This method checks to see if the two files are different lengths + * or if they point to the same file, before resorting to byte-by-byte + * comparison of the contents. + *

+ * Code origin: Avalon + * + * @param file1 the first file + * @param file2 the second file + * @return true if the content of the files are equal or they both don't + * exist, false otherwise + * @throws IOException in case of an I/O error + */ + public static boolean contentEquals(File file1, File file2) throws IOException { + boolean file1Exists = file1.exists(); + if (file1Exists != file2.exists()) { + return false; + } + + if (!file1Exists) { + // two not existing files are equal + return true; + } + + if (file1.isDirectory() || file2.isDirectory()) { + // don't want to compare directory contents + throw new IOException("Can't compare directories, only files"); + } + + if (file1.length() != file2.length()) { + // lengths differ, cannot be equal + return false; + } + + if (file1.getCanonicalFile().equals(file2.getCanonicalFile())) { + // same file + return true; + } + + InputStream input1 = null; + InputStream input2 = null; + try { + input1 = new FileInputStream(file1); + input2 = new FileInputStream(file2); + return IOUtils.contentEquals(input1, input2); + + } finally { + IOUtils.closeQuietly(input1); + IOUtils.closeQuietly(input2); + } + } + + //----------------------------------------------------------------------- + /** + * Compares the contents of two files to determine if they are equal or not. + *

+ * This method checks to see if the two files point to the same file, + * before resorting to line-by-line comparison of the contents. + *

+ * + * @param file1 the first file + * @param file2 the second file + * @param charsetName the character encoding to be used. + * May be null, in which case the platform default is used + * @return true if the content of the files are equal or neither exists, + * false otherwise + * @throws IOException in case of an I/O error + * @since 2.2 + * @see IOUtils#contentEqualsIgnoreEOL(Reader, Reader) + */ + public static boolean contentEqualsIgnoreEOL(File file1, File file2, String charsetName) throws IOException { + boolean file1Exists = file1.exists(); + if (file1Exists != file2.exists()) { + return false; + } + + if (!file1Exists) { + // two not existing files are equal + return true; + } + + if (file1.isDirectory() || file2.isDirectory()) { + // don't want to compare directory contents + throw new IOException("Can't compare directories, only files"); + } + + if (file1.getCanonicalFile().equals(file2.getCanonicalFile())) { + // same file + return true; + } + + Reader input1 = null; + Reader input2 = null; + try { + if (charsetName == null) { + input1 = new InputStreamReader(new FileInputStream(file1)); + input2 = new InputStreamReader(new FileInputStream(file2)); + } else { + input1 = new InputStreamReader(new FileInputStream(file1), charsetName); + input2 = new InputStreamReader(new FileInputStream(file2), charsetName); + } + return IOUtils.contentEqualsIgnoreEOL(input1, input2); + + } finally { + IOUtils.closeQuietly(input1); + IOUtils.closeQuietly(input2); + } + } + + //----------------------------------------------------------------------- + /** + * Convert from a URL to a File. + *

+ * From version 1.1 this method will decode the URL. + * Syntax such as file:///my%20docs/file.txt will be + * correctly decoded to /my docs/file.txt. Starting with version + * 1.5, this method uses UTF-8 to decode percent-encoded octets to characters. + * Additionally, malformed percent-encoded octets are handled leniently by + * passing them through literally. + * + * @param url the file URL to convert, {@code null} returns {@code null} + * @return the equivalent File object, or {@code null} + * if the URL's protocol is not file + */ + public static File toFile(URL url) { + if (url == null || !"file".equalsIgnoreCase(url.getProtocol())) { + return null; + } else { + String filename = url.getFile().replace('/', File.separatorChar); + filename = decodeUrl(filename); + return new File(filename); + } + } + + /** + * Decodes the specified URL as per RFC 3986, i.e. transforms + * percent-encoded octets to characters by decoding with the UTF-8 character + * set. This function is primarily intended for usage with + * {@link java.net.URL} which unfortunately does not enforce proper URLs. As + * such, this method will leniently accept invalid characters or malformed + * percent-encoded octets and simply pass them literally through to the + * result string. Except for rare edge cases, this will make unencoded URLs + * pass through unaltered. + * + * @param url The URL to decode, may be {@code null}. + * @return The decoded URL or {@code null} if the input was + * {@code null}. + */ + static String decodeUrl(String url) { + String decoded = url; + if (url != null && url.indexOf('%') >= 0) { + int n = url.length(); + StringBuffer buffer = new StringBuffer(); + ByteBuffer bytes = ByteBuffer.allocate(n); + for (int i = 0; i < n;) { + if (url.charAt(i) == '%') { + try { + do { + byte octet = (byte) Integer.parseInt(url.substring(i + 1, i + 3), 16); + bytes.put(octet); + i += 3; + } while (i < n && url.charAt(i) == '%'); + continue; + } catch (RuntimeException e) { + // malformed percent-encoded octet, fall through and + // append characters literally + } finally { + if (bytes.position() > 0) { + bytes.flip(); + buffer.append(UTF8.decode(bytes).toString()); + bytes.clear(); + } + } + } + buffer.append(url.charAt(i++)); + } + decoded = buffer.toString(); + } + return decoded; + } + + /** + * Converts each of an array of URL to a File. + *

+ * Returns an array of the same size as the input. + * If the input is {@code null}, an empty array is returned. + * If the input contains {@code null}, the output array contains {@code null} at the same + * index. + *

+ * This method will decode the URL. + * Syntax such as file:///my%20docs/file.txt will be + * correctly decoded to /my docs/file.txt. + * + * @param urls the file URLs to convert, {@code null} returns empty array + * @return a non-{@code null} array of Files matching the input, with a {@code null} item + * if there was a {@code null} at that index in the input array + * @throws IllegalArgumentException if any file is not a URL file + * @throws IllegalArgumentException if any file is incorrectly encoded + * @since 1.1 + */ + public static File[] toFiles(URL[] urls) { + if (urls == null || urls.length == 0) { + return EMPTY_FILE_ARRAY; + } + File[] files = new File[urls.length]; + for (int i = 0; i < urls.length; i++) { + URL url = urls[i]; + if (url != null) { + if (url.getProtocol().equals("file") == false) { + throw new IllegalArgumentException( + "URL could not be converted to a File: " + url); + } + files[i] = toFile(url); + } + } + return files; + } + + /** + * Converts each of an array of File to a URL. + *

+ * Returns an array of the same size as the input. + * + * @param files the files to convert, must not be {@code null} + * @return an array of URLs matching the input + * @throws IOException if a file cannot be converted + * @throws NullPointerException if the parameter is null + */ + public static URL[] toURLs(File[] files) throws IOException { + URL[] urls = new URL[files.length]; + + for (int i = 0; i < urls.length; i++) { + urls[i] = files[i].toURI().toURL(); + } + + return urls; + } + + //----------------------------------------------------------------------- + /** + * Copies a file to a directory preserving the file date. + *

+ * This method copies the contents of the specified source file + * to a file of the same name in the specified destination directory. + * The destination directory is created if it does not exist. + * If the destination file exists, then this method will overwrite it. + *

+ * Note: This method tries to preserve the file's last + * modified date/times using {@link File#setLastModified(long)}, however + * it is not guaranteed that the operation will succeed. + * If the modification operation fails, no indication is provided. + * + * @param srcFile an existing file to copy, must not be {@code null} + * @param destDir the directory to place the copy in, must not be {@code null} + * + * @throws NullPointerException if source or destination is null + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFile(File, File, boolean) + */ + public static void copyFileToDirectory(File srcFile, File destDir) throws IOException { + copyFileToDirectory(srcFile, destDir, true); + } + + /** + * Copies a file to a directory optionally preserving the file date. + *

+ * This method copies the contents of the specified source file + * to a file of the same name in the specified destination directory. + * The destination directory is created if it does not exist. + * If the destination file exists, then this method will overwrite it. + *

+ * Note: Setting preserveFileDate to + * {@code true} tries to preserve the file's last modified + * date/times using {@link File#setLastModified(long)}, however it is + * not guaranteed that the operation will succeed. + * If the modification operation fails, no indication is provided. + * + * @param srcFile an existing file to copy, must not be {@code null} + * @param destDir the directory to place the copy in, must not be {@code null} + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFile(File, File, boolean) + * @since 1.3 + */ + public static void copyFileToDirectory(File srcFile, File destDir, boolean preserveFileDate) throws IOException { + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (destDir.exists() && destDir.isDirectory() == false) { + throw new IllegalArgumentException("Destination '" + destDir + "' is not a directory"); + } + File destFile = new File(destDir, srcFile.getName()); + copyFile(srcFile, destFile, preserveFileDate); + } + + /** + * Copies a file to a new location preserving the file date. + *

+ * This method copies the contents of the specified source file to the + * specified destination file. The directory holding the destination file is + * created if it does not exist. If the destination file exists, then this + * method will overwrite it. + *

+ * Note: This method tries to preserve the file's last + * modified date/times using {@link File#setLastModified(long)}, however + * it is not guaranteed that the operation will succeed. + * If the modification operation fails, no indication is provided. + * + * @param srcFile an existing file to copy, must not be {@code null} + * @param destFile the new file, must not be {@code null} + * + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFileToDirectory(File, File) + */ + public static void copyFile(File srcFile, File destFile) throws IOException { + copyFile(srcFile, destFile, true); + } + + /** + * Copies a file to a new location. + *

+ * This method copies the contents of the specified source file + * to the specified destination file. + * The directory holding the destination file is created if it does not exist. + * If the destination file exists, then this method will overwrite it. + *

+ * Note: Setting preserveFileDate to + * {@code true} tries to preserve the file's last modified + * date/times using {@link File#setLastModified(long)}, however it is + * not guaranteed that the operation will succeed. + * If the modification operation fails, no indication is provided. + * + * @param srcFile an existing file to copy, must not be {@code null} + * @param destFile the new file, must not be {@code null} + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @see #copyFileToDirectory(File, File, boolean) + */ + public static void copyFile(File srcFile, File destFile, + boolean preserveFileDate) throws IOException { + if (srcFile == null) { + throw new NullPointerException("Source must not be null"); + } + if (destFile == null) { + throw new NullPointerException("Destination must not be null"); + } + if (srcFile.exists() == false) { + throw new FileNotFoundException("Source '" + srcFile + "' does not exist"); + } + if (srcFile.isDirectory()) { + throw new IOException("Source '" + srcFile + "' exists but is a directory"); + } + if (srcFile.getCanonicalPath().equals(destFile.getCanonicalPath())) { + throw new IOException("Source '" + srcFile + "' and destination '" + destFile + "' are the same"); + } + File parentFile = destFile.getParentFile(); + if (parentFile != null) { + if (!parentFile.mkdirs() && !parentFile.isDirectory()) { + throw new IOException("Destination '" + parentFile + "' directory cannot be created"); + } + } + if (destFile.exists() && destFile.canWrite() == false) { + throw new IOException("Destination '" + destFile + "' exists but is read-only"); + } + doCopyFile(srcFile, destFile, preserveFileDate); + } + + /** + * Copy bytes from a File to an OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a BufferedInputStream. + *

+ * + * @param input + * the File to read from + * @param output + * the OutputStream to write to + * @return the number of bytes copied + * @throws NullPointerException + * if the input or output is null + * @throws IOException + * if an I/O error occurs + * @since 2.1 + */ + public static long copyFile(File input, OutputStream output) throws IOException { + final FileInputStream fis = new FileInputStream(input); + try { + return IOUtils.copyLarge(fis, output); + } finally { + fis.close(); + } + } + + /** + * Internal copy file method. + * + * @param srcFile the validated source file, must not be {@code null} + * @param destFile the validated destination file, must not be {@code null} + * @param preserveFileDate whether to preserve the file date + * @throws IOException if an error occurs + */ + private static void doCopyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException { + if (destFile.exists() && destFile.isDirectory()) { + throw new IOException("Destination '" + destFile + "' exists but is a directory"); + } + + FileInputStream fis = null; + FileOutputStream fos = null; + FileChannel input = null; + FileChannel output = null; + try { + fis = new FileInputStream(srcFile); + fos = new FileOutputStream(destFile); + input = fis.getChannel(); + output = fos.getChannel(); + long size = input.size(); + long pos = 0; + long count = 0; + while (pos < size) { + count = size - pos > FILE_COPY_BUFFER_SIZE ? FILE_COPY_BUFFER_SIZE : size - pos; + pos += output.transferFrom(input, pos, count); + } + } finally { + IOUtils.closeQuietly(output); + IOUtils.closeQuietly(fos); + IOUtils.closeQuietly(input); + IOUtils.closeQuietly(fis); + } + + if (srcFile.length() != destFile.length()) { + throw new IOException("Failed to copy full contents from '" + + srcFile + "' to '" + destFile + "'"); + } + if (preserveFileDate) { + destFile.setLastModified(srcFile.lastModified()); + } + } + + //----------------------------------------------------------------------- + /** + * Copies a directory to within another directory preserving the file dates. + *

+ * This method copies the source directory and all its contents to a + * directory of the same name in the specified destination directory. + *

+ * The destination directory is created if it does not exist. + * If the destination directory did exist, then this method merges + * the source with the destination, with the source taking precedence. + *

+ * Note: This method tries to preserve the files' last + * modified date/times using {@link File#setLastModified(long)}, however + * it is not guaranteed that those operations will succeed. + * If the modification operation fails, no indication is provided. + * + * @param srcDir an existing directory to copy, must not be {@code null} + * @param destDir the directory to place the copy in, must not be {@code null} + * + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since 1.2 + */ + public static void copyDirectoryToDirectory(File srcDir, File destDir) throws IOException { + if (srcDir == null) { + throw new NullPointerException("Source must not be null"); + } + if (srcDir.exists() && srcDir.isDirectory() == false) { + throw new IllegalArgumentException("Source '" + destDir + "' is not a directory"); + } + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (destDir.exists() && destDir.isDirectory() == false) { + throw new IllegalArgumentException("Destination '" + destDir + "' is not a directory"); + } + copyDirectory(srcDir, new File(destDir, srcDir.getName()), true); + } + + /** + * Copies a whole directory to a new location preserving the file dates. + *

+ * This method copies the specified directory and all its child + * directories and files to the specified destination. + * The destination is the new location and name of the directory. + *

+ * The destination directory is created if it does not exist. + * If the destination directory did exist, then this method merges + * the source with the destination, with the source taking precedence. + *

+ * Note: This method tries to preserve the files' last + * modified date/times using {@link File#setLastModified(long)}, however + * it is not guaranteed that those operations will succeed. + * If the modification operation fails, no indication is provided. + * + * @param srcDir an existing directory to copy, must not be {@code null} + * @param destDir the new directory, must not be {@code null} + * + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since 1.1 + */ + public static void copyDirectory(File srcDir, File destDir) throws IOException { + copyDirectory(srcDir, destDir, true); + } + + /** + * Copies a whole directory to a new location. + *

+ * This method copies the contents of the specified source directory + * to within the specified destination directory. + *

+ * The destination directory is created if it does not exist. + * If the destination directory did exist, then this method merges + * the source with the destination, with the source taking precedence. + *

+ * Note: Setting preserveFileDate to + * {@code true} tries to preserve the files' last modified + * date/times using {@link File#setLastModified(long)}, however it is + * not guaranteed that those operations will succeed. + * If the modification operation fails, no indication is provided. + * + * @param srcDir an existing directory to copy, must not be {@code null} + * @param destDir the new directory, must not be {@code null} + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since 1.1 + */ + public static void copyDirectory(File srcDir, File destDir, + boolean preserveFileDate) throws IOException { + copyDirectory(srcDir, destDir, null, preserveFileDate); + } + + /** + * Copies a filtered directory to a new location preserving the file dates. + *

+ * This method copies the contents of the specified source directory + * to within the specified destination directory. + *

+ * The destination directory is created if it does not exist. + * If the destination directory did exist, then this method merges + * the source with the destination, with the source taking precedence. + *

+ * Note: This method tries to preserve the files' last + * modified date/times using {@link File#setLastModified(long)}, however + * it is not guaranteed that those operations will succeed. + * If the modification operation fails, no indication is provided. + * + *

Example: Copy directories only

+ *
+     *  // only copy the directory structure
+     *  FileUtils.copyDirectory(srcDir, destDir, DirectoryFileFilter.DIRECTORY);
+     *  
+ * + *

Example: Copy directories and txt files

+ *
+     *  // Create a filter for ".txt" files
+     *  IOFileFilter txtSuffixFilter = FileFilterUtils.suffixFileFilter(".txt");
+     *  IOFileFilter txtFiles = FileFilterUtils.andFileFilter(FileFileFilter.FILE, txtSuffixFilter);
+     *
+     *  // Create a filter for either directories or ".txt" files
+     *  FileFilter filter = FileFilterUtils.orFileFilter(DirectoryFileFilter.DIRECTORY, txtFiles);
+     *
+     *  // Copy using the filter
+     *  FileUtils.copyDirectory(srcDir, destDir, filter);
+     *  
+ * + * @param srcDir an existing directory to copy, must not be {@code null} + * @param destDir the new directory, must not be {@code null} + * @param filter the filter to apply, null means copy all directories and files + * should be the same as the original + * + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since 1.4 + */ + public static void copyDirectory(File srcDir, File destDir, + FileFilter filter) throws IOException { + copyDirectory(srcDir, destDir, filter, true); + } + + /** + * Copies a filtered directory to a new location. + *

+ * This method copies the contents of the specified source directory + * to within the specified destination directory. + *

+ * The destination directory is created if it does not exist. + * If the destination directory did exist, then this method merges + * the source with the destination, with the source taking precedence. + *

+ * Note: Setting preserveFileDate to + * {@code true} tries to preserve the files' last modified + * date/times using {@link File#setLastModified(long)}, however it is + * not guaranteed that those operations will succeed. + * If the modification operation fails, no indication is provided. + * + *

Example: Copy directories only

+ *
+     *  // only copy the directory structure
+     *  FileUtils.copyDirectory(srcDir, destDir, DirectoryFileFilter.DIRECTORY, false);
+     *  
+ * + *

Example: Copy directories and txt files

+ *
+     *  // Create a filter for ".txt" files
+     *  IOFileFilter txtSuffixFilter = FileFilterUtils.suffixFileFilter(".txt");
+     *  IOFileFilter txtFiles = FileFilterUtils.andFileFilter(FileFileFilter.FILE, txtSuffixFilter);
+     *
+     *  // Create a filter for either directories or ".txt" files
+     *  FileFilter filter = FileFilterUtils.orFileFilter(DirectoryFileFilter.DIRECTORY, txtFiles);
+     *
+     *  // Copy using the filter
+     *  FileUtils.copyDirectory(srcDir, destDir, filter, false);
+     *  
+ * + * @param srcDir an existing directory to copy, must not be {@code null} + * @param destDir the new directory, must not be {@code null} + * @param filter the filter to apply, null means copy all directories and files + * @param preserveFileDate true if the file date of the copy + * should be the same as the original + * + * @throws NullPointerException if source or destination is {@code null} + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs during copying + * @since 1.4 + */ + public static void copyDirectory(File srcDir, File destDir, + FileFilter filter, boolean preserveFileDate) throws IOException { + if (srcDir == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (srcDir.exists() == false) { + throw new FileNotFoundException("Source '" + srcDir + "' does not exist"); + } + if (srcDir.isDirectory() == false) { + throw new IOException("Source '" + srcDir + "' exists but is not a directory"); + } + if (srcDir.getCanonicalPath().equals(destDir.getCanonicalPath())) { + throw new IOException("Source '" + srcDir + "' and destination '" + destDir + "' are the same"); + } + + // Cater for destination being directory within the source directory (see IO-141) + List exclusionList = null; + if (destDir.getCanonicalPath().startsWith(srcDir.getCanonicalPath())) { + File[] srcFiles = filter == null ? srcDir.listFiles() : srcDir.listFiles(filter); + if (srcFiles != null && srcFiles.length > 0) { + exclusionList = new ArrayList(srcFiles.length); + for (File srcFile : srcFiles) { + File copiedFile = new File(destDir, srcFile.getName()); + exclusionList.add(copiedFile.getCanonicalPath()); + } + } + } + doCopyDirectory(srcDir, destDir, filter, preserveFileDate, exclusionList); + } + + /** + * Internal copy directory method. + * + * @param srcDir the validated source directory, must not be {@code null} + * @param destDir the validated destination directory, must not be {@code null} + * @param filter the filter to apply, null means copy all directories and files + * @param preserveFileDate whether to preserve the file date + * @param exclusionList List of files and directories to exclude from the copy, may be null + * @throws IOException if an error occurs + * @since 1.1 + */ + private static void doCopyDirectory(File srcDir, File destDir, FileFilter filter, + boolean preserveFileDate, List exclusionList) throws IOException { + // recurse + File[] srcFiles = filter == null ? srcDir.listFiles() : srcDir.listFiles(filter); + if (srcFiles == null) { // null if abstract pathname does not denote a directory, or if an I/O error occurs + throw new IOException("Failed to list contents of " + srcDir); + } + if (destDir.exists()) { + if (destDir.isDirectory() == false) { + throw new IOException("Destination '" + destDir + "' exists but is not a directory"); + } + } else { + if (!destDir.mkdirs() && !destDir.isDirectory()) { + throw new IOException("Destination '" + destDir + "' directory cannot be created"); + } + } + if (destDir.canWrite() == false) { + throw new IOException("Destination '" + destDir + "' cannot be written to"); + } + for (File srcFile : srcFiles) { + File dstFile = new File(destDir, srcFile.getName()); + if (exclusionList == null || !exclusionList.contains(srcFile.getCanonicalPath())) { + if (srcFile.isDirectory()) { + doCopyDirectory(srcFile, dstFile, filter, preserveFileDate, exclusionList); + } else { + doCopyFile(srcFile, dstFile, preserveFileDate); + } + } + } + + // Do this last, as the above has probably affected directory metadata + if (preserveFileDate) { + destDir.setLastModified(srcDir.lastModified()); + } + } + + //----------------------------------------------------------------------- + /** + * Copies bytes from the URL source to a file + * destination. The directories up to destination + * will be created if they don't already exist. destination + * will be overwritten if it already exists. + *

+ * Warning: this method does not set a connection or read timeout and thus + * might block forever. Use {@link #copyURLToFile(URL, File, int, int)} + * with reasonable timeouts to prevent this. + * + * @param source the URL to copy bytes from, must not be {@code null} + * @param destination the non-directory File to write bytes to + * (possibly overwriting), must not be {@code null} + * @throws IOException if source URL cannot be opened + * @throws IOException if destination is a directory + * @throws IOException if destination cannot be written + * @throws IOException if destination needs creating but can't be + * @throws IOException if an IO error occurs during copying + */ + public static void copyURLToFile(URL source, File destination) throws IOException { + InputStream input = source.openStream(); + copyInputStreamToFile(input, destination); + } + + /** + * Copies bytes from the URL source to a file + * destination. The directories up to destination + * will be created if they don't already exist. destination + * will be overwritten if it already exists. + * + * @param source the URL to copy bytes from, must not be {@code null} + * @param destination the non-directory File to write bytes to + * (possibly overwriting), must not be {@code null} + * @param connectionTimeout the number of milliseconds until this method + * will timeout if no connection could be established to the source + * @param readTimeout the number of milliseconds until this method will + * timeout if no data could be read from the source + * @throws IOException if source URL cannot be opened + * @throws IOException if destination is a directory + * @throws IOException if destination cannot be written + * @throws IOException if destination needs creating but can't be + * @throws IOException if an IO error occurs during copying + * @since 2.0 + */ + public static void copyURLToFile(URL source, File destination, + int connectionTimeout, int readTimeout) throws IOException { + URLConnection connection = source.openConnection(); + connection.setConnectTimeout(connectionTimeout); + connection.setReadTimeout(readTimeout); + InputStream input = connection.getInputStream(); + copyInputStreamToFile(input, destination); + } + + /** + * Copies bytes from an {@link InputStream} source to a file + * destination. The directories up to destination + * will be created if they don't already exist. destination + * will be overwritten if it already exists. + * + * @param source the InputStream to copy bytes from, must not be {@code null} + * @param destination the non-directory File to write bytes to + * (possibly overwriting), must not be {@code null} + * @throws IOException if destination is a directory + * @throws IOException if destination cannot be written + * @throws IOException if destination needs creating but can't be + * @throws IOException if an IO error occurs during copying + * @since 2.0 + */ + public static void copyInputStreamToFile(InputStream source, File destination) throws IOException { + try { + FileOutputStream output = openOutputStream(destination); + try { + IOUtils.copy(source, output); + output.close(); // don't swallow close Exception if copy completes normally + } finally { + IOUtils.closeQuietly(output); + } + } finally { + IOUtils.closeQuietly(source); + } + } + + //----------------------------------------------------------------------- + /** + * Deletes a directory recursively. + * + * @param directory directory to delete + * @throws IOException in case deletion is unsuccessful + */ + public static void deleteDirectory(File directory) throws IOException { + if (!directory.exists()) { + return; + } + + if (!isSymlink(directory)) { + cleanDirectory(directory); + } + + if (!directory.delete()) { + String message = + "Unable to delete directory " + directory + "."; + throw new IOException(message); + } + } + + /** + * Deletes a file, never throwing an exception. If file is a directory, delete it and all sub-directories. + *

+ * The difference between File.delete() and this method are: + *

    + *
  • A directory to be deleted does not have to be empty.
  • + *
  • No exceptions are thrown when a file or directory cannot be deleted.
  • + *
+ * + * @param file file or directory to delete, can be {@code null} + * @return {@code true} if the file or directory was deleted, otherwise + * {@code false} + * + * @since 1.4 + */ + public static boolean deleteQuietly(File file) { + if (file == null) { + return false; + } + try { + if (file.isDirectory()) { + cleanDirectory(file); + } + } catch (Exception ignored) { + } + + try { + return file.delete(); + } catch (Exception ignored) { + return false; + } + } + + /** + * Determines whether the {@code parent} directory contains the {@code child} element (a file or directory). + *

+ * Files are normalized before comparison. + *

+ * + * Edge cases: + *
    + *
  • A {@code directory} must not be null: if null, throw IllegalArgumentException
  • + *
  • A {@code directory} must be a directory: if not a directory, throw IllegalArgumentException
  • + *
  • A directory does not contain itself: return false
  • + *
  • A null child file is not contained in any parent: return false
  • + *
+ * + * @param directory + * the file to consider as the parent. + * @param child + * the file to consider as the child. + * @return true is the candidate leaf is under by the specified composite. False otherwise. + * @throws IOException + * if an IO error occurs while checking the files. + * @since 2.2 + * @see FilenameUtils#directoryContains(String, String) + */ + public static boolean directoryContains(final File directory, final File child) throws IOException { + + // Fail fast against NullPointerException + if (directory == null) { + throw new IllegalArgumentException("Directory must not be null"); + } + + if (!directory.isDirectory()) { + throw new IllegalArgumentException("Not a directory: " + directory); + } + + if (child == null) { + return false; + } + + if (!directory.exists() || !child.exists()) { + return false; + } + + // Canonicalize paths (normalizes relative paths) + String canonicalParent = directory.getCanonicalPath(); + String canonicalChild = child.getCanonicalPath(); + + return FilenameUtils.directoryContains(canonicalParent, canonicalChild); + } + + /** + * Cleans a directory without deleting it. + * + * @param directory directory to clean + * @throws IOException in case cleaning is unsuccessful + */ + public static void cleanDirectory(File directory) throws IOException { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (File file : files) { + try { + forceDelete(file); + } catch (IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } + + //----------------------------------------------------------------------- + /** + * Waits for NFS to propagate a file creation, imposing a timeout. + *

+ * This method repeatedly tests {@link File#exists()} until it returns + * true up to the maximum time specified in seconds. + * + * @param file the file to check, must not be {@code null} + * @param seconds the maximum time in seconds to wait + * @return true if file exists + * @throws NullPointerException if the file is {@code null} + */ + public static boolean waitFor(File file, int seconds) { + int timeout = 0; + int tick = 0; + while (!file.exists()) { + if (tick++ >= 10) { + tick = 0; + if (timeout++ > seconds) { + return false; + } + } + try { + Thread.sleep(100); + } catch (InterruptedException ignore) { + // ignore exception + } catch (Exception ex) { + break; + } + } + return true; + } + + //----------------------------------------------------------------------- + /** + * Reads the contents of a file into a String. + * The file is always closed. + * + * @param file the file to read, must not be {@code null} + * @param encoding the encoding to use, {@code null} means platform default + * @return the file contents, never {@code null} + * @throws IOException in case of an I/O error + * @since 2.3 + */ + public static String readFileToString(File file, Charset encoding) throws IOException { + InputStream in = null; + try { + in = openInputStream(file); + return IOUtils.toString(in, Charsets.toCharset(encoding)); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Reads the contents of a file into a String. The file is always closed. + * + * @param file + * the file to read, must not be {@code null} + * @param encoding + * the encoding to use, {@code null} means platform default + * @return the file contents, never {@code null} + * @throws IOException + * in case of an I/O error + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 2.3 + */ + public static String readFileToString(File file, String encoding) throws IOException { + return readFileToString(file, Charsets.toCharset(encoding)); + } + + + /** + * Reads the contents of a file into a String using the default encoding for the VM. + * The file is always closed. + * + * @param file the file to read, must not be {@code null} + * @return the file contents, never {@code null} + * @throws IOException in case of an I/O error + * @since 1.3.1 + */ + public static String readFileToString(File file) throws IOException { + return readFileToString(file, Charset.defaultCharset()); + } + + /** + * Reads the contents of a file into a byte array. + * The file is always closed. + * + * @param file the file to read, must not be {@code null} + * @return the file contents, never {@code null} + * @throws IOException in case of an I/O error + * @since 1.1 + */ + public static byte[] readFileToByteArray(File file) throws IOException { + InputStream in = null; + try { + in = openInputStream(file); + return IOUtils.toByteArray(in, file.length()); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Reads the contents of a file line by line to a List of Strings. + * The file is always closed. + * + * @param file the file to read, must not be {@code null} + * @param encoding the encoding to use, {@code null} means platform default + * @return the list of Strings representing each line in the file, never {@code null} + * @throws IOException in case of an I/O error + * @since 2.3 + */ + public static List readLines(File file, Charset encoding) throws IOException { + InputStream in = null; + try { + in = openInputStream(file); + return IOUtils.readLines(in, Charsets.toCharset(encoding)); + } finally { + IOUtils.closeQuietly(in); + } + } + + /** + * Reads the contents of a file line by line to a List of Strings. The file is always closed. + * + * @param file + * the file to read, must not be {@code null} + * @param encoding + * the encoding to use, {@code null} means platform default + * @return the list of Strings representing each line in the file, never {@code null} + * @throws IOException + * in case of an I/O error + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + */ + public static List readLines(File file, String encoding) throws IOException { + return readLines(file, Charsets.toCharset(encoding)); + } + + /** + * Reads the contents of a file line by line to a List of Strings using the default encoding for the VM. + * The file is always closed. + * + * @param file the file to read, must not be {@code null} + * @return the list of Strings representing each line in the file, never {@code null} + * @throws IOException in case of an I/O error + * @since 1.3 + */ + public static List readLines(File file) throws IOException { + return readLines(file, Charset.defaultCharset()); + } + + /** + * Returns an Iterator for the lines in a File. + *

+ * This method opens an InputStream for the file. + * When you have finished with the iterator you should close the stream + * to free internal resources. This can be done by calling the + * {@link LineIterator#close()} or + * {@link LineIterator#closeQuietly(LineIterator)} method. + *

+ * The recommended usage pattern is: + *

+     * LineIterator it = FileUtils.lineIterator(file, "UTF-8");
+     * try {
+     *   while (it.hasNext()) {
+     *     String line = it.nextLine();
+     *     /// do something with line
+     *   }
+     * } finally {
+     *   LineIterator.closeQuietly(iterator);
+     * }
+     * 
+ *

+ * If an exception occurs during the creation of the iterator, the + * underlying stream is closed. + * + * @param file the file to open for input, must not be {@code null} + * @param encoding the encoding to use, {@code null} means platform default + * @return an Iterator of the lines in the file, never {@code null} + * @throws IOException in case of an I/O error (file closed) + * @since 1.2 + */ + public static LineIterator lineIterator(File file, String encoding) throws IOException { + InputStream in = null; + try { + in = openInputStream(file); + return IOUtils.lineIterator(in, encoding); + } catch (IOException ex) { + IOUtils.closeQuietly(in); + throw ex; + } catch (RuntimeException ex) { + IOUtils.closeQuietly(in); + throw ex; + } + } + + /** + * Returns an Iterator for the lines in a File using the default encoding for the VM. + * + * @param file the file to open for input, must not be {@code null} + * @return an Iterator of the lines in the file, never {@code null} + * @throws IOException in case of an I/O error (file closed) + * @since 1.3 + * @see #lineIterator(File, String) + */ + public static LineIterator lineIterator(File file) throws IOException { + return lineIterator(file, null); + } + + //----------------------------------------------------------------------- + /** + * Writes a String to a file creating the file if it does not exist. + * + * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, {@code null} means platform default + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + * @since 2.4 + */ + public static void writeStringToFile(File file, String data, Charset encoding) throws IOException { + writeStringToFile(file, data, encoding, false); + } + + /** + * Writes a String to a file creating the file if it does not exist. + * + * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, {@code null} means platform default + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + */ + public static void writeStringToFile(File file, String data, String encoding) throws IOException { + writeStringToFile(file, data, encoding, false); + } + + /** + * Writes a String to a file creating the file if it does not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, {@code null} means platform default + * @param append if {@code true}, then the String will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @since 2.3 + */ + public static void writeStringToFile(File file, String data, Charset encoding, boolean append) throws IOException { + OutputStream out = null; + try { + out = openOutputStream(file, append); + IOUtils.write(data, out, encoding); + out.close(); // don't swallow close Exception if copy completes normally + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Writes a String to a file creating the file if it does not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, {@code null} means platform default + * @param append if {@code true}, then the String will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported by the VM + * @since 2.1 + */ + public static void writeStringToFile(File file, String data, String encoding, boolean append) throws IOException { + writeStringToFile(file, data, Charsets.toCharset(encoding), append); + } + + /** + * Writes a String to a file creating the file if it does not exist using the default encoding for the VM. + * + * @param file the file to write + * @param data the content to write to the file + * @throws IOException in case of an I/O error + */ + public static void writeStringToFile(File file, String data) throws IOException { + writeStringToFile(file, data, Charset.defaultCharset(), false); + } + + /** + * Writes a String to a file creating the file if it does not exist using the default encoding for the VM. + * + * @param file the file to write + * @param data the content to write to the file + * @param append if {@code true}, then the String will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @since 2.1 + */ + public static void writeStringToFile(File file, String data, boolean append) throws IOException { + writeStringToFile(file, data, Charset.defaultCharset(), append); + } + + /** + * Writes a CharSequence to a file creating the file if it does not exist using the default encoding for the VM. + * + * @param file the file to write + * @param data the content to write to the file + * @throws IOException in case of an I/O error + * @since 2.0 + */ + public static void write(File file, CharSequence data) throws IOException { + write(file, data, Charset.defaultCharset(), false); + } + + /** + * Writes a CharSequence to a file creating the file if it does not exist using the default encoding for the VM. + * + * @param file the file to write + * @param data the content to write to the file + * @param append if {@code true}, then the data will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @since 2.1 + */ + public static void write(File file, CharSequence data, boolean append) throws IOException { + write(file, data, Charset.defaultCharset(), append); + } + + /** + * Writes a CharSequence to a file creating the file if it does not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, {@code null} means platform default + * @throws IOException in case of an I/O error + * @since 2.3 + */ + public static void write(File file, CharSequence data, Charset encoding) throws IOException { + write(file, data, encoding, false); + } + + /** + * Writes a CharSequence to a file creating the file if it does not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, {@code null} means platform default + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + * @since 2.0 + */ + public static void write(File file, CharSequence data, String encoding) throws IOException { + write(file, data, encoding, false); + } + + /** + * Writes a CharSequence to a file creating the file if it does not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, {@code null} means platform default + * @param append if {@code true}, then the data will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @since 2.3 + */ + public static void write(File file, CharSequence data, Charset encoding, boolean append) throws IOException { + String str = data == null ? null : data.toString(); + writeStringToFile(file, str, encoding, append); + } + + /** + * Writes a CharSequence to a file creating the file if it does not exist. + * + * @param file the file to write + * @param data the content to write to the file + * @param encoding the encoding to use, {@code null} means platform default + * @param append if {@code true}, then the data will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported by the VM + * @since IO 2.1 + */ + public static void write(File file, CharSequence data, String encoding, boolean append) throws IOException { + write(file, data, Charsets.toCharset(encoding), append); + } + + /** + * Writes a byte array to a file creating the file if it does not exist. + *

+ * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write to + * @param data the content to write to the file + * @throws IOException in case of an I/O error + * @since 1.1 + */ + public static void writeByteArrayToFile(File file, byte[] data) throws IOException { + writeByteArrayToFile(file, data, false); + } + + /** + * Writes a byte array to a file creating the file if it does not exist. + * + * @param file the file to write to + * @param data the content to write to the file + * @param append if {@code true}, then bytes will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @since IO 2.1 + */ + public static void writeByteArrayToFile(File file, byte[] data, boolean append) throws IOException { + OutputStream out = null; + try { + out = openOutputStream(file, append); + out.write(data); + out.close(); // don't swallow close Exception if copy completes normally + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line. + * The specified character encoding and the default line ending will be used. + *

+ * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write to + * @param encoding the encoding to use, {@code null} means platform default + * @param lines the lines to write, {@code null} entries produce blank lines + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + * @since 1.1 + */ + public static void writeLines(File file, String encoding, Collection lines) throws IOException { + writeLines(file, encoding, lines, null, false); + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line, optionally appending. + * The specified character encoding and the default line ending will be used. + * + * @param file the file to write to + * @param encoding the encoding to use, {@code null} means platform default + * @param lines the lines to write, {@code null} entries produce blank lines + * @param append if {@code true}, then the lines will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + * @since 2.1 + */ + public static void writeLines(File file, String encoding, Collection lines, boolean append) throws IOException { + writeLines(file, encoding, lines, null, append); + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line. + * The default VM encoding and the default line ending will be used. + * + * @param file the file to write to + * @param lines the lines to write, {@code null} entries produce blank lines + * @throws IOException in case of an I/O error + * @since 1.3 + */ + public static void writeLines(File file, Collection lines) throws IOException { + writeLines(file, null, lines, null, false); + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line. + * The default VM encoding and the default line ending will be used. + * + * @param file the file to write to + * @param lines the lines to write, {@code null} entries produce blank lines + * @param append if {@code true}, then the lines will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @since 2.1 + */ + public static void writeLines(File file, Collection lines, boolean append) throws IOException { + writeLines(file, null, lines, null, append); + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line. + * The specified character encoding and the line ending will be used. + *

+ * NOTE: As from v1.3, the parent directories of the file will be created + * if they do not exist. + * + * @param file the file to write to + * @param encoding the encoding to use, {@code null} means platform default + * @param lines the lines to write, {@code null} entries produce blank lines + * @param lineEnding the line separator to use, {@code null} is system default + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + * @since 1.1 + */ + public static void writeLines(File file, String encoding, Collection lines, String lineEnding) + throws IOException { + writeLines(file, encoding, lines, lineEnding, false); + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line. + * The specified character encoding and the line ending will be used. + * + * @param file the file to write to + * @param encoding the encoding to use, {@code null} means platform default + * @param lines the lines to write, {@code null} entries produce blank lines + * @param lineEnding the line separator to use, {@code null} is system default + * @param append if {@code true}, then the lines will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @throws java.io.UnsupportedEncodingException if the encoding is not supported by the VM + * @since 2.1 + */ + public static void writeLines(File file, String encoding, Collection lines, String lineEnding, boolean append) + throws IOException { + FileOutputStream out = null; + try { + out = openOutputStream(file, append); + final BufferedOutputStream buffer = new BufferedOutputStream(out); + IOUtils.writeLines(lines, lineEnding, buffer, encoding); + buffer.flush(); + out.close(); // don't swallow close Exception if copy completes normally + } finally { + IOUtils.closeQuietly(out); + } + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line. + * The default VM encoding and the specified line ending will be used. + * + * @param file the file to write to + * @param lines the lines to write, {@code null} entries produce blank lines + * @param lineEnding the line separator to use, {@code null} is system default + * @throws IOException in case of an I/O error + * @since 1.3 + */ + public static void writeLines(File file, Collection lines, String lineEnding) throws IOException { + writeLines(file, null, lines, lineEnding, false); + } + + /** + * Writes the toString() value of each item in a collection to + * the specified File line by line. + * The default VM encoding and the specified line ending will be used. + * + * @param file the file to write to + * @param lines the lines to write, {@code null} entries produce blank lines + * @param lineEnding the line separator to use, {@code null} is system default + * @param append if {@code true}, then the lines will be added to the + * end of the file rather than overwriting + * @throws IOException in case of an I/O error + * @since 2.1 + */ + public static void writeLines(File file, Collection lines, String lineEnding, boolean append) + throws IOException { + writeLines(file, null, lines, lineEnding, append); + } + + //----------------------------------------------------------------------- + /** + * Deletes a file. If file is a directory, delete it and all sub-directories. + *

+ * The difference between File.delete() and this method are: + *

    + *
  • A directory to be deleted does not have to be empty.
  • + *
  • You get exceptions when a file or directory cannot be deleted. + * (java.io.File methods returns a boolean)
  • + *
+ * + * @param file file or directory to delete, must not be {@code null} + * @throws NullPointerException if the directory is {@code null} + * @throws FileNotFoundException if the file was not found + * @throws IOException in case deletion is unsuccessful + */ + public static void forceDelete(File file) throws IOException { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + boolean filePresent = file.exists(); + if (!file.delete()) { + if (!filePresent){ + throw new FileNotFoundException("File does not exist: " + file); + } + String message = + "Unable to delete file: " + file; + throw new IOException(message); + } + } + } + + /** + * Schedules a file to be deleted when JVM exits. + * If file is directory delete it and all sub-directories. + * + * @param file file or directory to delete, must not be {@code null} + * @throws NullPointerException if the file is {@code null} + * @throws IOException in case deletion is unsuccessful + */ + public static void forceDeleteOnExit(File file) throws IOException { + if (file.isDirectory()) { + deleteDirectoryOnExit(file); + } else { + file.deleteOnExit(); + } + } + + /** + * Schedules a directory recursively for deletion on JVM exit. + * + * @param directory directory to delete, must not be {@code null} + * @throws NullPointerException if the directory is {@code null} + * @throws IOException in case deletion is unsuccessful + */ + private static void deleteDirectoryOnExit(File directory) throws IOException { + if (!directory.exists()) { + return; + } + + directory.deleteOnExit(); + if (!isSymlink(directory)) { + cleanDirectoryOnExit(directory); + } + } + + /** + * Cleans a directory without deleting it. + * + * @param directory directory to clean, must not be {@code null} + * @throws NullPointerException if the directory is {@code null} + * @throws IOException in case cleaning is unsuccessful + */ + private static void cleanDirectoryOnExit(File directory) throws IOException { + if (!directory.exists()) { + String message = directory + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (!directory.isDirectory()) { + String message = directory + " is not a directory"; + throw new IllegalArgumentException(message); + } + + File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + throw new IOException("Failed to list contents of " + directory); + } + + IOException exception = null; + for (File file : files) { + try { + forceDeleteOnExit(file); + } catch (IOException ioe) { + exception = ioe; + } + } + + if (null != exception) { + throw exception; + } + } + + /** + * Makes a directory, including any necessary but nonexistent parent + * directories. If a file already exists with specified name but it is + * not a directory then an IOException is thrown. + * If the directory cannot be created (or does not already exist) + * then an IOException is thrown. + * + * @param directory directory to create, must not be {@code null} + * @throws NullPointerException if the directory is {@code null} + * @throws IOException if the directory cannot be created or the file already exists but is not a directory + */ + public static void forceMkdir(File directory) throws IOException { + if (directory.exists()) { + if (!directory.isDirectory()) { + String message = + "File " + + directory + + " exists and is " + + "not a directory. Unable to create directory."; + throw new IOException(message); + } + } else { + if (!directory.mkdirs()) { + // Double-check that some other thread or process hasn't made + // the directory in the background + if (!directory.isDirectory()) + { + String message = + "Unable to create directory " + directory; + throw new IOException(message); + } + } + } + } + + //----------------------------------------------------------------------- + /** + * Returns the size of the specified file or directory. If the provided + * {@link File} is a regular file, then the file's length is returned. + * If the argument is a directory, then the size of the directory is + * calculated recursively. If a directory or subdirectory is security + * restricted, its size will not be included. + * + * @param file the regular file or directory to return the size + * of (must not be {@code null}). + * + * @return the length of the file, or recursive size of the directory, + * provided (in bytes). + * + * @throws NullPointerException if the file is {@code null} + * @throws IllegalArgumentException if the file does not exist. + * + * @since 2.0 + */ + public static long sizeOf(File file) { + + if (!file.exists()) { + String message = file + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (file.isDirectory()) { + return sizeOfDirectory(file); + } else { + return file.length(); + } + + } + + /** + * Returns the size of the specified file or directory. If the provided + * {@link File} is a regular file, then the file's length is returned. + * If the argument is a directory, then the size of the directory is + * calculated recursively. If a directory or subdirectory is security + * restricted, its size will not be included. + * + * @param file the regular file or directory to return the size + * of (must not be {@code null}). + * + * @return the length of the file, or recursive size of the directory, + * provided (in bytes). + * + * @throws NullPointerException if the file is {@code null} + * @throws IllegalArgumentException if the file does not exist. + * + * @since 2.4 + */ + public static BigInteger sizeOfAsBigInteger(File file) { + + if (!file.exists()) { + String message = file + " does not exist"; + throw new IllegalArgumentException(message); + } + + if (file.isDirectory()) { + return sizeOfDirectoryAsBigInteger(file); + } else { + return BigInteger.valueOf(file.length()); + } + + } + + /** + * Counts the size of a directory recursively (sum of the length of all files). + * + * @param directory + * directory to inspect, must not be {@code null} + * @return size of directory in bytes, 0 if directory is security restricted, a negative number when the real total + * is greater than {@link Long#MAX_VALUE}. + * @throws NullPointerException + * if the directory is {@code null} + */ + public static long sizeOfDirectory(File directory) { + checkDirectory(directory); + + final File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + return 0L; + } + long size = 0; + + for (final File file : files) { + try { + if (!isSymlink(file)) { + size += sizeOf(file); + if (size < 0) { + break; + } + } + } catch (IOException ioe) { + // Ignore exceptions caught when asking if a File is a symlink. + } + } + + return size; + } + + /** + * Counts the size of a directory recursively (sum of the length of all files). + * + * @param directory + * directory to inspect, must not be {@code null} + * @return size of directory in bytes, 0 if directory is security restricted. + * @throws NullPointerException + * if the directory is {@code null} + * @since 2.4 + */ + public static BigInteger sizeOfDirectoryAsBigInteger(File directory) { + checkDirectory(directory); + + final File[] files = directory.listFiles(); + if (files == null) { // null if security restricted + return BigInteger.ZERO; + } + BigInteger size = BigInteger.ZERO; + + for (final File file : files) { + try { + if (!isSymlink(file)) { + size = size.add(BigInteger.valueOf(sizeOf(file))); + } + } catch (IOException ioe) { + // Ignore exceptions caught when asking if a File is a symlink. + } + } + + return size; + } + + /** + * Checks that the given {@code File} exists and is a directory. + * + * @param directory The {@code File} to check. + * @throws IllegalArgumentException if the given {@code File} does not exist or is not a directory. + */ + private static void checkDirectory(File directory) { + if (!directory.exists()) { + throw new IllegalArgumentException(directory + " does not exist"); + } + if (!directory.isDirectory()) { + throw new IllegalArgumentException(directory + " is not a directory"); + } + } + + //----------------------------------------------------------------------- + /** + * Tests if the specified File is newer than the reference + * File. + * + * @param file the File of which the modification date must + * be compared, must not be {@code null} + * @param reference the File of which the modification date + * is used, must not be {@code null} + * @return true if the File exists and has been modified more + * recently than the reference File + * @throws IllegalArgumentException if the file is {@code null} + * @throws IllegalArgumentException if the reference file is {@code null} or doesn't exist + */ + public static boolean isFileNewer(File file, File reference) { + if (reference == null) { + throw new IllegalArgumentException("No specified reference file"); + } + if (!reference.exists()) { + throw new IllegalArgumentException("The reference file '" + + reference + "' doesn't exist"); + } + return isFileNewer(file, reference.lastModified()); + } + + /** + * Tests if the specified File is newer than the specified + * Date. + * + * @param file the File of which the modification date + * must be compared, must not be {@code null} + * @param date the date reference, must not be {@code null} + * @return true if the File exists and has been modified + * after the given Date. + * @throws IllegalArgumentException if the file is {@code null} + * @throws IllegalArgumentException if the date is {@code null} + */ + public static boolean isFileNewer(File file, Date date) { + if (date == null) { + throw new IllegalArgumentException("No specified date"); + } + return isFileNewer(file, date.getTime()); + } + + /** + * Tests if the specified File is newer than the specified + * time reference. + * + * @param file the File of which the modification date must + * be compared, must not be {@code null} + * @param timeMillis the time reference measured in milliseconds since the + * epoch (00:00:00 GMT, January 1, 1970) + * @return true if the File exists and has been modified after + * the given time reference. + * @throws IllegalArgumentException if the file is {@code null} + */ + public static boolean isFileNewer(File file, long timeMillis) { + if (file == null) { + throw new IllegalArgumentException("No specified file"); + } + if (!file.exists()) { + return false; + } + return file.lastModified() > timeMillis; + } + + + //----------------------------------------------------------------------- + /** + * Tests if the specified File is older than the reference + * File. + * + * @param file the File of which the modification date must + * be compared, must not be {@code null} + * @param reference the File of which the modification date + * is used, must not be {@code null} + * @return true if the File exists and has been modified before + * the reference File + * @throws IllegalArgumentException if the file is {@code null} + * @throws IllegalArgumentException if the reference file is {@code null} or doesn't exist + */ + public static boolean isFileOlder(File file, File reference) { + if (reference == null) { + throw new IllegalArgumentException("No specified reference file"); + } + if (!reference.exists()) { + throw new IllegalArgumentException("The reference file '" + + reference + "' doesn't exist"); + } + return isFileOlder(file, reference.lastModified()); + } + + /** + * Tests if the specified File is older than the specified + * Date. + * + * @param file the File of which the modification date + * must be compared, must not be {@code null} + * @param date the date reference, must not be {@code null} + * @return true if the File exists and has been modified + * before the given Date. + * @throws IllegalArgumentException if the file is {@code null} + * @throws IllegalArgumentException if the date is {@code null} + */ + public static boolean isFileOlder(File file, Date date) { + if (date == null) { + throw new IllegalArgumentException("No specified date"); + } + return isFileOlder(file, date.getTime()); + } + + /** + * Tests if the specified File is older than the specified + * time reference. + * + * @param file the File of which the modification date must + * be compared, must not be {@code null} + * @param timeMillis the time reference measured in milliseconds since the + * epoch (00:00:00 GMT, January 1, 1970) + * @return true if the File exists and has been modified before + * the given time reference. + * @throws IllegalArgumentException if the file is {@code null} + */ + public static boolean isFileOlder(File file, long timeMillis) { + if (file == null) { + throw new IllegalArgumentException("No specified file"); + } + if (!file.exists()) { + return false; + } + return file.lastModified() < timeMillis; + } + + //----------------------------------------------------------------------- + /** + * Computes the checksum of a file using the CRC32 checksum routine. + * The value of the checksum is returned. + * + * @param file the file to checksum, must not be {@code null} + * @return the checksum value + * @throws NullPointerException if the file or checksum is {@code null} + * @throws IllegalArgumentException if the file is a directory + * @throws IOException if an IO error occurs reading the file + * @since 1.3 + */ + public static long checksumCRC32(File file) throws IOException { + CRC32 crc = new CRC32(); + checksum(file, crc); + return crc.getValue(); + } + + /** + * Computes the checksum of a file using the specified checksum object. + * Multiple files may be checked using one Checksum instance + * if desired simply by reusing the same checksum object. + * For example: + *
+     *   long csum = FileUtils.checksum(file, new CRC32()).getValue();
+     * 
+ * + * @param file the file to checksum, must not be {@code null} + * @param checksum the checksum object to be used, must not be {@code null} + * @return the checksum specified, updated with the content of the file + * @throws NullPointerException if the file or checksum is {@code null} + * @throws IllegalArgumentException if the file is a directory + * @throws IOException if an IO error occurs reading the file + * @since 1.3 + */ + public static Checksum checksum(File file, Checksum checksum) throws IOException { + if (file.isDirectory()) { + throw new IllegalArgumentException("Checksums can't be computed on directories"); + } + InputStream in = null; + try { + in = new CheckedInputStream(new FileInputStream(file), checksum); + IOUtils.copy(in, new NullOutputStream()); + } finally { + IOUtils.closeQuietly(in); + } + return checksum; + } + + /** + * Moves a directory. + *

+ * When the destination directory is on another file system, do a "copy and delete". + * + * @param srcDir the directory to be moved + * @param destDir the destination directory + * @throws NullPointerException if source or destination is {@code null} + * @throws FileExistsException if the destination directory exists + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since 1.4 + */ + public static void moveDirectory(File srcDir, File destDir) throws IOException { + if (srcDir == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (!srcDir.exists()) { + throw new FileNotFoundException("Source '" + srcDir + "' does not exist"); + } + if (!srcDir.isDirectory()) { + throw new IOException("Source '" + srcDir + "' is not a directory"); + } + if (destDir.exists()) { + throw new FileExistsException("Destination '" + destDir + "' already exists"); + } + boolean rename = srcDir.renameTo(destDir); + if (!rename) { + if (destDir.getCanonicalPath().startsWith(srcDir.getCanonicalPath())) { + throw new IOException("Cannot move directory: "+srcDir+" to a subdirectory of itself: "+destDir); + } + copyDirectory( srcDir, destDir ); + deleteDirectory( srcDir ); + if (srcDir.exists()) { + throw new IOException("Failed to delete original directory '" + srcDir + + "' after copy to '" + destDir + "'"); + } + } + } + + /** + * Moves a directory to another directory. + * + * @param src the file to be moved + * @param destDir the destination file + * @param createDestDir If {@code true} create the destination directory, + * otherwise if {@code false} throw an IOException + * @throws NullPointerException if source or destination is {@code null} + * @throws FileExistsException if the directory exists in the destination directory + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since 1.4 + */ + public static void moveDirectoryToDirectory(File src, File destDir, boolean createDestDir) throws IOException { + if (src == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination directory must not be null"); + } + if (!destDir.exists() && createDestDir) { + destDir.mkdirs(); + } + if (!destDir.exists()) { + throw new FileNotFoundException("Destination directory '" + destDir + + "' does not exist [createDestDir=" + createDestDir +"]"); + } + if (!destDir.isDirectory()) { + throw new IOException("Destination '" + destDir + "' is not a directory"); + } + moveDirectory(src, new File(destDir, src.getName())); + + } + + /** + * Moves a file. + *

+ * When the destination file is on another file system, do a "copy and delete". + * + * @param srcFile the file to be moved + * @param destFile the destination file + * @throws NullPointerException if source or destination is {@code null} + * @throws FileExistsException if the destination file exists + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since 1.4 + */ + public static void moveFile(File srcFile, File destFile) throws IOException { + if (srcFile == null) { + throw new NullPointerException("Source must not be null"); + } + if (destFile == null) { + throw new NullPointerException("Destination must not be null"); + } + if (!srcFile.exists()) { + throw new FileNotFoundException("Source '" + srcFile + "' does not exist"); + } + if (srcFile.isDirectory()) { + throw new IOException("Source '" + srcFile + "' is a directory"); + } + if (destFile.exists()) { + throw new FileExistsException("Destination '" + destFile + "' already exists"); + } + if (destFile.isDirectory()) { + throw new IOException("Destination '" + destFile + "' is a directory"); + } + boolean rename = srcFile.renameTo(destFile); + if (!rename) { + copyFile( srcFile, destFile ); + if (!srcFile.delete()) { + FileUtils.deleteQuietly(destFile); + throw new IOException("Failed to delete original file '" + srcFile + + "' after copy to '" + destFile + "'"); + } + } + } + + /** + * Moves a file to a directory. + * + * @param srcFile the file to be moved + * @param destDir the destination file + * @param createDestDir If {@code true} create the destination directory, + * otherwise if {@code false} throw an IOException + * @throws NullPointerException if source or destination is {@code null} + * @throws FileExistsException if the destination file exists + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since 1.4 + */ + public static void moveFileToDirectory(File srcFile, File destDir, boolean createDestDir) throws IOException { + if (srcFile == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination directory must not be null"); + } + if (!destDir.exists() && createDestDir) { + destDir.mkdirs(); + } + if (!destDir.exists()) { + throw new FileNotFoundException("Destination directory '" + destDir + + "' does not exist [createDestDir=" + createDestDir +"]"); + } + if (!destDir.isDirectory()) { + throw new IOException("Destination '" + destDir + "' is not a directory"); + } + moveFile(srcFile, new File(destDir, srcFile.getName())); + } + + /** + * Moves a file or directory to the destination directory. + *

+ * When the destination is on another file system, do a "copy and delete". + * + * @param src the file or directory to be moved + * @param destDir the destination directory + * @param createDestDir If {@code true} create the destination directory, + * otherwise if {@code false} throw an IOException + * @throws NullPointerException if source or destination is {@code null} + * @throws FileExistsException if the directory or file exists in the destination directory + * @throws IOException if source or destination is invalid + * @throws IOException if an IO error occurs moving the file + * @since 1.4 + */ + public static void moveToDirectory(File src, File destDir, boolean createDestDir) throws IOException { + if (src == null) { + throw new NullPointerException("Source must not be null"); + } + if (destDir == null) { + throw new NullPointerException("Destination must not be null"); + } + if (!src.exists()) { + throw new FileNotFoundException("Source '" + src + "' does not exist"); + } + if (src.isDirectory()) { + moveDirectoryToDirectory(src, destDir, createDestDir); + } else { + moveFileToDirectory(src, destDir, createDestDir); + } + } + + /** + * Determines whether the specified file is a Symbolic Link rather than an actual file. + *

+ * Will not return true if there is a Symbolic Link anywhere in the path, + * only if the specific file is. + *

+ * Note: the current implementation always returns {@code false} if the system + * is detected as Windows using {@link FilenameUtils#isSystemWindows()} + * + * @param file the file to check + * @return true if the file is a Symbolic Link + * @throws IOException if an IO error occurs while checking the file + * @since 2.0 + */ + public static boolean isSymlink(File file) throws IOException { + if (file == null) { + throw new NullPointerException("File must not be null"); + } + if (FilenameUtils.isSystemWindows()) { + return false; + } + File fileInCanonicalDir = null; + if (file.getParent() == null) { + fileInCanonicalDir = file; + } else { + File canonicalDir = file.getParentFile().getCanonicalFile(); + fileInCanonicalDir = new File(canonicalDir, file.getName()); + } + + if (fileInCanonicalDir.getCanonicalFile().equals(fileInCanonicalDir.getAbsoluteFile())) { + return false; + } else { + return true; + } + } + +} diff --git a/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/FilenameUtils.java b/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/FilenameUtils.java new file mode 100644 index 0000000..56f3da7 --- /dev/null +++ b/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/FilenameUtils.java @@ -0,0 +1,1367 @@ +package org.gcube.dataanalysis.executor.rscripts.generic; + +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Stack; + +import org.apache.commons.io.IOCase; + +/** + * General filename and filepath manipulation utilities. + *

+ * When dealing with filenames you can hit problems when moving from a Windows based development machine to a Unix based production machine. This class aims to help avoid those problems. + *

+ * NOTE: You may be able to avoid using this class entirely simply by using JDK {@link java.io.File File} objects and the two argument constructor {@link java.io.File#File(java.io.File, java.lang.String) File(File,String)}. + *

+ * Most methods on this class are designed to work the same on both Unix and Windows. Those that don't include 'System', 'Unix' or 'Windows' in their name. + *

+ * Most methods recognise both separators (forward and back), and both sets of prefixes. See the javadoc of each method for details. + *

+ * This class defines six components within a filename (example C:\dev\project\file.txt): + *

    + *
  • the prefix - C:\
  • + *
  • the path - dev\project\
  • + *
  • the full path - C:\dev\project\
  • + *
  • the name - file.txt
  • + *
  • the base name - file
  • + *
  • the extension - txt
  • + *
+ * Note that this class works best if directory filenames end with a separator. If you omit the last separator, it is impossible to determine if the filename corresponds to a file or a directory. As a result, we have chosen to say it corresponds to a file. + *

+ * This class only supports Unix and Windows style names. Prefixes are matched as follows: + * + *

+ * Windows:
+ * a\b\c.txt           --> ""          --> relative
+ * \a\b\c.txt          --> "\"         --> current drive absolute
+ * C:a\b\c.txt         --> "C:"        --> drive relative
+ * C:\a\b\c.txt        --> "C:\"       --> absolute
+ * \\server\a\b\c.txt  --> "\\server\" --> UNC
+ * 
+ * Unix:
+ * a/b/c.txt           --> ""          --> relative
+ * /a/b/c.txt          --> "/"         --> absolute
+ * ~/a/b/c.txt         --> "~/"        --> current user
+ * ~                   --> "~/"        --> current user (slash added)
+ * ~user/a/b/c.txt     --> "~user/"    --> named user
+ * ~user               --> "~user/"    --> named user (slash added)
+ * 
+ * + * Both prefix styles are matched always, irrespective of the machine that you are currently running on. + *

+ * Origin of code: Excalibur, Alexandria, Tomcat, Commons-Utils. + * + * @version $Id: FilenameUtils.java 1307462 2012-03-30 15:13:11Z ggregory $ + * @since 1.1 + */ +public class FilenameUtils { + + /** + * The extension separator character. + * + * @since 1.4 + */ + public static final char EXTENSION_SEPARATOR = '.'; + + /** + * The extension separator String. + * + * @since 1.4 + */ + public static final String EXTENSION_SEPARATOR_STR = Character.toString(EXTENSION_SEPARATOR); + + /** + * The Unix separator character. + */ + private static final char UNIX_SEPARATOR = '/'; + + /** + * The Windows separator character. + */ + private static final char WINDOWS_SEPARATOR = '\\'; + + /** + * The system separator character. + */ + private static final char SYSTEM_SEPARATOR = File.separatorChar; + + /** + * The separator character that is the opposite of the system separator. + */ + private static final char OTHER_SEPARATOR; + static { + if (isSystemWindows()) { + OTHER_SEPARATOR = UNIX_SEPARATOR; + } else { + OTHER_SEPARATOR = WINDOWS_SEPARATOR; + } + } + + /** + * Instances should NOT be constructed in standard programming. + */ + public FilenameUtils() { + super(); + } + + // ----------------------------------------------------------------------- + /** + * Determines if Windows file system is in use. + * + * @return true if the system is Windows + */ + static boolean isSystemWindows() { + return SYSTEM_SEPARATOR == WINDOWS_SEPARATOR; + } + + // ----------------------------------------------------------------------- + /** + * Checks if the character is a separator. + * + * @param ch + * the character to check + * @return true if it is a separator character + */ + private static boolean isSeparator(char ch) { + return ch == UNIX_SEPARATOR || ch == WINDOWS_SEPARATOR; + } + + // ----------------------------------------------------------------------- + /** + * Normalizes a path, removing double and single dot path steps. + *

+ * This method normalizes a path to a standard format. The input may contain separators in either Unix or Windows format. The output will contain separators in the format of the system. + *

+ * A trailing slash will be retained. A double slash will be merged to a single slash (but UNC names are handled). A single dot path segment will be removed. A double dot will cause that path segment and the one before to be removed. If the double dot has no parent path segment to work with, {@code null} is returned. + *

+ * The output will be the same on both Unix and Windows except for the separator character. + * + *

+	 * /foo//               -->   /foo/
+	 * /foo/./              -->   /foo/
+	 * /foo/../bar          -->   /bar
+	 * /foo/../bar/         -->   /bar/
+	 * /foo/../bar/../baz   -->   /baz
+	 * //foo//./bar         -->   /foo/bar
+	 * /../                 -->   null
+	 * ../foo               -->   null
+	 * foo/bar/..           -->   foo/
+	 * foo/../../bar        -->   null
+	 * foo/../bar           -->   bar
+	 * //server/foo/../bar  -->   //server/bar
+	 * //server/../bar      -->   null
+	 * C:\foo\..\bar        -->   C:\bar
+	 * C:\..\bar            -->   null
+	 * ~/foo/../bar/        -->   ~/bar/
+	 * ~/../bar             -->   null
+	 * 
+ * + * (Note the file separator returned will be correct for Windows/Unix) + * + * @param filename + * the filename to normalize, null returns null + * @return the normalized filename, or null if invalid + */ + public static String normalize(String filename) { + return doNormalize(filename, SYSTEM_SEPARATOR, true); + } + + /** + * Normalizes a path, removing double and single dot path steps. + *

+ * This method normalizes a path to a standard format. The input may contain separators in either Unix or Windows format. The output will contain separators in the format specified. + *

+ * A trailing slash will be retained. A double slash will be merged to a single slash (but UNC names are handled). A single dot path segment will be removed. A double dot will cause that path segment and the one before to be removed. If the double dot has no parent path segment to work with, {@code null} is returned. + *

+ * The output will be the same on both Unix and Windows except for the separator character. + * + *

+	 * /foo//               -->   /foo/
+	 * /foo/./              -->   /foo/
+	 * /foo/../bar          -->   /bar
+	 * /foo/../bar/         -->   /bar/
+	 * /foo/../bar/../baz   -->   /baz
+	 * //foo//./bar         -->   /foo/bar
+	 * /../                 -->   null
+	 * ../foo               -->   null
+	 * foo/bar/..           -->   foo/
+	 * foo/../../bar        -->   null
+	 * foo/../bar           -->   bar
+	 * //server/foo/../bar  -->   //server/bar
+	 * //server/../bar      -->   null
+	 * C:\foo\..\bar        -->   C:\bar
+	 * C:\..\bar            -->   null
+	 * ~/foo/../bar/        -->   ~/bar/
+	 * ~/../bar             -->   null
+	 * 
+ * + * The output will be the same on both Unix and Windows including the separator character. + * + * @param filename + * the filename to normalize, null returns null + * @param unixSeparator + * {@code true} if a unix separator should be used or {@code false} if a windows separator should be used. + * @return the normalized filename, or null if invalid + * @since 2.0 + */ + public static String normalize(String filename, boolean unixSeparator) { + char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR; + return doNormalize(filename, separator, true); + } + + // ----------------------------------------------------------------------- + /** + * Normalizes a path, removing double and single dot path steps, and removing any final directory separator. + *

+ * This method normalizes a path to a standard format. The input may contain separators in either Unix or Windows format. The output will contain separators in the format of the system. + *

+ * A trailing slash will be removed. A double slash will be merged to a single slash (but UNC names are handled). A single dot path segment will be removed. A double dot will cause that path segment and the one before to be removed. If the double dot has no parent path segment to work with, {@code null} is returned. + *

+ * The output will be the same on both Unix and Windows except for the separator character. + * + *

+	 * /foo//               -->   /foo
+	 * /foo/./              -->   /foo
+	 * /foo/../bar          -->   /bar
+	 * /foo/../bar/         -->   /bar
+	 * /foo/../bar/../baz   -->   /baz
+	 * //foo//./bar         -->   /foo/bar
+	 * /../                 -->   null
+	 * ../foo               -->   null
+	 * foo/bar/..           -->   foo
+	 * foo/../../bar        -->   null
+	 * foo/../bar           -->   bar
+	 * //server/foo/../bar  -->   //server/bar
+	 * //server/../bar      -->   null
+	 * C:\foo\..\bar        -->   C:\bar
+	 * C:\..\bar            -->   null
+	 * ~/foo/../bar/        -->   ~/bar
+	 * ~/../bar             -->   null
+	 * 
+ * + * (Note the file separator returned will be correct for Windows/Unix) + * + * @param filename + * the filename to normalize, null returns null + * @return the normalized filename, or null if invalid + */ + public static String normalizeNoEndSeparator(String filename) { + return doNormalize(filename, SYSTEM_SEPARATOR, false); + } + + /** + * Normalizes a path, removing double and single dot path steps, and removing any final directory separator. + *

+ * This method normalizes a path to a standard format. The input may contain separators in either Unix or Windows format. The output will contain separators in the format specified. + *

+ * A trailing slash will be removed. A double slash will be merged to a single slash (but UNC names are handled). A single dot path segment will be removed. A double dot will cause that path segment and the one before to be removed. If the double dot has no parent path segment to work with, {@code null} is returned. + *

+ * The output will be the same on both Unix and Windows including the separator character. + * + *

+	 * /foo//               -->   /foo
+	 * /foo/./              -->   /foo
+	 * /foo/../bar          -->   /bar
+	 * /foo/../bar/         -->   /bar
+	 * /foo/../bar/../baz   -->   /baz
+	 * //foo//./bar         -->   /foo/bar
+	 * /../                 -->   null
+	 * ../foo               -->   null
+	 * foo/bar/..           -->   foo
+	 * foo/../../bar        -->   null
+	 * foo/../bar           -->   bar
+	 * //server/foo/../bar  -->   //server/bar
+	 * //server/../bar      -->   null
+	 * C:\foo\..\bar        -->   C:\bar
+	 * C:\..\bar            -->   null
+	 * ~/foo/../bar/        -->   ~/bar
+	 * ~/../bar             -->   null
+	 * 
+ * + * @param filename + * the filename to normalize, null returns null + * @param unixSeparator + * {@code true} if a unix separator should be used or {@code false} if a windows separtor should be used. + * @return the normalized filename, or null if invalid + * @since 2.0 + */ + public static String normalizeNoEndSeparator(String filename, boolean unixSeparator) { + char separator = unixSeparator ? UNIX_SEPARATOR : WINDOWS_SEPARATOR; + return doNormalize(filename, separator, false); + } + + /** + * Internal method to perform the normalization. + * + * @param filename + * the filename + * @param separator + * The separator character to use + * @param keepSeparator + * true to keep the final separator + * @return the normalized filename + */ + private static String doNormalize(String filename, char separator, boolean keepSeparator) { + if (filename == null) { + return null; + } + int size = filename.length(); + if (size == 0) { + return filename; + } + int prefix = getPrefixLength(filename); + if (prefix < 0) { + return null; + } + + char[] array = new char[size + 2]; // +1 for possible extra slash, +2 for arraycopy + filename.getChars(0, filename.length(), array, 0); + + // fix separators throughout + char otherSeparator = separator == SYSTEM_SEPARATOR ? OTHER_SEPARATOR : SYSTEM_SEPARATOR; + for (int i = 0; i < array.length; i++) { + if (array[i] == otherSeparator) { + array[i] = separator; + } + } + + // add extra separator on the end to simplify code below + boolean lastIsDirectory = true; + if (array[size - 1] != separator) { + array[size++] = separator; + lastIsDirectory = false; + } + + // adjoining slashes + for (int i = prefix + 1; i < size; i++) { + if (array[i] == separator && array[i - 1] == separator) { + System.arraycopy(array, i, array, i - 1, size - i); + size--; + i--; + } + } + + // dot slash + for (int i = prefix + 1; i < size; i++) { + if (array[i] == separator && array[i - 1] == '.' && (i == prefix + 1 || array[i - 2] == separator)) { + if (i == size - 1) { + lastIsDirectory = true; + } + System.arraycopy(array, i + 1, array, i - 1, size - i); + size -= 2; + i--; + } + } + + // double dot slash + outer: for (int i = prefix + 2; i < size; i++) { + if (array[i] == separator && array[i - 1] == '.' && array[i - 2] == '.' && (i == prefix + 2 || array[i - 3] == separator)) { + if (i == prefix + 2) { + return null; + } + if (i == size - 1) { + lastIsDirectory = true; + } + int j; + for (j = i - 4; j >= prefix; j--) { + if (array[j] == separator) { + // remove b/../ from a/b/../c + System.arraycopy(array, i + 1, array, j + 1, size - i); + size -= i - j; + i = j + 1; + continue outer; + } + } + // remove a/../ from a/../c + System.arraycopy(array, i + 1, array, prefix, size - i); + size -= i + 1 - prefix; + i = prefix + 1; + } + } + + if (size <= 0) { // should never be less than 0 + return ""; + } + if (size <= prefix) { // should never be less than prefix + return new String(array, 0, size); + } + if (lastIsDirectory && keepSeparator) { + return new String(array, 0, size); // keep trailing separator + } + return new String(array, 0, size - 1); // lose trailing separator + } + + // ----------------------------------------------------------------------- + /** + * Concatenates a filename to a base path using normal command line style rules. + *

+ * The effect is equivalent to resultant directory after changing directory to the first argument, followed by changing directory to the second argument. + *

+ * The first argument is the base path, the second is the path to concatenate. The returned path is always normalized via {@link #normalize(String)}, thus .. is handled. + *

+ * If pathToAdd is absolute (has an absolute prefix), then it will be normalized and returned. Otherwise, the paths will be joined, normalized and returned. + *

+ * The output will be the same on both Unix and Windows except for the separator character. + * + *

+	 * /foo/ + bar          -->   /foo/bar
+	 * /foo + bar           -->   /foo/bar
+	 * /foo + /bar          -->   /bar
+	 * /foo + C:/bar        -->   C:/bar
+	 * /foo + C:bar         -->   C:bar (*)
+	 * /foo/a/ + ../bar     -->   foo/bar
+	 * /foo/ + ../../bar    -->   null
+	 * /foo/ + /bar         -->   /bar
+	 * /foo/.. + /bar       -->   /bar
+	 * /foo + bar/c.txt     -->   /foo/bar/c.txt
+	 * /foo/c.txt + bar     -->   /foo/c.txt/bar (!)
+	 * 
+ * + * (*) Note that the Windows relative drive prefix is unreliable when used with this method. (!) Note that the first parameter must be a path. If it ends with a name, then the name will be built into the concatenated path. If this might be a problem, use {@link #getFullPath(String)} on the base path argument. + * + * @param basePath + * the base path to attach to, always treated as a path + * @param fullFilenameToAdd + * the filename (or path) to attach to the base + * @return the concatenated path, or null if invalid + */ + public static String concat(String basePath, String fullFilenameToAdd) { + int prefix = getPrefixLength(fullFilenameToAdd); + if (prefix < 0) { + return null; + } + if (prefix > 0) { + return normalize(fullFilenameToAdd); + } + if (basePath == null) { + return null; + } + int len = basePath.length(); + if (len == 0) { + return normalize(fullFilenameToAdd); + } + char ch = basePath.charAt(len - 1); + if (isSeparator(ch)) { + return normalize(basePath + fullFilenameToAdd); + } else { + return normalize(basePath + '/' + fullFilenameToAdd); + } + } + + /** + * Determines whether the {@code parent} directory contains the {@code child} element (a file or directory). + *

+ * The files names are expected to be normalized. + *

+ * + * Edge cases: + *
    + *
  • A {@code directory} must not be null: if null, throw IllegalArgumentException
  • + *
  • A directory does not contain itself: return false
  • + *
  • A null child file is not contained in any parent: return false
  • + *
+ * + * @param canonicalParent + * the file to consider as the parent. + * @param canonicalChild + * the file to consider as the child. + * @return true is the candidate leaf is under by the specified composite. False otherwise. + * @throws IOException + * if an IO error occurs while checking the files. + * @since 2.2 + * @see FileUtils#directoryContains(File, File) + */ + public static boolean directoryContains(final String canonicalParent, final String canonicalChild) throws IOException { + + // Fail fast against NullPointerException + if (canonicalParent == null) { + throw new IllegalArgumentException("Directory must not be null"); + } + + if (canonicalChild == null) { + return false; + } + + if (IOCase.SYSTEM.checkEquals(canonicalParent, canonicalChild)) { + return false; + } + + return IOCase.SYSTEM.checkStartsWith(canonicalChild, canonicalParent); + } + + // ----------------------------------------------------------------------- + /** + * Converts all separators to the Unix separator of forward slash. + * + * @param path + * the path to be changed, null ignored + * @return the updated path + */ + public static String separatorsToUnix(String path) { + if (path == null || path.indexOf(WINDOWS_SEPARATOR) == -1) { + return path; + } + return path.replace(WINDOWS_SEPARATOR, UNIX_SEPARATOR); + } + + /** + * Converts all separators to the Windows separator of backslash. + * + * @param path + * the path to be changed, null ignored + * @return the updated path + */ + public static String separatorsToWindows(String path) { + if (path == null || path.indexOf(UNIX_SEPARATOR) == -1) { + return path; + } + return path.replace(UNIX_SEPARATOR, WINDOWS_SEPARATOR); + } + + /** + * Converts all separators to the system separator. + * + * @param path + * the path to be changed, null ignored + * @return the updated path + */ + public static String separatorsToSystem(String path) { + if (path == null) { + return null; + } + if (isSystemWindows()) { + return separatorsToWindows(path); + } else { + return separatorsToUnix(path); + } + } + + // ----------------------------------------------------------------------- + /** + * Returns the length of the filename prefix, such as C:/ or ~/. + *

+ * This method will handle a file in either Unix or Windows format. + *

+ * The prefix length includes the first slash in the full filename if applicable. Thus, it is possible that the length returned is greater than the length of the input string. + * + *

+	 * Windows:
+	 * a\b\c.txt           --> ""          --> relative
+	 * \a\b\c.txt          --> "\"         --> current drive absolute
+	 * C:a\b\c.txt         --> "C:"        --> drive relative
+	 * C:\a\b\c.txt        --> "C:\"       --> absolute
+	 * \\server\a\b\c.txt  --> "\\server\" --> UNC
+	 * 
+	 * Unix:
+	 * a/b/c.txt           --> ""          --> relative
+	 * /a/b/c.txt          --> "/"         --> absolute
+	 * ~/a/b/c.txt         --> "~/"        --> current user
+	 * ~                   --> "~/"        --> current user (slash added)
+	 * ~user/a/b/c.txt     --> "~user/"    --> named user
+	 * ~user               --> "~user/"    --> named user (slash added)
+	 * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. ie. both Unix and Windows prefixes are matched regardless. + * + * @param filename + * the filename to find the prefix in, null returns -1 + * @return the length of the prefix, -1 if invalid or null + */ + public static int getPrefixLength(String filename) { + if (filename == null) { + return -1; + } + int len = filename.length(); + if (len == 0) { + return 0; + } + char ch0 = filename.charAt(0); + if (ch0 == ':') { + return -1; + } + if (len == 1) { + if (ch0 == '~') { + return 2; // return a length greater than the input + } + return isSeparator(ch0) ? 1 : 0; + } else { + if (ch0 == '~') { + int posUnix = filename.indexOf(UNIX_SEPARATOR, 1); + int posWin = filename.indexOf(WINDOWS_SEPARATOR, 1); + if (posUnix == -1 && posWin == -1) { + return len + 1; // return a length greater than the input + } + posUnix = posUnix == -1 ? posWin : posUnix; + posWin = posWin == -1 ? posUnix : posWin; + return Math.min(posUnix, posWin) + 1; + } + char ch1 = filename.charAt(1); + if (ch1 == ':') { + ch0 = Character.toUpperCase(ch0); + if (ch0 >= 'A' && ch0 <= 'Z') { + if (len == 2 || isSeparator(filename.charAt(2)) == false) { + return 2; + } + return 3; + } + return -1; + + } else if (isSeparator(ch0) && isSeparator(ch1)) { + int posUnix = filename.indexOf(UNIX_SEPARATOR, 2); + int posWin = filename.indexOf(WINDOWS_SEPARATOR, 2); + if (posUnix == -1 && posWin == -1 || posUnix == 2 || posWin == 2) { + return -1; + } + posUnix = posUnix == -1 ? posWin : posUnix; + posWin = posWin == -1 ? posUnix : posWin; + return Math.min(posUnix, posWin) + 1; + } else { + return isSeparator(ch0) ? 1 : 0; + } + } + } + + /** + * Returns the index of the last directory separator character. + *

+ * This method will handle a file in either Unix or Windows format. The position of the last forward or backslash is returned. + *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename + * the filename to find the last path separator in, null returns -1 + * @return the index of the last separator character, or -1 if there is no such character + */ + public static int indexOfLastSeparator(String filename) { + if (filename == null) { + return -1; + } + int lastUnixPos = filename.lastIndexOf(UNIX_SEPARATOR); + int lastWindowsPos = filename.lastIndexOf(WINDOWS_SEPARATOR); + return Math.max(lastUnixPos, lastWindowsPos); + } + + /** + * Returns the index of the last extension separator character, which is a dot. + *

+ * This method also checks that there is no directory separator after the last dot. To do this it uses {@link #indexOfLastSeparator(String)} which will handle a file in either Unix or Windows format. + *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename + * the filename to find the last path separator in, null returns -1 + * @return the index of the last separator character, or -1 if there is no such character + */ + public static int indexOfExtension(String filename) { + if (filename == null) { + return -1; + } + int extensionPos = filename.lastIndexOf(EXTENSION_SEPARATOR); + int lastSeparator = indexOfLastSeparator(filename); + return lastSeparator > extensionPos ? -1 : extensionPos; + } + + // ----------------------------------------------------------------------- + /** + * Gets the prefix from a full filename, such as C:/ or ~/. + *

+ * This method will handle a file in either Unix or Windows format. The prefix includes the first slash in the full filename where applicable. + * + *

+	 * Windows:
+	 * a\b\c.txt           --> ""          --> relative
+	 * \a\b\c.txt          --> "\"         --> current drive absolute
+	 * C:a\b\c.txt         --> "C:"        --> drive relative
+	 * C:\a\b\c.txt        --> "C:\"       --> absolute
+	 * \\server\a\b\c.txt  --> "\\server\" --> UNC
+	 * 
+	 * Unix:
+	 * a/b/c.txt           --> ""          --> relative
+	 * /a/b/c.txt          --> "/"         --> absolute
+	 * ~/a/b/c.txt         --> "~/"        --> current user
+	 * ~                   --> "~/"        --> current user (slash added)
+	 * ~user/a/b/c.txt     --> "~user/"    --> named user
+	 * ~user               --> "~user/"    --> named user (slash added)
+	 * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. ie. both Unix and Windows prefixes are matched regardless. + * + * @param filename + * the filename to query, null returns null + * @return the prefix of the file, null if invalid + */ + public static String getPrefix(String filename) { + if (filename == null) { + return null; + } + int len = getPrefixLength(filename); + if (len < 0) { + return null; + } + if (len > filename.length()) { + return filename + UNIX_SEPARATOR; // we know this only happens for unix + } + return filename.substring(0, len); + } + + /** + * Gets the path from a full filename, which excludes the prefix. + *

+ * This method will handle a file in either Unix or Windows format. The method is entirely text based, and returns the text before and including the last forward or backslash. + * + *

+	 * C:\a\b\c.txt --> a\b\
+	 * ~/a/b/c.txt  --> a/b/
+	 * a.txt        --> ""
+	 * a/b/c        --> a/b/
+	 * a/b/c/       --> a/b/c/
+	 * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + *

+ * This method drops the prefix from the result. See {@link #getFullPath(String)} for the method that retains the prefix. + * + * @param filename + * the filename to query, null returns null + * @return the path of the file, an empty string if none exists, null if invalid + */ + public static String getPath(String filename) { + return doGetPath(filename, 1); + } + + /** + * Gets the path from a full filename, which excludes the prefix, and also excluding the final directory separator. + *

+ * This method will handle a file in either Unix or Windows format. The method is entirely text based, and returns the text before the last forward or backslash. + * + *

+	 * C:\a\b\c.txt --> a\b
+	 * ~/a/b/c.txt  --> a/b
+	 * a.txt        --> ""
+	 * a/b/c        --> a/b
+	 * a/b/c/       --> a/b/c
+	 * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + *

+ * This method drops the prefix from the result. See {@link #getFullPathNoEndSeparator(String)} for the method that retains the prefix. + * + * @param filename + * the filename to query, null returns null + * @return the path of the file, an empty string if none exists, null if invalid + */ + public static String getPathNoEndSeparator(String filename) { + return doGetPath(filename, 0); + } + + /** + * Does the work of getting the path. + * + * @param filename + * the filename + * @param separatorAdd + * 0 to omit the end separator, 1 to return it + * @return the path + */ + private static String doGetPath(String filename, int separatorAdd) { + if (filename == null) { + return null; + } + int prefix = getPrefixLength(filename); + if (prefix < 0) { + return null; + } + int index = indexOfLastSeparator(filename); + int endIndex = index + separatorAdd; + if (prefix >= filename.length() || index < 0 || prefix >= endIndex) { + return ""; + } + return filename.substring(prefix, endIndex); + } + + /** + * Gets the full path from a full filename, which is the prefix + path. + *

+ * This method will handle a file in either Unix or Windows format. The method is entirely text based, and returns the text before and including the last forward or backslash. + * + *

+	 * C:\a\b\c.txt --> C:\a\b\
+	 * ~/a/b/c.txt  --> ~/a/b/
+	 * a.txt        --> ""
+	 * a/b/c        --> a/b/
+	 * a/b/c/       --> a/b/c/
+	 * C:           --> C:
+	 * C:\          --> C:\
+	 * ~            --> ~/
+	 * ~/           --> ~/
+	 * ~user        --> ~user/
+	 * ~user/       --> ~user/
+	 * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename + * the filename to query, null returns null + * @return the path of the file, an empty string if none exists, null if invalid + */ + public static String getFullPath(String filename) { + return doGetFullPath(filename, true); + } + + /** + * Gets the full path from a full filename, which is the prefix + path, and also excluding the final directory separator. + *

+ * This method will handle a file in either Unix or Windows format. The method is entirely text based, and returns the text before the last forward or backslash. + * + *

+	 * C:\a\b\c.txt --> C:\a\b
+	 * ~/a/b/c.txt  --> ~/a/b
+	 * a.txt        --> ""
+	 * a/b/c        --> a/b
+	 * a/b/c/       --> a/b/c
+	 * C:           --> C:
+	 * C:\          --> C:\
+	 * ~            --> ~
+	 * ~/           --> ~
+	 * ~user        --> ~user
+	 * ~user/       --> ~user
+	 * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename + * the filename to query, null returns null + * @return the path of the file, an empty string if none exists, null if invalid + */ + public static String getFullPathNoEndSeparator(String filename) { + return doGetFullPath(filename, false); + } + + /** + * Does the work of getting the path. + * + * @param filename + * the filename + * @param includeSeparator + * true to include the end separator + * @return the path + */ + private static String doGetFullPath(String filename, boolean includeSeparator) { + if (filename == null) { + return null; + } + int prefix = getPrefixLength(filename); + if (prefix < 0) { + return null; + } + if (prefix >= filename.length()) { + if (includeSeparator) { + return getPrefix(filename); // add end slash if necessary + } else { + return filename; + } + } + int index = indexOfLastSeparator(filename); + if (index < 0) { + return filename.substring(0, prefix); + } + int end = index + (includeSeparator ? 1 : 0); + if (end == 0) { + end++; + } + return filename.substring(0, end); + } + + /** + * Gets the name minus the path from a full filename. + *

+ * This method will handle a file in either Unix or Windows format. The text after the last forward or backslash is returned. + * + *

+	 * a/b/c.txt --> c.txt
+	 * a.txt     --> a.txt
+	 * a/b/c     --> c
+	 * a/b/c/    --> ""
+	 * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename + * the filename to query, null returns null + * @return the name of the file without the path, or an empty string if none exists + */ + public static String getName(String filename) { + if (filename == null) { + return null; + } + int index = indexOfLastSeparator(filename); + return filename.substring(index + 1); + } + + /** + * Gets the base name, minus the full path and extension, from a full filename. + *

+ * This method will handle a file in either Unix or Windows format. The text after the last forward or backslash and before the last dot is returned. + * + *

+	 * a/b/c.txt --> c
+	 * a.txt     --> a
+	 * a/b/c     --> c
+	 * a/b/c/    --> ""
+	 * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename + * the filename to query, null returns null + * @return the name of the file without the path, or an empty string if none exists + */ + public static String getBaseName(String filename) { + return removeExtension(getName(filename)); + } + + /** + * Gets the extension of a filename. + *

+ * This method returns the textual part of the filename after the last dot. There must be no directory separator after the dot. + * + *

+	 * foo.txt      --> "txt"
+	 * a/b/c.jpg    --> "jpg"
+	 * a/b.txt/c    --> ""
+	 * a/b/c        --> ""
+	 * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename + * the filename to retrieve the extension of. + * @return the extension of the file or an empty string if none exists or {@code null} if the filename is {@code null}. + */ + public static String getExtension(String filename) { + if (filename == null) { + return null; + } + int index = indexOfExtension(filename); + if (index == -1) { + return ""; + } else { + return filename.substring(index + 1); + } + } + + // ----------------------------------------------------------------------- + /** + * Removes the extension from a filename. + *

+ * This method returns the textual part of the filename before the last dot. There must be no directory separator after the dot. + * + *

+	 * foo.txt    --> foo
+	 * a\b\c.jpg  --> a\b\c
+	 * a\b\c      --> a\b\c
+	 * a.b\c      --> a.b\c
+	 * 
+ *

+ * The output will be the same irrespective of the machine that the code is running on. + * + * @param filename + * the filename to query, null returns null + * @return the filename minus the extension + */ + public static String removeExtension(String filename) { + if (filename == null) { + return null; + } + int index = indexOfExtension(filename); + if (index == -1) { + return filename; + } else { + return filename.substring(0, index); + } + } + + // ----------------------------------------------------------------------- + /** + * Checks whether two filenames are equal exactly. + *

+ * No processing is performed on the filenames other than comparison, thus this is merely a null-safe case-sensitive equals. + * + * @param filename1 + * the first filename to query, may be null + * @param filename2 + * the second filename to query, may be null + * @return true if the filenames are equal, null equals null + * @see IOCase#SENSITIVE + */ + public static boolean equals(String filename1, String filename2) { + return equals(filename1, filename2, false, IOCase.SENSITIVE); + } + + /** + * Checks whether two filenames are equal using the case rules of the system. + *

+ * No processing is performed on the filenames other than comparison. The check is case-sensitive on Unix and case-insensitive on Windows. + * + * @param filename1 + * the first filename to query, may be null + * @param filename2 + * the second filename to query, may be null + * @return true if the filenames are equal, null equals null + * @see IOCase#SYSTEM + */ + public static boolean equalsOnSystem(String filename1, String filename2) { + return equals(filename1, filename2, false, IOCase.SYSTEM); + } + + // ----------------------------------------------------------------------- + /** + * Checks whether two filenames are equal after both have been normalized. + *

+ * Both filenames are first passed to {@link #normalize(String)}. The check is then performed in a case-sensitive manner. + * + * @param filename1 + * the first filename to query, may be null + * @param filename2 + * the second filename to query, may be null + * @return true if the filenames are equal, null equals null + * @see IOCase#SENSITIVE + */ + public static boolean equalsNormalized(String filename1, String filename2) { + return equals(filename1, filename2, true, IOCase.SENSITIVE); + } + + /** + * Checks whether two filenames are equal after both have been normalized and using the case rules of the system. + *

+ * Both filenames are first passed to {@link #normalize(String)}. The check is then performed case-sensitive on Unix and case-insensitive on Windows. + * + * @param filename1 + * the first filename to query, may be null + * @param filename2 + * the second filename to query, may be null + * @return true if the filenames are equal, null equals null + * @see IOCase#SYSTEM + */ + public static boolean equalsNormalizedOnSystem(String filename1, String filename2) { + return equals(filename1, filename2, true, IOCase.SYSTEM); + } + + /** + * Checks whether two filenames are equal, optionally normalizing and providing control over the case-sensitivity. + * + * @param filename1 + * the first filename to query, may be null + * @param filename2 + * the second filename to query, may be null + * @param normalized + * whether to normalize the filenames + * @param caseSensitivity + * what case sensitivity rule to use, null means case-sensitive + * @return true if the filenames are equal, null equals null + * @since 1.3 + */ + public static boolean equals(String filename1, String filename2, boolean normalized, IOCase caseSensitivity) { + + if (filename1 == null || filename2 == null) { + return filename1 == null && filename2 == null; + } + if (normalized) { + filename1 = normalize(filename1); + filename2 = normalize(filename2); + if (filename1 == null || filename2 == null) { + throw new NullPointerException("Error normalizing one or both of the file names"); + } + } + if (caseSensitivity == null) { + caseSensitivity = IOCase.SENSITIVE; + } + return caseSensitivity.checkEquals(filename1, filename2); + } + + // ----------------------------------------------------------------------- + /** + * Checks whether the extension of the filename is that specified. + *

+ * This method obtains the extension as the textual part of the filename after the last dot. There must be no directory separator after the dot. The extension check is case-sensitive on all platforms. + * + * @param filename + * the filename to query, null returns false + * @param extension + * the extension to check for, null or empty checks for no extension + * @return true if the filename has the specified extension + */ + public static boolean isExtension(String filename, String extension) { + if (filename == null) { + return false; + } + if (extension == null || extension.length() == 0) { + return indexOfExtension(filename) == -1; + } + String fileExt = getExtension(filename); + return fileExt.equals(extension); + } + + /** + * Checks whether the extension of the filename is one of those specified. + *

+ * This method obtains the extension as the textual part of the filename after the last dot. There must be no directory separator after the dot. The extension check is case-sensitive on all platforms. + * + * @param filename + * the filename to query, null returns false + * @param extensions + * the extensions to check for, null checks for no extension + * @return true if the filename is one of the extensions + */ + public static boolean isExtension(String filename, String[] extensions) { + if (filename == null) { + return false; + } + if (extensions == null || extensions.length == 0) { + return indexOfExtension(filename) == -1; + } + String fileExt = getExtension(filename); + for (String extension : extensions) { + if (fileExt.equals(extension)) { + return true; + } + } + return false; + } + + /** + * Checks whether the extension of the filename is one of those specified. + *

+ * This method obtains the extension as the textual part of the filename after the last dot. There must be no directory separator after the dot. The extension check is case-sensitive on all platforms. + * + * @param filename + * the filename to query, null returns false + * @param extensions + * the extensions to check for, null checks for no extension + * @return true if the filename is one of the extensions + */ + public static boolean isExtension(String filename, Collection extensions) { + if (filename == null) { + return false; + } + if (extensions == null || extensions.isEmpty()) { + return indexOfExtension(filename) == -1; + } + String fileExt = getExtension(filename); + for (String extension : extensions) { + if (fileExt.equals(extension)) { + return true; + } + } + return false; + } + + // ----------------------------------------------------------------------- + /** + * Checks a filename to see if it matches the specified wildcard matcher, always testing case-sensitive. + *

+ * The wildcard matcher uses the characters '?' and '*' to represent a single or multiple (zero or more) wildcard characters. This is the same as often found on Dos/Unix command lines. The check is case-sensitive always. + * + *

+	 * wildcardMatch("c.txt", "*.txt")      --> true
+	 * wildcardMatch("c.txt", "*.jpg")      --> false
+	 * wildcardMatch("a/b/c.txt", "a/b/*")  --> true
+	 * wildcardMatch("c.txt", "*.???")      --> true
+	 * wildcardMatch("c.txt", "*.????")     --> false
+	 * 
+ * + * N.B. the sequence "*?" does not work properly at present in match strings. + * + * @param filename + * the filename to match on + * @param wildcardMatcher + * the wildcard string to match against + * @return true if the filename matches the wilcard string + * @see IOCase#SENSITIVE + */ + public static boolean wildcardMatch(String filename, String wildcardMatcher) { + return wildcardMatch(filename, wildcardMatcher, IOCase.SENSITIVE); + } + + /** + * Checks a filename to see if it matches the specified wildcard matcher using the case rules of the system. + *

+ * The wildcard matcher uses the characters '?' and '*' to represent a single or multiple (zero or more) wildcard characters. This is the same as often found on Dos/Unix command lines. The check is case-sensitive on Unix and case-insensitive on Windows. + * + *

+	 * wildcardMatch("c.txt", "*.txt")      --> true
+	 * wildcardMatch("c.txt", "*.jpg")      --> false
+	 * wildcardMatch("a/b/c.txt", "a/b/*")  --> true
+	 * wildcardMatch("c.txt", "*.???")      --> true
+	 * wildcardMatch("c.txt", "*.????")     --> false
+	 * 
+ * + * N.B. the sequence "*?" does not work properly at present in match strings. + * + * @param filename + * the filename to match on + * @param wildcardMatcher + * the wildcard string to match against + * @return true if the filename matches the wilcard string + * @see IOCase#SYSTEM + */ + public static boolean wildcardMatchOnSystem(String filename, String wildcardMatcher) { + return wildcardMatch(filename, wildcardMatcher, IOCase.SYSTEM); + } + + /** + * Checks a filename to see if it matches the specified wildcard matcher allowing control over case-sensitivity. + *

+ * The wildcard matcher uses the characters '?' and '*' to represent a single or multiple (zero or more) wildcard characters. N.B. the sequence "*?" does not work properly at present in match strings. + * + * @param filename + * the filename to match on + * @param wildcardMatcher + * the wildcard string to match against + * @param caseSensitivity + * what case sensitivity rule to use, null means case-sensitive + * @return true if the filename matches the wilcard string + * @since 1.3 + */ + public static boolean wildcardMatch(String filename, String wildcardMatcher, IOCase caseSensitivity) { + if (filename == null && wildcardMatcher == null) { + return true; + } + if (filename == null || wildcardMatcher == null) { + return false; + } + if (caseSensitivity == null) { + caseSensitivity = IOCase.SENSITIVE; + } + String[] wcs = splitOnTokens(wildcardMatcher); + boolean anyChars = false; + int textIdx = 0; + int wcsIdx = 0; + Stack backtrack = new Stack(); + + // loop around a backtrack stack, to handle complex * matching + do { + if (backtrack.size() > 0) { + int[] array = backtrack.pop(); + wcsIdx = array[0]; + textIdx = array[1]; + anyChars = true; + } + + // loop whilst tokens and text left to process + while (wcsIdx < wcs.length) { + + if (wcs[wcsIdx].equals("?")) { + // ? so move to next text char + textIdx++; + if (textIdx > filename.length()) { + break; + } + anyChars = false; + + } else if (wcs[wcsIdx].equals("*")) { + // set any chars status + anyChars = true; + if (wcsIdx == wcs.length - 1) { + textIdx = filename.length(); + } + + } else { + // matching text token + if (anyChars) { + // any chars then try to locate text token + textIdx = caseSensitivity.checkIndexOf(filename, textIdx, wcs[wcsIdx]); + if (textIdx == -1) { + // token not found + break; + } + int repeat = caseSensitivity.checkIndexOf(filename, textIdx + 1, wcs[wcsIdx]); + if (repeat >= 0) { + backtrack.push(new int[] { wcsIdx, repeat }); + } + } else { + // matching from current position + if (!caseSensitivity.checkRegionMatches(filename, textIdx, wcs[wcsIdx])) { + // couldnt match token + break; + } + } + + // matched text token, move text index to end of matched token + textIdx += wcs[wcsIdx].length(); + anyChars = false; + } + + wcsIdx++; + } + + // full match + if (wcsIdx == wcs.length && textIdx == filename.length()) { + return true; + } + + } while (backtrack.size() > 0); + + return false; + } + + /** + * Splits a string into a number of tokens. The text is split by '?' and '*'. Where multiple '*' occur consecutively they are collapsed into a single '*'. + * + * @param text + * the text to split + * @return the array of tokens, never null + */ + static String[] splitOnTokens(String text) { + // used by wildcardMatch + // package level so a unit test may run on this + + if (text.indexOf('?') == -1 && text.indexOf('*') == -1) { + return new String[] { text }; + } + + char[] array = text.toCharArray(); + ArrayList list = new ArrayList(); + StringBuilder buffer = new StringBuilder(); + for (int i = 0; i < array.length; i++) { + if (array[i] == '?' || array[i] == '*') { + if (buffer.length() != 0) { + list.add(buffer.toString()); + buffer.setLength(0); + } + if (array[i] == '?') { + list.add("?"); + } else if (list.isEmpty() || i > 0 && list.get(list.size() - 1).equals("*") == false) { + list.add("*"); + } + } else { + buffer.append(array[i]); + } + } + if (buffer.length() != 0) { + list.add(buffer.toString()); + } + + return list.toArray(new String[list.size()]); + } + +} diff --git a/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/GenericRScript.java b/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/GenericRScript.java index 802e943..8f15412 100644 --- a/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/GenericRScript.java +++ b/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/GenericRScript.java @@ -210,7 +210,7 @@ public abstract class GenericRScript extends StandardLocalExternalAlgorithm { String preparedFile = new File(config.getConfigPath(), rowFile.getName()).getAbsolutePath(); AnalysisLogger.getLogger().debug("Copying " + rowFile.getAbsolutePath() + " to " + preparedFile); - FileUtils.moveFileToDirectory(rowFile, new File(config.getConfigPath()), false); + org.gcube.dataanalysis.executor.rscripts.generic.FileUtils.moveFileToDirectory(rowFile, new File(config.getConfigPath()), false); files.add(preparedFile); break; diff --git a/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/IOUtils.java b/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/IOUtils.java new file mode 100644 index 0000000..e27e7eb --- /dev/null +++ b/src/main/java/org/gcube/dataanalysis/executor/rscripts/generic/IOUtils.java @@ -0,0 +1,2547 @@ +package org.gcube.dataanalysis.executor.rscripts.generic; + + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.CharArrayWriter; +import java.io.Closeable; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.nio.channels.Selector; +import java.nio.charset.Charset; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.apache.commons.codec.Charsets; +import org.apache.commons.io.LineIterator; +import org.apache.commons.io.output.ByteArrayOutputStream; +import org.apache.commons.io.output.StringBuilderWriter; + +/** + * General IO stream manipulation utilities. + *

+ * This class provides static utility methods for input/output operations. + *

    + *
  • closeQuietly - these methods close a stream ignoring nulls and exceptions + *
  • toXxx/read - these methods read data from a stream + *
  • write - these methods write data to a stream + *
  • copy - these methods copy all the data from one stream to another + *
  • contentEquals - these methods compare the content of two streams + *
+ *

+ * The byte-to-char methods and char-to-byte methods involve a conversion step. + * Two methods are provided in each case, one that uses the platform default + * encoding and the other which allows you to specify an encoding. You are + * encouraged to always specify an encoding because relying on the platform + * default can lead to unexpected results, for example when moving from + * development to production. + *

+ * All the methods in this class that read a stream are buffered internally. + * This means that there is no cause to use a BufferedInputStream + * or BufferedReader. The default buffer size of 4K has been shown + * to be efficient in tests. + *

+ * Wherever possible, the methods in this class do not flush or close + * the stream. This is to avoid making non-portable assumptions about the + * streams' origin and further use. Thus the caller is still responsible for + * closing streams after use. + *

+ * Origin of code: Excalibur. + * + * @version $Id: IOUtils.java 1326636 2012-04-16 14:54:53Z ggregory $ + */ +public class IOUtils { + // NOTE: This class is focussed on InputStream, OutputStream, Reader and + // Writer. Each method should take at least one of these as a parameter, + // or return one of them. + + private static final int EOF = -1; + /** + * The Unix directory separator character. + */ + public static final char DIR_SEPARATOR_UNIX = '/'; + /** + * The Windows directory separator character. + */ + public static final char DIR_SEPARATOR_WINDOWS = '\\'; + /** + * The system directory separator character. + */ + public static final char DIR_SEPARATOR = File.separatorChar; + /** + * The Unix line separator string. + */ + public static final String LINE_SEPARATOR_UNIX = "\n"; + /** + * The Windows line separator string. + */ + public static final String LINE_SEPARATOR_WINDOWS = "\r\n"; + /** + * The system line separator string. + */ + public static final String LINE_SEPARATOR; + + static { + // avoid security issues + StringBuilderWriter buf = new StringBuilderWriter(4); + PrintWriter out = new PrintWriter(buf); + out.println(); + LINE_SEPARATOR = buf.toString(); + out.close(); + } + + /** + * The default buffer size ({@value}) to use for + * {@link #copyLarge(InputStream, OutputStream)} + * and + * {@link #copyLarge(Reader, Writer)} + */ + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /** + * The default buffer size to use for the skip() methods. + */ + private static final int SKIP_BUFFER_SIZE = 2048; + + // Allocated in the relevant skip method if necessary. + /* + * N.B. no need to synchronize these because: + * - we don't care if the buffer is created multiple times (the data is ignored) + * - we always use the same size buffer, so if it it is recreated it will still be OK + * (if the buffer size were variable, we would need to synch. to ensure some other thread + * did not create a smaller one) + */ + private static char[] SKIP_CHAR_BUFFER; + private static byte[] SKIP_BYTE_BUFFER; + + /** + * Instances should NOT be constructed in standard programming. + */ + public IOUtils() { + super(); + } + + //----------------------------------------------------------------------- + + /** + * Closes a URLConnection. + * + * @param conn the connection to close. + * @since 2.4 + */ + public static void close(URLConnection conn) { + if (conn instanceof HttpURLConnection) { + ((HttpURLConnection) conn).disconnect(); + } + } + + /** + * Unconditionally close an Reader. + *

+ * Equivalent to {@link Reader#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+     *   char[] data = new char[1024];
+     *   Reader in = null;
+     *   try {
+     *       in = new FileReader("foo.txt");
+     *       in.read(data);
+     *       in.close(); //close errors are handled
+     *   } catch (Exception e) {
+     *       // error handling
+     *   } finally {
+     *       IOUtils.closeQuietly(in);
+     *   }
+     * 
+ * + * @param input the Reader to close, may be null or already closed + */ + public static void closeQuietly(Reader input) { + closeQuietly((Closeable)input); + } + + /** + * Unconditionally close a Writer. + *

+ * Equivalent to {@link Writer#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+     *   Writer out = null;
+     *   try {
+     *       out = new StringWriter();
+     *       out.write("Hello World");
+     *       out.close(); //close errors are handled
+     *   } catch (Exception e) {
+     *       // error handling
+     *   } finally {
+     *       IOUtils.closeQuietly(out);
+     *   }
+     * 
+ * + * @param output the Writer to close, may be null or already closed + */ + public static void closeQuietly(Writer output) { + closeQuietly((Closeable)output); + } + + /** + * Unconditionally close an InputStream. + *

+ * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+     *   byte[] data = new byte[1024];
+     *   InputStream in = null;
+     *   try {
+     *       in = new FileInputStream("foo.txt");
+     *       in.read(data);
+     *       in.close(); //close errors are handled
+     *   } catch (Exception e) {
+     *       // error handling
+     *   } finally {
+     *       IOUtils.closeQuietly(in);
+     *   }
+     * 
+ * + * @param input the InputStream to close, may be null or already closed + */ + public static void closeQuietly(InputStream input) { + closeQuietly((Closeable)input); + } + + /** + * Unconditionally close an OutputStream. + *

+ * Equivalent to {@link OutputStream#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+     * byte[] data = "Hello, World".getBytes();
+     *
+     * OutputStream out = null;
+     * try {
+     *     out = new FileOutputStream("foo.txt");
+     *     out.write(data);
+     *     out.close(); //close errors are handled
+     * } catch (IOException e) {
+     *     // error handling
+     * } finally {
+     *     IOUtils.closeQuietly(out);
+     * }
+     * 
+ * + * @param output the OutputStream to close, may be null or already closed + */ + public static void closeQuietly(OutputStream output) { + closeQuietly((Closeable)output); + } + + /** + * Unconditionally close a Closeable. + *

+ * Equivalent to {@link Closeable#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+     *   Closeable closeable = null;
+     *   try {
+     *       closeable = new FileReader("foo.txt");
+     *       // process closeable
+     *       closeable.close();
+     *   } catch (Exception e) {
+     *       // error handling
+     *   } finally {
+     *       IOUtils.closeQuietly(closeable);
+     *   }
+     * 
+ * + * @param closeable the object to close, may be null or already closed + * @since 2.0 + */ + public static void closeQuietly(Closeable closeable) { + try { + if (closeable != null) { + closeable.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + /** + * Unconditionally close a Socket. + *

+ * Equivalent to {@link Socket#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+     *   Socket socket = null;
+     *   try {
+     *       socket = new Socket("http://www.foo.com/", 80);
+     *       // process socket
+     *       socket.close();
+     *   } catch (Exception e) {
+     *       // error handling
+     *   } finally {
+     *       IOUtils.closeQuietly(socket);
+     *   }
+     * 
+ * + * @param sock the Socket to close, may be null or already closed + * @since 2.0 + */ + public static void closeQuietly(Socket sock){ + if (sock != null){ + try { + sock.close(); + } catch (IOException ioe) { + // ignored + } + } + } + + /** + * Unconditionally close a Selector. + *

+ * Equivalent to {@link Selector#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+     *   Selector selector = null;
+     *   try {
+     *       selector = Selector.open();
+     *       // process socket
+     *       
+     *   } catch (Exception e) {
+     *       // error handling
+     *   } finally {
+     *       IOUtils.closeQuietly(selector);
+     *   }
+     * 
+ * + * @param selector the Selector to close, may be null or already closed + * @since 2.2 + */ + public static void closeQuietly(Selector selector){ + if (selector != null){ + try { + selector.close(); + } catch (IOException ioe) { + // ignored + } + } + } + + /** + * Unconditionally close a ServerSocket. + *

+ * Equivalent to {@link ServerSocket#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + *

+ * Example code: + *

+     *   ServerSocket socket = null;
+     *   try {
+     *       socket = new ServerSocket();
+     *       // process socket
+     *       socket.close();
+     *   } catch (Exception e) {
+     *       // error handling
+     *   } finally {
+     *       IOUtils.closeQuietly(socket);
+     *   }
+     * 
+ * + * @param sock the ServerSocket to close, may be null or already closed + * @since 2.2 + */ + public static void closeQuietly(ServerSocket sock){ + if (sock != null){ + try { + sock.close(); + } catch (IOException ioe) { + // ignored + } + } + } + + /** + * Fetches entire contents of an InputStream and represent + * same data as result InputStream. + *

+ * This method is useful where, + *

    + *
  • Source InputStream is slow.
  • + *
  • It has network resources associated, so we cannot keep it open for + * long time.
  • + *
  • It has network timeout associated.
  • + *
+ * It can be used in favor of {@link #toByteArray(InputStream)}, since it + * avoids unnecessary allocation and copy of byte[].
+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input Stream to be fully buffered. + * @return A fully buffered stream. + * @throws IOException if an I/O error occurs + * @since 2.0 + */ + public static InputStream toBufferedInputStream(InputStream input) throws IOException { + return ByteArrayOutputStream.toBufferedInputStream(input); + } + + /** + * Returns the given reader if it is a {@link BufferedReader}, otherwise creates a toBufferedReader for the given + * reader. + * + * @param reader + * the reader to wrap or return + * @return the given reader or a new {@link BufferedReader} for the given reader + * @since 2.2 + */ + public static BufferedReader toBufferedReader(Reader reader) { + return reader instanceof BufferedReader ? (BufferedReader) reader : new BufferedReader(reader); + } + + // read toByteArray + //----------------------------------------------------------------------- + /** + * Get the contents of an InputStream as a byte[]. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + /** + * Get contents of an InputStream as a byte[]. + * Use this method instead of toByteArray(InputStream) + * when InputStream size is known. + * NOTE: the method checks that the length can safely be cast to an int without truncation + * before using {@link IOUtils#toByteArray(java.io.InputStream, int)} to read into the byte array. + * (Arrays can have no more than Integer.MAX_VALUE entries anyway) + * + * @param input the InputStream to read from + * @param size the size of InputStream + * @return the requested byte array + * @throws IOException if an I/O error occurs or InputStream size differ from parameter size + * @throws IllegalArgumentException if size is less than zero or size is greater than Integer.MAX_VALUE + * @see IOUtils#toByteArray(java.io.InputStream, int) + * @since 2.1 + */ + public static byte[] toByteArray(InputStream input, long size) throws IOException { + + if(size > Integer.MAX_VALUE) { + throw new IllegalArgumentException("Size cannot be greater than Integer max value: " + size); + } + + return toByteArray(input, (int) size); + } + + /** + * Get the contents of an InputStream as a byte[]. + * Use this method instead of toByteArray(InputStream) + * when InputStream size is known + * @param input the InputStream to read from + * @param size the size of InputStream + * @return the requested byte array + * @throws IOException if an I/O error occurs or InputStream size differ from parameter size + * @throws IllegalArgumentException if size is less than zero + * @since 2.1 + */ + public static byte[] toByteArray(InputStream input, int size) throws IOException { + + if (size < 0) { + throw new IllegalArgumentException("Size must be equal or greater than zero: " + size); + } + + if (size == 0) { + return new byte[0]; + } + + byte[] data = new byte[size]; + int offset = 0; + int readed; + + while (offset < size && (readed = input.read(data, offset, size - offset)) != EOF) { + offset += readed; + } + + if (offset != size) { + throw new IOException("Unexpected readed size. current: " + offset + ", excepted: " + size); + } + + return data; + } + + /** + * Get the contents of a Reader as a byte[] + * using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(Reader input) throws IOException { + return toByteArray(input, Charset.defaultCharset()); + } + + /** + * Get the contents of a Reader as a byte[] + * using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from + * @param encoding the encoding to use, null means platform default + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static byte[] toByteArray(Reader input, Charset encoding) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output, encoding); + return output.toByteArray(); + } + + /** + * Get the contents of a Reader as a byte[] + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from + * @param encoding the encoding to use, null means platform default + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + */ + public static byte[] toByteArray(Reader input, String encoding) throws IOException { + return toByteArray(input, Charsets.toCharset(encoding)); + } + + /** + * Get the contents of a String as a byte[] + * using the default character encoding of the platform. + *

+ * This is the same as {@link String#getBytes()}. + * + * @param input the String to convert + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs (never occurs) + * @deprecated Use {@link String#getBytes()} + */ + @Deprecated + public static byte[] toByteArray(String input) throws IOException { + return input.getBytes(); + } + + /** + * Get the contents of a URI as a byte[]. + * + * @param uri + * the URI to read + * @return the requested byte array + * @throws NullPointerException + * if the uri is null + * @throws IOException + * if an I/O exception occurs + * @since 2.4 + */ + public static byte[] toByteArray(URI uri) throws IOException { + return IOUtils.toByteArray(uri.toURL()); + } + + /** + * Get the contents of a URL as a byte[]. + * + * @param url + * the URL to read + * @return the requested byte array + * @throws NullPointerException + * if the input is null + * @throws IOException + * if an I/O exception occurs + * @since 2.4 + */ + public static byte[] toByteArray(URL url) throws IOException { + URLConnection conn = url.openConnection(); + try { + return IOUtils.toByteArray(conn); + } finally { + close(conn); + } + } + + /** + * Get the contents of a URLConnection as a byte[]. + * + * @param urlConn + * the URLConnection to read + * @return the requested byte array + * @throws NullPointerException + * if the urlConn is null + * @throws IOException + * if an I/O exception occurs + * @since 2.4 + */ + public static byte[] toByteArray(URLConnection urlConn) throws IOException { + InputStream inputStream = urlConn.getInputStream(); + try { + return IOUtils.toByteArray(inputStream); + } finally { + inputStream.close(); + } + } + + // read char[] + //----------------------------------------------------------------------- + /** + * Get the contents of an InputStream as a character array + * using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param is the InputStream to read from + * @return the requested character array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static char[] toCharArray(InputStream is) throws IOException { + return toCharArray(is, Charset.defaultCharset()); + } + + /** + * Get the contents of an InputStream as a character array + * using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param is the InputStream to read from + * @param encoding the encoding to use, null means platform default + * @return the requested character array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static char[] toCharArray(InputStream is, Charset encoding) + throws IOException { + CharArrayWriter output = new CharArrayWriter(); + copy(is, output, encoding); + return output.toCharArray(); + } + + /** + * Get the contents of an InputStream as a character array + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param is the InputStream to read from + * @param encoding the encoding to use, null means platform default + * @return the requested character array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + */ + public static char[] toCharArray(InputStream is, String encoding) throws IOException { + return toCharArray(is, Charsets.toCharset(encoding)); + } + + /** + * Get the contents of a Reader as a character array. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from + * @return the requested character array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static char[] toCharArray(Reader input) throws IOException { + CharArrayWriter sw = new CharArrayWriter(); + copy(input, sw); + return sw.toCharArray(); + } + + // read toString + //----------------------------------------------------------------------- + /** + * Get the contents of an InputStream as a String + * using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static String toString(InputStream input) throws IOException { + return toString(input, Charset.defaultCharset()); + } + + /** + * Get the contents of an InputStream as a String + * using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * @param input the InputStream to read from + * @param encoding the encoding to use, null means platform default + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static String toString(InputStream input, Charset encoding) throws IOException { + StringBuilderWriter sw = new StringBuilderWriter(); + copy(input, sw, encoding); + return sw.toString(); + } + + /** + * Get the contents of an InputStream as a String + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from + * @param encoding the encoding to use, null means platform default + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + */ + public static String toString(InputStream input, String encoding) + throws IOException { + return toString(input, Charsets.toCharset(encoding)); + } + + /** + * Get the contents of a Reader as a String. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static String toString(Reader input) throws IOException { + StringBuilderWriter sw = new StringBuilderWriter(); + copy(input, sw); + return sw.toString(); + } + + /** + * Gets the contents at the given URI. + * + * @param uri + * The URI source. + * @return The contents of the URL as a String. + * @throws IOException if an I/O exception occurs. + * @since 2.1 + */ + public static String toString(URI uri) throws IOException { + return toString(uri, Charset.defaultCharset()); + } + + /** + * Gets the contents at the given URI. + * + * @param uri + * The URI source. + * @param encoding + * The encoding name for the URL contents. + * @return The contents of the URL as a String. + * @throws IOException if an I/O exception occurs. + * @since 2.3. + */ + public static String toString(URI uri, Charset encoding) throws IOException { + return toString(uri.toURL(), Charsets.toCharset(encoding)); + } + + /** + * Gets the contents at the given URI. + * + * @param uri + * The URI source. + * @param encoding + * The encoding name for the URL contents. + * @return The contents of the URL as a String. + * @throws IOException if an I/O exception occurs. + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 2.1 + */ + public static String toString(URI uri, String encoding) throws IOException { + return toString(uri, Charsets.toCharset(encoding)); + } + + /** + * Gets the contents at the given URL. + * + * @param url + * The URL source. + * @return The contents of the URL as a String. + * @throws IOException if an I/O exception occurs. + * @since 2.1 + */ + public static String toString(URL url) throws IOException { + return toString(url, Charset.defaultCharset()); + } + + /** + * Gets the contents at the given URL. + * + * @param url + * The URL source. + * @param encoding + * The encoding name for the URL contents. + * @return The contents of the URL as a String. + * @throws IOException if an I/O exception occurs. + * @since 2.3 + */ + public static String toString(URL url, Charset encoding) throws IOException { + InputStream inputStream = url.openStream(); + try { + return toString(inputStream, encoding); + } finally { + inputStream.close(); + } + } + + /** + * Gets the contents at the given URL. + * + * @param url + * The URL source. + * @param encoding + * The encoding name for the URL contents. + * @return The contents of the URL as a String. + * @throws IOException if an I/O exception occurs. + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 2.1 + */ + public static String toString(URL url, String encoding) throws IOException { + return toString(url, Charsets.toCharset(encoding)); + } + + /** + * Get the contents of a byte[] as a String + * using the default character encoding of the platform. + * + * @param input the byte array to read from + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs (never occurs) + * @deprecated Use {@link String#String(byte[])} + */ + @Deprecated + public static String toString(byte[] input) throws IOException { + return new String(input); + } + + /** + * Get the contents of a byte[] as a String + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + * + * @param input the byte array to read from + * @param encoding the encoding to use, null means platform default + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs (never occurs) + */ + public static String toString(byte[] input, String encoding) throws IOException { + return new String(input, Charsets.toCharset(encoding)); + } + + // readLines + //----------------------------------------------------------------------- + /** + * Get the contents of an InputStream as a list of Strings, + * one entry per line, using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from, not null + * @return the list of Strings, never null + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static List readLines(InputStream input) throws IOException { + return readLines(input, Charset.defaultCharset()); + } + + /** + * Get the contents of an InputStream as a list of Strings, + * one entry per line, using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from, not null + * @param encoding the encoding to use, null means platform default + * @return the list of Strings, never null + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static List readLines(InputStream input, Charset encoding) throws IOException { + InputStreamReader reader = new InputStreamReader(input, Charsets.toCharset(encoding)); + return readLines(reader); + } + + /** + * Get the contents of an InputStream as a list of Strings, + * one entry per line, using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + * + * @param input the InputStream to read from, not null + * @param encoding the encoding to use, null means platform default + * @return the list of Strings, never null + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + */ + public static List readLines(InputStream input, String encoding) throws IOException { + return readLines(input, Charsets.toCharset(encoding)); + } + + /** + * Get the contents of a Reader as a list of Strings, + * one entry per line. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + * + * @param input the Reader to read from, not null + * @return the list of Strings, never null + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static List readLines(Reader input) throws IOException { + BufferedReader reader = toBufferedReader(input); + List list = new ArrayList(); + String line = reader.readLine(); + while (line != null) { + list.add(line); + line = reader.readLine(); + } + return list; + } + + // lineIterator + //----------------------------------------------------------------------- + /** + * Return an Iterator for the lines in a Reader. + *

+ * LineIterator holds a reference to the open + * Reader specified here. When you have finished with the + * iterator you should close the reader to free internal resources. + * This can be done by closing the reader directly, or by calling + * {@link LineIterator#close()} or {@link LineIterator#closeQuietly(LineIterator)}. + *

+ * The recommended usage pattern is: + *

+     * try {
+     *   LineIterator it = IOUtils.lineIterator(reader);
+     *   while (it.hasNext()) {
+     *     String line = it.nextLine();
+     *     /// do something with line
+     *   }
+     * } finally {
+     *   IOUtils.closeQuietly(reader);
+     * }
+     * 
+ * + * @param reader the Reader to read from, not null + * @return an Iterator of the lines in the reader, never null + * @throws IllegalArgumentException if the reader is null + * @since 1.2 + */ + public static LineIterator lineIterator(Reader reader) { + return new LineIterator(reader); + } + + /** + * Return an Iterator for the lines in an InputStream, using + * the character encoding specified (or default encoding if null). + *

+ * LineIterator holds a reference to the open + * InputStream specified here. When you have finished with + * the iterator you should close the stream to free internal resources. + * This can be done by closing the stream directly, or by calling + * {@link LineIterator#close()} or {@link LineIterator#closeQuietly(LineIterator)}. + *

+ * The recommended usage pattern is: + *

+     * try {
+     *   LineIterator it = IOUtils.lineIterator(stream, charset);
+     *   while (it.hasNext()) {
+     *     String line = it.nextLine();
+     *     /// do something with line
+     *   }
+     * } finally {
+     *   IOUtils.closeQuietly(stream);
+     * }
+     * 
+ * + * @param input the InputStream to read from, not null + * @param encoding the encoding to use, null means platform default + * @return an Iterator of the lines in the reader, never null + * @throws IllegalArgumentException if the input is null + * @throws IOException if an I/O error occurs, such as if the encoding is invalid + * @since 2.3 + */ + public static LineIterator lineIterator(InputStream input, Charset encoding) throws IOException { + return new LineIterator(new InputStreamReader(input, Charsets.toCharset(encoding))); + } + + /** + * Return an Iterator for the lines in an InputStream, using + * the character encoding specified (or default encoding if null). + *

+ * LineIterator holds a reference to the open + * InputStream specified here. When you have finished with + * the iterator you should close the stream to free internal resources. + * This can be done by closing the stream directly, or by calling + * {@link LineIterator#close()} or {@link LineIterator#closeQuietly(LineIterator)}. + *

+ * The recommended usage pattern is: + *

+     * try {
+     *   LineIterator it = IOUtils.lineIterator(stream, "UTF-8");
+     *   while (it.hasNext()) {
+     *     String line = it.nextLine();
+     *     /// do something with line
+     *   }
+     * } finally {
+     *   IOUtils.closeQuietly(stream);
+     * }
+     * 
+ * + * @param input the InputStream to read from, not null + * @param encoding the encoding to use, null means platform default + * @return an Iterator of the lines in the reader, never null + * @throws IllegalArgumentException if the input is null + * @throws IOException if an I/O error occurs, such as if the encoding is invalid + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.2 + */ + public static LineIterator lineIterator(InputStream input, String encoding) throws IOException { + return lineIterator(input, Charsets.toCharset(encoding)); + } + + //----------------------------------------------------------------------- + /** + * Convert the specified CharSequence to an input stream, encoded as bytes + * using the default character encoding of the platform. + * + * @param input the CharSequence to convert + * @return an input stream + * @since 2.0 + */ + public static InputStream toInputStream(CharSequence input) { + return toInputStream(input, Charset.defaultCharset()); + } + + /** + * Convert the specified CharSequence to an input stream, encoded as bytes + * using the specified character encoding. + * + * @param input the CharSequence to convert + * @param encoding the encoding to use, null means platform default + * @return an input stream + * @since 2.3 + */ + public static InputStream toInputStream(CharSequence input, Charset encoding) { + return toInputStream(input.toString(), encoding); + } + + /** + * Convert the specified CharSequence to an input stream, encoded as bytes + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + * + * @param input the CharSequence to convert + * @param encoding the encoding to use, null means platform default + * @return an input stream + * @throws IOException if the encoding is invalid + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 2.0 + */ + public static InputStream toInputStream(CharSequence input, String encoding) throws IOException { + return toInputStream(input, Charsets.toCharset(encoding)); + } + + //----------------------------------------------------------------------- + /** + * Convert the specified string to an input stream, encoded as bytes + * using the default character encoding of the platform. + * + * @param input the string to convert + * @return an input stream + * @since 1.1 + */ + public static InputStream toInputStream(String input) { + return toInputStream(input, Charset.defaultCharset()); + } + + /** + * Convert the specified string to an input stream, encoded as bytes + * using the specified character encoding. + * + * @param input the string to convert + * @param encoding the encoding to use, null means platform default + * @return an input stream + * @since 2.3 + */ + public static InputStream toInputStream(String input, Charset encoding) { + return new ByteArrayInputStream(input.getBytes(Charsets.toCharset(encoding))); + } + + /** + * Convert the specified string to an input stream, encoded as bytes + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + * + * @param input the string to convert + * @param encoding the encoding to use, null means platform default + * @return an input stream + * @throws IOException if the encoding is invalid + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + */ + public static InputStream toInputStream(String input, String encoding) throws IOException { + byte[] bytes = input.getBytes(Charsets.toCharset(encoding)); + return new ByteArrayInputStream(bytes); + } + + // write byte[] + //----------------------------------------------------------------------- + /** + * Writes bytes from a byte[] to an OutputStream. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the OutputStream to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static void write(byte[] data, OutputStream output) + throws IOException { + if (data != null) { + output.write(data); + } + } + + /** + * Writes bytes from a byte[] to chars on a Writer + * using the default character encoding of the platform. + *

+ * This method uses {@link String#String(byte[])}. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the Writer to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static void write(byte[] data, Writer output) throws IOException { + write(data, output, Charset.defaultCharset()); + } + + /** + * Writes bytes from a byte[] to chars on a Writer + * using the specified character encoding. + *

+ * This method uses {@link String#String(byte[], String)}. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the Writer to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static void write(byte[] data, Writer output, Charset encoding) throws IOException { + if (data != null) { + output.write(new String(data, Charsets.toCharset(encoding))); + } + } + + /** + * Writes bytes from a byte[] to chars on a Writer + * using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#String(byte[], String)}. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the Writer to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + */ + public static void write(byte[] data, Writer output, String encoding) throws IOException { + write(data, output, Charsets.toCharset(encoding)); + } + + // write char[] + //----------------------------------------------------------------------- + /** + * Writes chars from a char[] to a Writer + * using the default character encoding of the platform. + * + * @param data the char array to write, do not modify during output, + * null ignored + * @param output the Writer to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static void write(char[] data, Writer output) throws IOException { + if (data != null) { + output.write(data); + } + } + + /** + * Writes chars from a char[] to bytes on an + * OutputStream. + *

+ * This method uses {@link String#String(char[])} and + * {@link String#getBytes()}. + * + * @param data the char array to write, do not modify during output, + * null ignored + * @param output the OutputStream to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static void write(char[] data, OutputStream output) + throws IOException { + write(data, output, Charset.defaultCharset()); + } + + /** + * Writes chars from a char[] to bytes on an + * OutputStream using the specified character encoding. + *

+ * This method uses {@link String#String(char[])} and + * {@link String#getBytes(String)}. + * + * @param data the char array to write, do not modify during output, + * null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static void write(char[] data, OutputStream output, Charset encoding) throws IOException { + if (data != null) { + output.write(new String(data).getBytes(Charsets.toCharset(encoding))); + } + } + + /** + * Writes chars from a char[] to bytes on an + * OutputStream using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#String(char[])} and + * {@link String#getBytes(String)}. + * + * @param data the char array to write, do not modify during output, + * null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + */ + public static void write(char[] data, OutputStream output, String encoding) + throws IOException { + write(data, output, Charsets.toCharset(encoding)); + } + + // write CharSequence + //----------------------------------------------------------------------- + /** + * Writes chars from a CharSequence to a Writer. + * + * @param data the CharSequence to write, null ignored + * @param output the Writer to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 2.0 + */ + public static void write(CharSequence data, Writer output) throws IOException { + if (data != null) { + write(data.toString(), output); + } + } + + /** + * Writes chars from a CharSequence to bytes on an + * OutputStream using the default character encoding of the + * platform. + *

+ * This method uses {@link String#getBytes()}. + * + * @param data the CharSequence to write, null ignored + * @param output the OutputStream to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 2.0 + */ + public static void write(CharSequence data, OutputStream output) + throws IOException { + write(data, output, Charset.defaultCharset()); + } + + /** + * Writes chars from a CharSequence to bytes on an + * OutputStream using the specified character encoding. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the CharSequence to write, null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static void write(CharSequence data, OutputStream output, Charset encoding) throws IOException { + if (data != null) { + write(data.toString(), output, encoding); + } + } + + /** + * Writes chars from a CharSequence to bytes on an + * OutputStream using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the CharSequence to write, null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 2.0 + */ + public static void write(CharSequence data, OutputStream output, String encoding) throws IOException { + write(data, output, Charsets.toCharset(encoding)); + } + + // write String + //----------------------------------------------------------------------- + /** + * Writes chars from a String to a Writer. + * + * @param data the String to write, null ignored + * @param output the Writer to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static void write(String data, Writer output) throws IOException { + if (data != null) { + output.write(data); + } + } + + /** + * Writes chars from a String to bytes on an + * OutputStream using the default character encoding of the + * platform. + *

+ * This method uses {@link String#getBytes()}. + * + * @param data the String to write, null ignored + * @param output the OutputStream to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static void write(String data, OutputStream output) + throws IOException { + write(data, output, Charset.defaultCharset()); + } + + /** + * Writes chars from a String to bytes on an + * OutputStream using the specified character encoding. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the String to write, null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static void write(String data, OutputStream output, Charset encoding) throws IOException { + if (data != null) { + output.write(data.getBytes(Charsets.toCharset(encoding))); + } + } + + /** + * Writes chars from a String to bytes on an + * OutputStream using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the String to write, null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + */ + public static void write(String data, OutputStream output, String encoding) + throws IOException { + write(data, output, Charsets.toCharset(encoding)); + } + + // write StringBuffer + //----------------------------------------------------------------------- + /** + * Writes chars from a StringBuffer to a Writer. + * + * @param data the StringBuffer to write, null ignored + * @param output the Writer to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + * @deprecated replaced by write(CharSequence, Writer) + */ + @Deprecated + public static void write(StringBuffer data, Writer output) + throws IOException { + if (data != null) { + output.write(data.toString()); + } + } + + /** + * Writes chars from a StringBuffer to bytes on an + * OutputStream using the default character encoding of the + * platform. + *

+ * This method uses {@link String#getBytes()}. + * + * @param data the StringBuffer to write, null ignored + * @param output the OutputStream to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + * @deprecated replaced by write(CharSequence, OutputStream) + */ + @Deprecated + public static void write(StringBuffer data, OutputStream output) + throws IOException { + write(data, output, (String) null); + } + + /** + * Writes chars from a StringBuffer to bytes on an + * OutputStream using the specified character encoding. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link String#getBytes(String)}. + * + * @param data the StringBuffer to write, null ignored + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + * @deprecated replaced by write(CharSequence, OutputStream, String) + */ + @Deprecated + public static void write(StringBuffer data, OutputStream output, String encoding) throws IOException { + if (data != null) { + output.write(data.toString().getBytes(Charsets.toCharset(encoding))); + } + } + + // writeLines + //----------------------------------------------------------------------- + /** + * Writes the toString() value of each item in a collection to + * an OutputStream line by line, using the default character + * encoding of the platform and the specified line ending. + * + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @param output the OutputStream to write to, not null, not closed + * @throws NullPointerException if the output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static void writeLines(Collection lines, String lineEnding, + OutputStream output) throws IOException { + writeLines(lines, lineEnding, output, Charset.defaultCharset()); + } + + /** + * Writes the toString() value of each item in a collection to + * an OutputStream line by line, using the specified character + * encoding and the specified line ending. + * + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @param output the OutputStream to write to, not null, not closed + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the output is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static void writeLines(Collection lines, String lineEnding, OutputStream output, Charset encoding) + throws IOException { + if (lines == null) { + return; + } + if (lineEnding == null) { + lineEnding = LINE_SEPARATOR; + } + Charset cs = Charsets.toCharset(encoding); + for (Object line : lines) { + if (line != null) { + output.write(line.toString().getBytes(cs)); + } + output.write(lineEnding.getBytes(cs)); + } + } + + /** + * Writes the toString() value of each item in a collection to + * an OutputStream line by line, using the specified character + * encoding and the specified line ending. + *

+ * Character encoding names can be found at + * IANA. + * + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @param output the OutputStream to write to, not null, not closed + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the output is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + */ + public static void writeLines(Collection lines, String lineEnding, + OutputStream output, String encoding) throws IOException { + writeLines(lines, lineEnding, output, Charsets.toCharset(encoding)); + } + + /** + * Writes the toString() value of each item in a collection to + * a Writer line by line, using the specified line ending. + * + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @param writer the Writer to write to, not null, not closed + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static void writeLines(Collection lines, String lineEnding, + Writer writer) throws IOException { + if (lines == null) { + return; + } + if (lineEnding == null) { + lineEnding = LINE_SEPARATOR; + } + for (Object line : lines) { + if (line != null) { + writer.write(line.toString()); + } + writer.write(lineEnding); + } + } + + // copy from InputStream + //----------------------------------------------------------------------- + /** + * Copy bytes from an InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * Large streams (over 2GB) will return a bytes copied value of + * -1 after the copy has completed since the correct + * number of bytes cannot be returned as an int. For large streams + * use the copyLarge(InputStream, OutputStream) method. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied, or -1 if > Integer.MAX_VALUE + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static int copy(InputStream input, OutputStream output) throws IOException { + long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copy bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.3 + */ + public static long copyLarge(InputStream input, OutputStream output) + throws IOException { + return copyLarge(input, output, new byte[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copy bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedInputStream. + *

+ * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param buffer the buffer to use for the copy + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(InputStream input, OutputStream output, byte[] buffer) + throws IOException { + long count = 0; + int n = 0; + while (EOF != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Copy some or all bytes from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input bytes. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param inputOffset : number of bytes to skip from input before copying + * -ve values are ignored + * @param length : number of bytes to copy. -ve means all + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(InputStream input, OutputStream output, long inputOffset, long length) + throws IOException { + return copyLarge(input, output, inputOffset, length, new byte[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copy some or all bytes from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input bytes. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedInputStream. + *

+ * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param inputOffset : number of bytes to skip from input before copying + * -ve values are ignored + * @param length : number of bytes to copy. -ve means all + * @param buffer the buffer to use for the copy + * + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(InputStream input, OutputStream output, + final long inputOffset, final long length, byte[] buffer) throws IOException { + if (inputOffset > 0) { + skipFully(input, inputOffset); + } + if (length == 0) { + return 0; + } + final int bufferLength = buffer.length; + int bytesToRead = bufferLength; + if (length > 0 && length < bufferLength) { + bytesToRead = (int) length; + } + int read; + long totalRead = 0; + while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) { + output.write(buffer, 0, read); + totalRead += read; + if (length > 0) { // only adjust length if not reading to the end + // Note the cast must work because buffer.length is an integer + bytesToRead = (int) Math.min(length - totalRead, bufferLength); + } + } + return totalRead; + } + + /** + * Copy bytes from an InputStream to chars on a + * Writer using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static void copy(InputStream input, Writer output) + throws IOException { + copy(input, output, Charset.defaultCharset()); + } + + /** + * Copy bytes from an InputStream to chars on a + * Writer using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static void copy(InputStream input, Writer output, Charset encoding) throws IOException { + InputStreamReader in = new InputStreamReader(input, Charsets.toCharset(encoding)); + copy(in, output); + } + + /** + * Copy bytes from an InputStream to chars on a + * Writer using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + */ + public static void copy(InputStream input, Writer output, String encoding) throws IOException { + copy(input, output, Charsets.toCharset(encoding)); + } + + // copy from Reader + //----------------------------------------------------------------------- + /** + * Copy chars from a Reader to a Writer. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Large streams (over 2GB) will return a chars copied value of + * -1 after the copy has completed since the correct + * number of chars cannot be returned as an int. For large streams + * use the copyLarge(Reader, Writer) method. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @return the number of characters copied, or -1 if > Integer.MAX_VALUE + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static int copy(Reader input, Writer output) throws IOException { + long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copy chars from a large (over 2GB) Reader to a Writer. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.3 + */ + public static long copyLarge(Reader input, Writer output) throws IOException { + return copyLarge(input, output, new char[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copy chars from a large (over 2GB) Reader to a Writer. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedReader. + *

+ * + * @param input the Reader to read from + * @param output the Writer to write to + * @param buffer the buffer to be used for the copy + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(Reader input, Writer output, char [] buffer) throws IOException { + long count = 0; + int n = 0; + while (EOF != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Copy some or all chars from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input chars. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @param inputOffset : number of chars to skip from input before copying + * -ve values are ignored + * @param length : number of chars to copy. -ve means all + * @return the number of chars copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(Reader input, Writer output, final long inputOffset, final long length) + throws IOException { + return copyLarge(input, output, inputOffset, length, new char[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copy some or all chars from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input chars. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedReader. + *

+ * + * @param input the Reader to read from + * @param output the Writer to write to + * @param inputOffset : number of chars to skip from input before copying + * -ve values are ignored + * @param length : number of chars to copy. -ve means all + * @param buffer the buffer to be used for the copy + * @return the number of chars copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(Reader input, Writer output, final long inputOffset, final long length, char [] buffer) + throws IOException { + if (inputOffset > 0) { + skipFully(input, inputOffset); + } + if (length == 0) { + return 0; + } + int bytesToRead = buffer.length; + if (length > 0 && length < buffer.length) { + bytesToRead = (int) length; + } + int read; + long totalRead = 0; + while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) { + output.write(buffer, 0, read); + totalRead += read; + if (length > 0) { // only adjust length if not reading to the end + // Note the cast must work because buffer.length is an integer + bytesToRead = (int) Math.min(length - totalRead, buffer.length); + } + } + return totalRead; + } + + /** + * Copy chars from a Reader to bytes on an + * OutputStream using the default character encoding of the + * platform, and calling flush. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

+ * This method uses {@link OutputStreamWriter}. + * + * @param input the Reader to read from + * @param output the OutputStream to write to + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static void copy(Reader input, OutputStream output) + throws IOException { + copy(input, output, Charset.defaultCharset()); + } + + /** + * Copy chars from a Reader to bytes on an + * OutputStream using the specified character encoding, and + * calling flush. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ *

+ * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

+ *

+ * This method uses {@link OutputStreamWriter}. + *

+ * + * @param input the Reader to read from + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static void copy(Reader input, OutputStream output, Charset encoding) throws IOException { + OutputStreamWriter out = new OutputStreamWriter(output, Charsets.toCharset(encoding)); + copy(input, out); + // XXX Unless anyone is planning on rewriting OutputStreamWriter, + // we have to flush here. + out.flush(); + } + + /** + * Copy chars from a Reader to bytes on an + * OutputStream using the specified character encoding, and + * calling flush. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Character encoding names can be found at + * IANA. + *

+ * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

+ * This method uses {@link OutputStreamWriter}. + * + * @param input the Reader to read from + * @param output the OutputStream to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws UnsupportedCharsetException + * thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not + * supported. + * @since 1.1 + */ + public static void copy(Reader input, OutputStream output, String encoding) throws IOException { + copy(input, output, Charsets.toCharset(encoding)); + } + + // content equals + //----------------------------------------------------------------------- + /** + * Compare the contents of two Streams to determine if they are equal or + * not. + *

+ * This method buffers the input internally using + * BufferedInputStream if they are not already buffered. + * + * @param input1 the first stream + * @param input2 the second stream + * @return true if the content of the streams are equal or they both don't + * exist, false otherwise + * @throws NullPointerException if either input is null + * @throws IOException if an I/O error occurs + */ + public static boolean contentEquals(InputStream input1, InputStream input2) + throws IOException { + if (!(input1 instanceof BufferedInputStream)) { + input1 = new BufferedInputStream(input1); + } + if (!(input2 instanceof BufferedInputStream)) { + input2 = new BufferedInputStream(input2); + } + + int ch = input1.read(); + while (EOF != ch) { + int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + int ch2 = input2.read(); + return ch2 == EOF; + } + + /** + * Compare the contents of two Readers to determine if they are equal or + * not. + *

+ * This method buffers the input internally using + * BufferedReader if they are not already buffered. + * + * @param input1 the first reader + * @param input2 the second reader + * @return true if the content of the readers are equal or they both don't + * exist, false otherwise + * @throws NullPointerException if either input is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static boolean contentEquals(Reader input1, Reader input2) + throws IOException { + + input1 = toBufferedReader(input1); + input2 = toBufferedReader(input2); + + int ch = input1.read(); + while (EOF != ch) { + int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + int ch2 = input2.read(); + return ch2 == EOF; + } + + /** + * Compare the contents of two Readers to determine if they are equal or + * not, ignoring EOL characters. + *

+ * This method buffers the input internally using + * BufferedReader if they are not already buffered. + * + * @param input1 the first reader + * @param input2 the second reader + * @return true if the content of the readers are equal (ignoring EOL differences), false otherwise + * @throws NullPointerException if either input is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static boolean contentEqualsIgnoreEOL(Reader input1, Reader input2) + throws IOException { + BufferedReader br1 = toBufferedReader(input1); + BufferedReader br2 = toBufferedReader(input2); + + String line1 = br1.readLine(); + String line2 = br2.readLine(); + while (line1 != null && line2 != null && line1.equals(line2)) { + line1 = br1.readLine(); + line2 = br2.readLine(); + } + return line1 == null ? line2 == null ? true : false : line1.equals(line2); + } + + /** + * Skip bytes from an input byte stream. + * This implementation guarantees that it will read as many bytes + * as possible before giving up; this may not always be the case for + * subclasses of {@link Reader}. + * + * @param input byte stream to skip + * @param toSkip number of bytes to skip. + * @return number of bytes actually skipped. + * + * @see InputStream#skip(long) + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @since 2.0 + */ + public static long skip(InputStream input, long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); + } + /* + * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data + * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer + * size were variable, we would need to synch. to ensure some other thread did not create a smaller one) + */ + if (SKIP_BYTE_BUFFER == null) { + SKIP_BYTE_BUFFER = new byte[SKIP_BUFFER_SIZE]; + } + long remain = toSkip; + while (remain > 0) { + long n = input.read(SKIP_BYTE_BUFFER, 0, (int) Math.min(remain, SKIP_BUFFER_SIZE)); + if (n < 0) { // EOF + break; + } + remain -= n; + } + return toSkip - remain; + } + + /** + * Skip characters from an input character stream. + * This implementation guarantees that it will read as many characters + * as possible before giving up; this may not always be the case for + * subclasses of {@link Reader}. + * + * @param input character stream to skip + * @param toSkip number of characters to skip. + * @return number of characters actually skipped. + * + * @see Reader#skip(long) + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @since 2.0 + */ + public static long skip(Reader input, long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); + } + /* + * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data + * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer + * size were variable, we would need to synch. to ensure some other thread did not create a smaller one) + */ + if (SKIP_CHAR_BUFFER == null) { + SKIP_CHAR_BUFFER = new char[SKIP_BUFFER_SIZE]; + } + long remain = toSkip; + while (remain > 0) { + long n = input.read(SKIP_CHAR_BUFFER, 0, (int) Math.min(remain, SKIP_BUFFER_SIZE)); + if (n < 0) { // EOF + break; + } + remain -= n; + } + return toSkip - remain; + } + + /** + * Skip the requested number of bytes or fail if there are not enough left. + *

+ * This allows for the possibility that {@link InputStream#skip(long)} may + * not skip as many bytes as requested (most likely because of reaching EOF). + * + * @param input stream to skip + * @param toSkip the number of bytes to skip + * @see InputStream#skip(long) + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @throws EOFException if the number of bytes skipped was incorrect + * @since 2.0 + */ + public static void skipFully(InputStream input, long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip); + } + long skipped = skip(input, toSkip); + if (skipped != toSkip) { + throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped); + } + } + + /** + * Skip the requested number of characters or fail if there are not enough left. + *

+ * This allows for the possibility that {@link Reader#skip(long)} may + * not skip as many characters as requested (most likely because of reaching EOF). + * + * @param input stream to skip + * @param toSkip the number of characters to skip + * @see Reader#skip(long) + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @throws EOFException if the number of characters skipped was incorrect + * @since 2.0 + */ + public static void skipFully(Reader input, long toSkip) throws IOException { + long skipped = skip(input, toSkip); + if (skipped != toSkip) { + throw new EOFException("Chars to skip: " + toSkip + " actual: " + skipped); + } + } + + + /** + * Read characters from an input character stream. + * This implementation guarantees that it will read as many characters + * as possible before giving up; this may not always be the case for + * subclasses of {@link Reader}. + * + * @param input where to read input from + * @param buffer destination + * @param offset inital offset into buffer + * @param length length to read, must be >= 0 + * @return actual length read; may be less than requested if EOF was reached + * @throws IOException if a read error occurs + * @since 2.2 + */ + public static int read(Reader input, char[] buffer, int offset, int length) throws IOException { + if (length < 0) { + throw new IllegalArgumentException("Length must not be negative: " + length); + } + int remaining = length; + while (remaining > 0) { + int location = length - remaining; + int count = input.read(buffer, offset + location, remaining); + if (EOF == count) { // EOF + break; + } + remaining -= count; + } + return length - remaining; + } + + /** + * Read characters from an input character stream. + * This implementation guarantees that it will read as many characters + * as possible before giving up; this may not always be the case for + * subclasses of {@link Reader}. + * + * @param input where to read input from + * @param buffer destination + * @return actual length read; may be less than requested if EOF was reached + * @throws IOException if a read error occurs + * @since 2.2 + */ + public static int read(Reader input, char[] buffer) throws IOException { + return read(input, buffer, 0, buffer.length); + } + + /** + * Read bytes from an input stream. + * This implementation guarantees that it will read as many bytes + * as possible before giving up; this may not always be the case for + * subclasses of {@link InputStream}. + * + * @param input where to read input from + * @param buffer destination + * @param offset inital offset into buffer + * @param length length to read, must be >= 0 + * @return actual length read; may be less than requested if EOF was reached + * @throws IOException if a read error occurs + * @since 2.2 + */ + public static int read(InputStream input, byte[] buffer, int offset, int length) throws IOException { + if (length < 0) { + throw new IllegalArgumentException("Length must not be negative: " + length); + } + int remaining = length; + while (remaining > 0) { + int location = length - remaining; + int count = input.read(buffer, offset + location, remaining); + if (EOF == count) { // EOF + break; + } + remaining -= count; + } + return length - remaining; + } + + /** + * Read bytes from an input stream. + * This implementation guarantees that it will read as many bytes + * as possible before giving up; this may not always be the case for + * subclasses of {@link InputStream}. + * + * @param input where to read input from + * @param buffer destination + * @return actual length read; may be less than requested if EOF was reached + * @throws IOException if a read error occurs + * @since 2.2 + */ + public static int read(InputStream input, byte[] buffer) throws IOException { + return read(input, buffer, 0, buffer.length); + } + + /** + * Read the requested number of characters or fail if there are not enough left. + *

+ * This allows for the possibility that {@link Reader#read(char[], int, int)} may + * not read as many characters as requested (most likely because of reaching EOF). + * + * @param input where to read input from + * @param buffer destination + * @param offset inital offset into buffer + * @param length length to read, must be >= 0 + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if length is negative + * @throws EOFException if the number of characters read was incorrect + * @since 2.2 + */ + public static void readFully(Reader input, char[] buffer, int offset, int length) throws IOException { + int actual = read(input, buffer, offset, length); + if (actual != length) { + throw new EOFException("Length to read: " + length + " actual: " + actual); + } + } + + /** + * Read the requested number of characters or fail if there are not enough left. + *

+ * This allows for the possibility that {@link Reader#read(char[], int, int)} may + * not read as many characters as requested (most likely because of reaching EOF). + * + * @param input where to read input from + * @param buffer destination + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if length is negative + * @throws EOFException if the number of characters read was incorrect + * @since 2.2 + */ + public static void readFully(Reader input, char[] buffer) throws IOException { + readFully(input, buffer, 0, buffer.length); + } + + /** + * Read the requested number of bytes or fail if there are not enough left. + *

+ * This allows for the possibility that {@link InputStream#read(byte[], int, int)} may + * not read as many bytes as requested (most likely because of reaching EOF). + * + * @param input where to read input from + * @param buffer destination + * @param offset inital offset into buffer + * @param length length to read, must be >= 0 + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if length is negative + * @throws EOFException if the number of bytes read was incorrect + * @since 2.2 + */ + public static void readFully(InputStream input, byte[] buffer, int offset, int length) throws IOException { + int actual = read(input, buffer, offset, length); + if (actual != length) { + throw new EOFException("Length to read: " + length + " actual: " + actual); + } + } + + /** + * Read the requested number of bytes or fail if there are not enough left. + *

+ * This allows for the possibility that {@link InputStream#read(byte[], int, int)} may + * not read as many bytes as requested (most likely because of reaching EOF). + * + * @param input where to read input from + * @param buffer destination + * + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if length is negative + * @throws EOFException if the number of bytes read was incorrect + * @since 2.2 + */ + public static void readFully(InputStream input, byte[] buffer) throws IOException { + readFully(input, buffer, 0, buffer.length); + } +}