/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2008-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.nio;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.Exceptions;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.io.ContentFormatException;
import org.geotoolkit.lang.Static;
import org.geotoolkit.resources.Errors;
import java.io.*;
import java.net.*;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.spi.FileSystemProvider;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import static java.nio.file.Files.*;
import static java.nio.file.StandardOpenOption.*;
/**
* Utility methods related to I/O operations.
*
* @author Martin Desruisseaux (Geomatys)
* @author Johann Sorel (Geomatys)
* @author Quentin Boileau (Geomatys)
* @version 4.1
*
* @since 3.00
* @module
*/
public final class IOUtilities extends Static {
/**
* Logger
*/
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.io");
/**
* The default buffer size for copy operations.
*/
private static final int BUFFER_SIZE = 8192;
/**
* Default charset with UTF-8
*/
public static final Charset UTF8_CHARSET = Charset.forName("UTF-8");
/**
* The writer to the console (if possible) or standard output stream otherwise.
* This is created when first needed.
*/
private static Writer stdout;
/**
* A printer wrapping {@link #stdout}, created when first needed.
*/
private static PrintWriter printer;
/**
* Keep all {@link Path} to delete on JVM shutdown event on a concurrent Set.
*/
private static final Set<Path> DELETE_ON_EXIT_PATHS = Collections.newSetFromMap(new ConcurrentHashMap<Path, Boolean>());
static {
Runtime.getRuntime().addShutdownHook(new Thread() {
public void run() {
for (Path path : DELETE_ON_EXIT_PATHS) {
if (Files.exists(path)) {
try {
if (Files.isDirectory(path)) {
IOUtilities.deleteRecursively(path);
} else {
Files.deleteIfExists(path);
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Unable to delete on exit file : " + path.toString());
}
}
}
}
});
}
/**
* Do not allow instantiation of this class.
*/
private IOUtilities() {
}
/**
* Register {@link Path} for a delete on JM shutdown event.
* Under the hood, this method register path to be delete using {@link Runtime#addShutdownHook(Thread)}.
*
* @param path path to delete
*/
public static void deleteOnExit(Path path) {
DELETE_ON_EXIT_PATHS.add(path);
}
/**
* Returns a writer to the standard output stream. This method returns the
* {@linkplain Console#writer() console writer} if available, because that
* writer uses a more appropriate encoding on some platform. In no console
* writer is available, then a writer wrapping the {@linkplain System#out
* standard output stream} is returned.
*
* @return A writer to the standard output stream.
*
* @since 3.17
*/
public static synchronized Writer standardWriter() {
if (stdout == null) {
final Console console = System.console();
if (console != null) {
stdout = printer = console.writer();
} else {
stdout = new OutputStreamWriter(System.out);
}
}
return stdout;
}
/**
* Returns a printer to the standard output stream. This method returns the
* {@linkplain Console#writer() console printer} if available, because that
* printer uses a more appropriate encoding on some platform. In no console
* printer is available, then a printer wrapping the {@linkplain System#out
* standard output stream} is returned.
*
* @return A printer to the standard output stream.
*
* @see org.geotoolkit.io.NumberedLineWriter#getStandardOutput()
*
* @since 3.17
*/
public static synchronized PrintWriter standardPrintWriter() {
if (printer == null) {
final Writer writer = standardWriter();
if (printer == null) {
printer = new PrintWriter(writer, true);
}
}
return printer;
}
/**
* If the two given Path are equals, return that Path. Otherwise returns the first
* common parent found.
*
* @param root The Path which is the most likely to be the root.
* @param file The other Path, which is more likely to be in a sub-directory of the root.
* @return The root or a common parent, or {@code null} if no common parent has been found.
*/
public static Path commonParent(Path root, final Path file) {
while (root != null) {
root = root.normalize();
if (file != null) {
final Path nfile = file.normalize();
for (Path candidate = nfile; candidate != null; candidate = candidate.getParent()) {
if (root.equals(candidate)) {
return root;
}
}
root = root.getParent();
}
}
return null;
}
/**
* Tries to convert the given path to a {@link File} object if possible, or returns
* the path unchanged otherwise. Conversion attempts are performed for paths of class
* {@link CharSequence}, {@link URL}, {@link URI} or {@link Path}.
* <p>
* If a conversion from a {@link URL} object was necessary, then the URL is assumed
* to <strong>not</strong> be encoded.
*
* @param path The path to convert to a {@link File} if possible.
* @return The path as a {@link File} if this conversion was possible.
* @throws IOException If an error occurred while converting the path to a file.
*
* @since 3.07
* @deprecated prefer using {@link #tryToPath(Object)} method to deal with Path instead of File
*/
public static Object tryToFile(Object path) throws IOException {
if (path == null) {
return null;
}
if (path instanceof File) {
return (File) path;
} else if (path instanceof CharSequence) {
return org.apache.sis.internal.storage.IOUtilities.toFileOrURL(path.toString(), null);
} else if (path instanceof URL) {
final URL url = (URL) path;
if (url.getProtocol().equalsIgnoreCase("file")) {
return org.apache.sis.internal.storage.IOUtilities.toFile(url, null);
}
} else if (path instanceof URI) {
final URI uri = (URI) path;
final String scheme = uri.getScheme();
if (scheme != null && scheme.equalsIgnoreCase("file")) try {
return new File(uri);
} catch (IllegalArgumentException cause) {
/*
* Typically because the URI contains a fragment (for example a query part)
* that can not be represented as a File. We consider that as an error
* because the scheme pretended that we had a file URI.
*/
final IOException e = new MalformedURLException(concatenate(
Errors.format(Errors.Keys.IllegalArgument_2, "URI", path), cause));
e.initCause(cause);
throw e;
}
} else if (path instanceof Path) {
return ((Path) path).toFile();
}
return path;
}
/**
* Tries to convert the given object to a {@link Path} object if possible, or returns
* the path unchanged otherwise. Conversion attempts are performed for objects of class
* {@link CharSequence}, {@link URL}, {@link URI} or {@link File}.
* <p>
* If a conversion from a {@link URL} object was necessary, then the URL is assumed
* to <strong>not</strong> be encoded.
*
* @param path The path to convert to a {@link Path} if possible.
* @return The path as a {@link Path} if this conversion was possible. A Conversion to Path can fail
* if input Object is not supported or converted Path use a FileSystem not supported by default.
*
* @since 3.20 (derived from 3.07)
*/
public static Object tryToPath(Object path) {
try {
return toPath(path);
} catch (IllegalArgumentException | IOException ex) {
// input candidate can't be converted into Path
return path;
}
}
/**
* Tries to convert the given candidate into a {@link Path} object if possible, or throw IllegalArgumentException
* if candidate can't be converted. Conversion attempts are performed for paths of class
* {@link CharSequence}, {@link URL}, {@link URI} or {@link File}.
* <p>
* If a conversion from a {@link URL} object was necessary, then the URL is assumed
* to <strong>not</strong> be encoded.
*
* @param candidate The candidate to convert to a {@link Path} if possible. Can return {@code null} if input
* candidate was {@code null}
* @return The candidate as a {@link Path} if this conversion was possible.
* @throws IOException If an error occurred while converting the candidate to a Path. For example a not supported
* FileSystem
* @throws IllegalArgumentException input object can't be converted into {@link Path}
*
*/
public static Path toPath(Object candidate) throws IOException, IllegalArgumentException {
if (candidate == null) {
return null;
}
if (candidate instanceof Path) {
return (Path) candidate;
}
if (candidate instanceof CharSequence) {
URI uri = URI.create(toSafeURI(String.valueOf(candidate)));
if (uri.getScheme() != null) {
//let java check matching FileSystem
return Paths.get(uri);
} else {
//assume on local filesystem
return Paths.get(candidate.toString());
}
} else if (candidate instanceof URL) {
final URL url = (URL) candidate;
return org.apache.sis.internal.storage.IOUtilities.toPath(url, null);
} else if (candidate instanceof File) {
return ((File) candidate).toPath();
} else if (candidate instanceof URI) {
final URI uri = (URI) candidate;
try {
if (uri.getScheme() == null) {
//scheme null, consider as Path on default FileSystem
return Paths.get(uri.toString());
}
return Paths.get(uri);
} catch (IllegalArgumentException | FileSystemNotFoundException cause) {
final String message = Exceptions.formatChainedMessages(null,
org.apache.sis.util.resources.Errors.format(org.apache.sis.util.resources.Errors.Keys.IllegalArgumentValue_2, "URI", uri), cause);
/*
* If the exception is IllegalArgumentException, then the URI scheme has been recognized
* but the URI syntax is illegal for that file system. So we can consider that the URL is
* malformed in regard to the rules of that particular file system.
*/
final IOException e;
if (cause instanceof IllegalArgumentException) {
e = new MalformedURLException(message);
e.initCause(cause);
} else {
e = new IOException(message, cause);
}
throw e;
}
}
throw new IllegalArgumentException("Can't convert "+candidate.getClass()+" into a Path."+
"Supported candidate type are CharSequence, URL, URI, File and Path");
}
/**
* Encode a String into a safe URI format.
* For example space characters will be replaced with %20
*
* @param candidate String
* @return encoded candidate String
*/
private static String toSafeURI(String candidate) {
StringBuilder sb = new StringBuilder();
for (char ch : candidate.toCharArray()) {
if (isUnsafe(ch)) {
sb.append('%');
sb.append(Integer.toHexString(ch));
} else {
sb.append(ch);
}
}
return sb.toString();
}
/**
* Check if a char need to be encoded into URI format.
* Ignore reserved characters see <a href="https://tools.ietf.org/html/rfc3986#section-2.2">URI RFC/a>
*
* @param ch
* @return
*/
private static boolean isUnsafe(char ch) {
return ch > 128 || " <>".indexOf(ch) >= 0;
}
/**
* Returns {@code true} if the method in this class can process the given object as a path.
* Note: {@link String} input is not considered compatible as {@link Path}.
*
* @param path The object to test, or {@code null}.
* @return {@code true} If the given object is non-null and can be processed like a path.
*
* @since 3.08
*/
public static boolean canProcessAsPath(final Object path) {
return (
//(path instanceof CharSequence) ||
(path instanceof File) ||
(path instanceof URL) ||
(path instanceof URI) ||
(path instanceof Path));
}
/**
* Test if an object can be process as Path (see {@link #canProcessAsPath(Object)}) and if input object
* target a known FileSystem using his scheme and {@link FileSystemProvider#installedProviders()}.
*
* @param path candidate object
* @return {@code true} if input object is compatible with {@link Path} API AND
* if FileSystem is known. {@code false} otherwise.
*/
public static boolean isFileSystemSupported(final Object path) {
if (!canProcessAsPath(path)) {
return false;
}
URI uri = null;
if (path instanceof URL) {
try {
uri = org.apache.sis.internal.storage.IOUtilities.toURI((URL) path, "UTF-8");
} catch (IOException e) {
//unable to create URI from URL
return false;
}
} else if (path instanceof URI) {
uri = (URI) path;
} else {
return true;
}
String scheme = uri.getScheme();
if (scheme == null) {
//scheme null, consider as Path on default FileSystem
return true;
} else {
// loop over FS providers and test path creation
for (FileSystemProvider provider: FileSystemProvider.installedProviders()) {
if (provider.getScheme().equalsIgnoreCase(scheme)) {
try {
provider.getPath(uri);
return true;
} catch (IllegalArgumentException fse) {
// path construction failed
}
}
}
//no matching FS provider
return false;
}
}
/**
* Wrap of {@link org.apache.sis.internal.storage.IOUtilities#filename(Object)} (Object)} to avoid double import.
* Returns the filename from a {@link Path}, {@link File}, {@link URL}, {@link URI} or {@link CharSequence}
* instance. If the given argument is specialized type like {@code Path} or {@code File}, then this method uses
* dedicated API like {@link Path#getFileName()}. Otherwise this method gets a string representation of the path
* and returns the part after the last {@code '/'} or platform-dependent name separator character, if any.
*
* @param input An instance of one of the above-cited types, or {@code null}.
* @return The filename in the given input, or {@code null} if the given object is null or of unknown type.
*/
public static String filename(Object input) {
return org.apache.sis.internal.storage.IOUtilities.filename(input);
}
/**
* Wrap of {@link org.apache.sis.internal.storage.IOUtilities#extension(Object)} (Object)} to avoid double import.
* Returns the filename extension from a {@link Path}, {@link File}, {@link URL}, {@link URI} or
* {@link CharSequence} instance. If no extension is found, returns an empty string. If the given
* object is of unknown type, return {@code null}.
*
* @param input An instance of one of the above-cited types, or {@code null}.
* @return The extension in the given path, or an empty string if none, or {@code null}
* if the given object is null or of unknown type.
*/
public static String extension(Object input) {
return org.apache.sis.internal.storage.IOUtilities.extension(input);
}
/**
* Extract file name without extension from path object (Usually instance of {@link String},
* {@link File}, {@link URL}, {@link URI} or {@link Path}).
*
* @param path candidate path object
* @return path file name without extension or {@code null} if input object can't be processed as {@link Path}.
*/
public static String filenameWithoutExtension(Object path) {
final boolean isPath = canProcessAsPath(path);
if (!isPath) {
return null;
}
try {
return filenameWithoutExtension(toPath(path));
} catch (IOException e) {
//silent exception
return null;
}
}
/**
* Extract file name without extension from a {@link Path}.
* If path file name doesn't have an extension, full file name String
* will be returned.
*
* @param path path to process
* @return file name without extension.
* @throws NullPointerException if input path is {@code null}.
*/
public static String filenameWithoutExtension(Path path) {
ArgumentChecks.ensureNonNull("path", path);
final String fileName = path.getFileName().toString();
final int dot = fileName.lastIndexOf('.');
if (dot > 0) {
return fileName.substring(0, dot);
}
return fileName;
}
/**
* Changes the extension of the given {@link String}, {@link File}, {@link URL}, {@link URI}
* {@link Path} argument. If the argument is not recognized, returns {@code null}.
* If the result of this method is an object equals to {@code path}, then the
* {@code path} instance is returned.
* <p>
* Note that this method converts {@link URI} objects to {@link URL}.
*
* @param path The path as a {@link String}, {@link File}, {@link URL}, {@link URI} or {@link Path}.
* @param extension The new extension, without leading dot.
* @return The path with the new extension, or {@code null} if the given object has
* not been recognized (including {@code null} path).
* @throws MalformedURLException If the given object is an {@link URI} or {@link URL},
* and changing the extension does not result in a valid URL.
*
* @since 3.07
*/
public static Object changeExtension(final Object path, final String extension) throws MalformedURLException, IOException {
final boolean isPath = canProcessAsPath(path);
if (!isPath) {
//special case of CharSequence and String
if (path instanceof CharSequence) {
String pathStr = (String) path;
int dotIdx = pathStr.lastIndexOf('.');
if (dotIdx > 0) {
pathStr = pathStr.substring(0, dotIdx);
}
return pathStr + "." + extension;
}
return null;
}
Path realPath = toPath(path);
final Path outPath = changeExtension(realPath, extension);
if (path instanceof Path) {
return outPath;
} else if (path instanceof String) {
return outPath.toString();
} else if (path instanceof File) {
return outPath.toFile();
} else if (path instanceof URL || path instanceof URI) {
return outPath.toUri().toURL();
}
return null;
}
/**
* Changes the extension of the given {@link Path} argument.
*
* @param path The path as {@link Path}.
* @param extension The new extension, without leading dot.
* @return The path with the new extension.
*/
public static Path changeExtension(final Path path, final String extension) {
final String previousExt = org.apache.sis.internal.storage.IOUtilities.extension(path);
if ((previousExt == null && extension == null) || (previousExt != null && previousExt.equals(extension))) {
return path;
}
final String siblingName = filenameWithoutExtension(path) +'.'+ extension;
return path.resolveSibling(siblingName);
}
/**
* Opens an input stream from the given {@link String}, {@link File}, {@link URL}, {@link URI},
* {@link Path} or {@link InputStream}. The stream will not be buffered, and is not required to support the mark or
* reset methods.
* <p>
* It is the caller responsibility to close the given stream. This method does not accept
* pre-existing streams because they would usually require a different handling by the
* caller (e.g. in many case, the caller will not want to close such pre-existing streams).
*
* @param resource The file to open,
* @return The input stream for the given resource.
* @throws IOException If an error occurred while opening the given file or input resource is not supported.
*/
public static InputStream open(Object resource) throws IOException {
return open(resource, READ);
}
/**
* Opens an input stream from the given {@link String}, {@link File}, {@link URL}, {@link URI},
* {@link Path} or {@link InputStream}. The stream will not be buffered, and is not required to support the mark or
* reset methods.
* <p>
* It is the caller responsibility to close the given stream. This method does not accept
* pre-existing streams because they would usually require a different handling by the
* caller (e.g. in many case, the caller will not want to close such pre-existing streams).
*
* @param resource The file to open,
* @param options open options
* @return The input stream for the given resource.
* @throws IOException If an error occurred while opening the given file or input resource is not supported.
*/
public static InputStream open(Object resource, OpenOption... options) throws IOException {
ArgumentChecks.ensureNonNull("resource", resource);
if (resource instanceof InputStream) {
return (InputStream) resource;
}
if (!canProcessAsPath(resource)) {
throw new IOException("Can not handle input type : " + resource.getClass());
}
try {
Path realPath = toPath(resource);
return newInputStream(realPath, options);
} catch (IOException e) {
/*
An IOException is also thrown if toPath catch a FileSystemNotFoundException (http case)
Try with URL.
Typical case when resource input is an URL not supported as FileSystem
Path conversion will fail
*/
URL url = null;
if (resource instanceof URL) {
url = (URL) resource;
}
if (resource instanceof URI) {
URI uri = (URI) resource;
try {
url = uri.toURL();
} catch (MalformedURLException e2) {
e.addSuppressed(e2);
}
}
if (url != null) {
return url.openStream();
}
throw e;
}
}
/**
* Opens a reader from the given {@link String}, {@link File}, {@link URL}, {@link URI} ,
* {@link Path} or {@link InputStream}. The character encoding is assumed ISO-LATIN-1.
*
* @param path The file to open, as a {@link String}, {@link File}, {@link URL} or {@link URI}.
* @return The buffered reader for the given file.
* @throws IOException If an error occurred while opening the given file.
* @throws ClassCastException If the given object is not a known type.
*/
public static LineNumberReader openLatin(final Object path) throws IOException {
return new LineNumberReader(new InputStreamReader(open(path), "ISO-8859-1"));
}
/**
* Opens an output stream from the given {@link String}, {@link File}, {@link URL},
* {@link URI}, {@link Path} or {@link OutputStream}.
*
* @param resource The resource to open,
* @return The output stream for the given file.
* @throws IOException If an error occurred while opening the given file or input resource is not supported.
*
* @since 3.07
*/
public static OutputStream openWrite(Object resource) throws IOException {
return openWrite(resource, CREATE, WRITE);
}
/**
* Opens an output stream from the given {@link String}, {@link File}, {@link URL},
* {@link URI}, {@link Path} or {@link OutputStream}.
*
* @param resource The resource to open,
* @param options open options
* @return The output stream for the given file.
* @throws IOException If an error occurred while opening the given file or input resource is not supported.
*/
public static OutputStream openWrite(Object resource, OpenOption... options) throws IOException {
ArgumentChecks.ensureNonNull("resource", resource);
if (resource instanceof OutputStream) {
return (OutputStream) resource;
}
if (!canProcessAsPath(resource)) {
throw new IOException("Can not handle input type : " + resource.getClass());
}
try {
Path realPath = toPath(resource);
return newOutputStream(realPath, options);
} catch (IOException e) {
/*
Try with URL.
Typical case when resource input is an URL not supported as FileSystem
Path conversion will fail
*/
URL url = null;
if (resource instanceof URL) {
url = (URL) resource;
}
if (resource instanceof URI) {
URI uri = (URI) resource;
try {
url = uri.toURL();
} catch (MalformedURLException e2) {
e.addSuppressed(e2);
}
}
if (url != null) {
URLConnection connection = url.openConnection();
connection.setDoOutput(true);
return connection.getOutputStream();
}
throw e;
}
}
/**
* Closes the given stream if it is closeable, or do nothing otherwise. Closeable
* objects are any instance of {@link Closeable}.
*
* @param stream The object to close if it is closeable.
* @throws IOException If an error occurred while closing the stream.
*
* @since 3.07
*/
public static void close(final Object stream) throws IOException {
if (stream instanceof Closeable) {
((Closeable) stream).close();
}
// On JDK6, we needed an explicit check for ImageInputStream.
// Since JDK7, this is not needed anymore.
}
/**
* Reads a row of a matrix and stores the values in the given buffer.
*
* @param in The input stream.
* @param grid Where to store the values.
* @param offset Index of the first value to write in {@code grid}.
* @param numCol Expected number of columns.
* @throws IOException if an error occurred while reading the row.
*/
public static void readMatrixRow(final BufferedReader in, final double[] grid,
final int offset, final int numCol) throws IOException
{
final String line = in.readLine();
if (line == null) {
throw new EOFException(Errors.format(Errors.Keys.EndOfDataFile));
}
final StringTokenizer tokens = new StringTokenizer(line);
for (int i=0; i<numCol; i++) {
if (!tokens.hasMoreTokens()) {
throw new ContentFormatException(Errors.format(
Errors.Keys.LineTooShort_2, i, numCol));
}
final String token = tokens.nextToken();
final double value;
try {
value = Double.parseDouble(token);
} catch (NumberFormatException e) {
throw new ContentFormatException(concatenate(
Errors.format(Errors.Keys.UnparsableNumber_1, token), e), e);
}
grid[offset + i] = value;
}
if (tokens.hasMoreElements()) {
throw new ContentFormatException(Errors.format(Errors.Keys.LineTooLong_3,
numCol+tokens.countTokens(), numCol, tokens.nextToken()));
}
}
/**
* Reads a row of a matrix and stores the values in the given buffer.
*
* @param in The input stream.
* @param grid Where to store the values.
* @param offset Index of the first value to write in {@code grid}.
* @param numCol Expected number of columns.
* @throws IOException if an error occurred while reading the row.
*/
public static void readMatrixRow(final BufferedReader in, final float[] grid,
final int offset, final int numCol) throws IOException
{
final String line = in.readLine();
if (line == null) {
throw new EOFException(Errors.format(Errors.Keys.EndOfDataFile));
}
final StringTokenizer tokens = new StringTokenizer(line);
for (int i=0; i<numCol; i++) {
if (!tokens.hasMoreTokens()) {
throw new ContentFormatException(Errors.format(
Errors.Keys.LineTooShort_2, i, numCol));
}
final String token = tokens.nextToken();
final float value;
try {
value = Float.parseFloat(token);
} catch (NumberFormatException e) {
throw new ContentFormatException(concatenate(
Errors.format(Errors.Keys.UnparsableNumber_1, token), e), e);
}
grid[offset + i] = value;
}
if (tokens.hasMoreElements()) {
throw new ContentFormatException(Errors.format(Errors.Keys.LineTooLong_3,
numCol+tokens.countTokens(), numCol, tokens.nextToken()));
}
}
/**
* Concatenates the given message with the message of the given exception, if any.
* This is used when an exception is catch and rethrow, in order to provide more
* useful message.
*/
private static String concatenate(String message, final Exception exception) {
final String cause = exception.getLocalizedMessage();
if (cause != null) {
message = message + ' ' + cause;
}
return message;
}
/**
* Copies the content from the given input stream to the given output stream.
* This method does not close the given streams.
*
* @param input The source of bytes to copy.
* @param output The destination where to copy.
* @throws IOException If an error occurred while performing the copy operation.
*
* @since 3.20
*/
public static void copy(final InputStream input, final OutputStream output) throws IOException {
final byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = input.read(buffer)) >= 0) {
output.write(buffer, 0, bytesRead);
}
}
/**
* Copy content behind source Path into target Path.
* If source Path is a directory, all his content will be copied recursively.
*
* @param sourcePath source to copy, can be a file or a folder
* @param targetPath target where source will be copied
* @param copyOption optional copy option
* @throws IOException
*/
public static void copy(Path sourcePath, Path targetPath, CopyOption... copyOption) throws IOException {
ArgumentChecks.ensureNonNull("sourcePath", sourcePath);
ArgumentChecks.ensureNonNull("targetPath", targetPath);
if (isDirectory(sourcePath)) {
Files.walkFileTree(sourcePath, new CopyFileVisitor(targetPath, copyOption));
} else {
Files.copy(sourcePath, targetPath, copyOption);
}
}
/**
* This method delete recursively a file or a folder.
*
* @param root The File or directory to delete.
*/
public static void deleteRecursively(final Path root) throws IOException {
ArgumentChecks.ensureNonNull("root", root);
if (Files.exists(root)) {
if (Files.isRegularFile(root)) {
Files.deleteIfExists(root);
} else {
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.deleteIfExists(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.deleteIfExists(dir);
return FileVisitResult.CONTINUE;
}
});
}
}
}
/**
* This method delete recursively a file or directory behind a Path without raising an exception if delete failed.
* If an exception occurs, message will be logged as debug.
*
* TODO create asyncDeleteSilently to delete recursively a file/folder in a separate thread
*
* @param path file or folder to delete
* @return delete status. {@code true} if delete succeed, {@code false} otherwise
*/
public static boolean deleteSilently(Path path) {
try {
deleteRecursively(path);
return true;
} catch (IOException e) {
LOGGER.log(Level.FINER, e.getLocalizedMessage(), e);
return false;
}
}
/**
* Append the specified text at the end of the File.
*
* @param text The text to append to the file.
* @param filePath The url file.
*
* @throws IOException if the file does not exist or cannot be read.
*/
public static void appendToFile(final String text, final Path filePath) throws IOException {
try (BufferedWriter output = Files.newBufferedWriter(filePath, UTF8_CHARSET, APPEND)) {
output.newLine();
output.write(text);
output.flush();
}
}
/**
* Empty a file.
*
* @param filePath The url file.
*
* @throws IOException if the file does not exist or cannot be read.
*/
public static void emptyFile(final Path filePath) throws IOException {
if (Files.exists(filePath)) {
Files.delete(filePath);
}
Files.createFile(filePath);
}
/**
* Traverse a directory an return children files with depth 0.
* Result of this method can result of an OutOfMemory if scanned
* folder contains very large number of files.
*
* @param directory input Path, should be a directory
* @return children Path
* @throws IllegalArgumentException if input Path is not a directory
* @throws IOException if an error occurs during directory scanning
*/
public static List<Path> listChildren(Path directory) throws IllegalArgumentException, IOException {
return listChildren(directory, "*");
}
/**
* Traverse a directory an return children files with depth 0.
* Result of this method can result of an OutOfMemory if scanned
* folder contains very large number of files.
*
* @param directory input Path, should be a directory
* @param glob glob filter
* @return children Path
* @throws IllegalArgumentException if input Path is not a directory
* @throws IOException if an error occurs during directory scanning
*/
public static List<Path> listChildren(Path directory, String glob) throws IllegalArgumentException, IOException {
if (!Files.isDirectory(directory)) {
throw new IllegalArgumentException("Input Path is not a directory or doesn't exist");
}
if (glob == null || glob.isEmpty()) {
glob = "*";
}
final List<Path> children = new LinkedList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory, glob)) {
for (Path child : stream) {
children.add(child);
}
}
//asc sort
Collections.sort(children);
return children;
}
/**
* Read the contents of a file into String using UTF-8 encoding.
*
* @param filePath the file path
* @return The file contents as string
* @throws IOException if the file does not exist or cannot be read.
*/
public static String toString(final Path filePath) throws IOException {
return toString(filePath, UTF8_CHARSET);
}
/**
* Read the contents of a file into String using specified encoding.
*
* @param filePath the file path
* @param encoding encoding of file
* @return The file contents as string
* @throws IOException if the file does not exist or cannot be read.
*/
public static String toString(final Path filePath, final Charset encoding) throws IOException {
final List<String> lines = Files.readAllLines(filePath, encoding);
final StringBuilder sb = new StringBuilder();
for (String line : lines) {
sb.append(line).append('\n');
}
return sb.toString();
}
/**
* Read the contents of a stream into String using UTF-8 encoding and close the Stream.
*
* @param stream input steam
* @return The file contents as string
* @throws IOException if the file does not exist or cannot be read.
*/
public static String toString(final InputStream stream) throws IOException {
return toString(stream, UTF8_CHARSET);
}
/**
* Read the contents of a stream into String with specified encoding and close the Stream.
*
* @param stream input steam
* @return The file contents as string
* @throws IOException if the file does not exist or cannot be read.
*/
public static String toString(final InputStream stream, final Charset encoding) throws IOException {
final StringBuilder sb = new StringBuilder();
try (BufferedReader br = new BufferedReader(new InputStreamReader(stream, encoding))) {
String line;
while ((line = br.readLine()) != null) {
sb.append(line).append('\n');
}
} finally {
stream.close();
}
return sb.toString();
}
/**
* Write the contents of a string into path using UTF-8 encoding.
*
* @param content string to write
* @param outputPath the Path to write into.
* @throws IOException if the file does not exist or cannot be read.
*/
public static void writeString(final String content, final Path outputPath) throws IOException {
writeString(content, outputPath, UTF8_CHARSET);
}
/**
* Write the contents of a string into path.
*
* @param content string to write
* @param outputPath the Path to write into.
* @param encoding encoding of output file.
* @throws IOException if the file does not exist or cannot be read.
*/
public static void writeString(final String content, final Path outputPath, final Charset encoding) throws IOException {
ArgumentChecks.ensureNonNull("content", content);
ArgumentChecks.ensureNonNull("outputPath", outputPath);
try (BufferedWriter bw = Files.newBufferedWriter(outputPath, encoding, CREATE, WRITE, TRUNCATE_EXISTING)) {
bw.write(content);
}
}
/**
* Write the content of an {@link InputStream} into a Path
* .
* @param stream InputStream to write
* @param outputPath the Path to write into.
* @throws IOException
*/
public static void writeStream (final InputStream stream, final Path outputPath) throws IOException {
ArgumentChecks.ensureNonNull("stream", stream);
ArgumentChecks.ensureNonNull("outputPath", outputPath);
try (OutputStream outputStream = Files.newOutputStream(outputPath, CREATE, WRITE, TRUNCATE_EXISTING)) {
copy(stream, outputStream);
}
}
/**
* Load the properties from a properties file.
* If the file does not exist it will be created and an empty Properties object will be return.
*
* @param path to a properties file.
* @return a Properties Object.
*/
public static Properties getPropertiesFromFile(final Path path) throws IOException {
ArgumentChecks.ensureNonNull("path", path);
final Properties prop = new Properties();
try (InputStream in = Files.newInputStream(path)) {
prop.load(in);
}
return prop;
}
/**
* Store an Properties object "prop" into the specified File.
* If file behind input path already exist, content will be truncated first.
*
* @param prop A properties Object.
* @param path A file.
* @param comment Comment in output property file. Can be {@code null}.
* @throws IOException
*/
public static void storeProperties(final Properties prop, final Path path, String comment) throws IOException {
ArgumentChecks.ensureNonNull("prop", prop);
ArgumentChecks.ensureNonNull("path", path);
//replace content
try (OutputStream out = Files.newOutputStream(path, CREATE, WRITE, TRUNCATE_EXISTING)) {
prop.store(out, (comment != null ? comment : ""));
}
}
/**
* Searches in the Context ClassLoader for the named file and returns it.
*
* @param resource resource path. As package or path format (with '.' or '/' separators)
* @return A file path if it exist or null otherwise.
*/
public static Path getResourceAsPath(String resource) throws URISyntaxException, IOException {
return getResourceAsPath(resource, null);
}
/**
* Searches in the Context ClassLoader for the named file and returns it.
*
* @param resource resource path. As package or path format (with '.' or '/' separators)
* @param classLoader classloader, can be null
* @return A file path if it exist or null otherwise.
*/
public static Path getResourceAsPath(String resource, ClassLoader classLoader) throws URISyntaxException, IOException {
//change package based location to path based
final String extension = org.apache.sis.internal.storage.IOUtilities.extension(resource);
int lastDotIdx = resource.lastIndexOf('.');
if (lastDotIdx > 0) {
resource = resource.substring(0, lastDotIdx);
}
resource = resource.replace('.', '/') ;
if (!extension.isEmpty()) {
resource +="." + extension;
}
if (classLoader == null) {
classLoader = Thread.currentThread().getContextClassLoader();
}
final URL systemResource = classLoader.getResource(resource);
if (systemResource != null) {
URI resourceURI = systemResource.toURI();
if (resourceURI.getScheme().startsWith("jar")) {
//get or create FileSystem if needed (if resource is in another JAR)
FileSystem fileSystem;
try {
fileSystem = FileSystems.getFileSystem(resourceURI);
} catch (FileSystemNotFoundException ex) {
fileSystem = FileSystems.newFileSystem(resourceURI, new HashMap<String, Object>());
}
return fileSystem.getPath(resource);
} else {
return Paths.get(resourceURI);
}
}
return null;
}
/**
* Find and copy resource into a target directory.
*
* @param resource resource path relative to resource directory like "org/geotoolkit/myresource".
* Can target a file or a directory
* @param classLoader {@link ClassLoader} used for resource search.
* (Use current thread ClassLoader if {@code null}).
* @param output Output directory where resource will be copied.
* @param createResourceHierarchy flag to enable/disable creation of resource parent directory in output directory
* e.g.:
* <pre><code>
* IOUtilities.copyResource("org/geotoolkit/myresource", null, Paths.get("directory"), true);
* // will create directory/org/geotoolkit/myresource
* IOUtilities.copyResource("org/geotoolkit/myresource", null, Paths.get("directory"), false);
* // will create directory/myresource
* </code></pre>
* @return Path to extracted resource.
* If resource is a directory, returned Path will target root directory where resource content is copied.
* @throws IOException if copy or file access went wrong
* @throws URISyntaxException an error occurs during resource URI conversion
* @throws IllegalArgumentException if output Path is not a directory
* @throws FileNotFoundException if resource is not found on classloader
*/
public static Path copyResource(String resource, ClassLoader classLoader, Path output, boolean createResourceHierarchy)
throws IOException, URISyntaxException, IllegalArgumentException {
ArgumentChecks.ensureNonNull("resource", resource);
ArgumentChecks.ensureNonNull("output", output);
output = output.toAbsolutePath();
if (!Files.exists(output)) {
Files.createDirectories(output);
}
if (!Files.isDirectory(output)) {
throw new IllegalArgumentException("Output is not a directory");
}
if (classLoader == null) {
classLoader = Thread.currentThread().getContextClassLoader();
}
URL resURL = classLoader.getResource(resource);
if (resURL != null) {
URI resURI = resURL.toURI();
//handle JAR resources
Path resPath;
if (resURI.getScheme().startsWith("jar")) {
FileSystem fileSystem;
try {
fileSystem = FileSystems.getFileSystem(resURI);
} catch (FileSystemNotFoundException ex) {
fileSystem = FileSystems.newFileSystem(resURI, new HashMap<String, Object>());
}
resPath = fileSystem.getPath(resource);
//hack add start / for CopyFileVisitor relativize
if (!resPath.toString().startsWith("/")) {
resPath = resPath.getFileSystem().getPath("/", resPath.toString());
}
} else {
resPath = Paths.get(resURI);
}
String resourceName = resPath.getFileName().toString();
//prepare workspace directory
Path copyDir = output;
if (Files.isDirectory(resPath)) {
copyDir = output.resolve(resourceName);
}
if (createResourceHierarchy) {
if (Files.isDirectory(resPath)) {
copyDir = output.resolve(resource);
} else {
String lastDirectory = resource.substring(0, resource.lastIndexOf('/'));
copyDir = output.resolve(lastDirectory);
}
}
Files.createDirectories(copyDir);
Path outputPath = copyDir;
if (Files.isRegularFile(resPath)) {
outputPath = outputPath.resolve(resourceName);
}
//copy files
Files.walkFileTree(resPath, new CopyFileVisitor(copyDir, StandardCopyOption.REPLACE_EXISTING));
return outputPath;
}
throw new FileNotFoundException("Unable to find resource "+resource+ " into ClassLoader "+classLoader);
}
}