/*
* Copyright 2014 Matthias Braun, Martin Gangl
*
* 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, either version 3 of the License, or (at your option) any
* later version.
*
* 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.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.bges;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.DirectoryStream;
import java.nio.file.FileSystems;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.SerializationUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.StandardSystemProperty;
import com.google.common.io.Files;
import eu.bges.config.KeyNotFoundException;
/**
* Constants and functions for dealing with files and file systems.
*
* @author Matthias Braun
* @author Martin Gangl
*/
public final class IOUtil {
private static final Logger LOG = LoggerFactory.getLogger(IOUtil.class);
/**
* A regular expression matching every string.
*/
private static final String MATCH_ALL = ".*";
/**
* Error message for dealing with IO exceptions. The braces are used by <a
* href="http://www.slf4j.org/">SLF4J</a> to insert the file or directory
* name.
*/
private static final String COULD_NOT_READ_FILE = "IOException while reading {}",
COULD_NOT_OPEN_DIR = "IOException opening directory {}",
COULD_NOT_PARSE_FILE = "Could not parse file {}",
COULD_NOT_WRITE_TO_FILE = "Could not write to file {}";
/**
* The separator between directories is the forward slash (also for
* Windows).
*/
public static final String DIR_SEP = "/";
/**
* Separates lines in files. On UNIX systems, this is "\n"; on Microsoft
* Windows systems this is "\r\n".
*/
public static final String EOL = System.lineSeparator();
/**
* The Eight-bit UCS Transformation Format.
*/
public static final Charset UTF_8 = Charset.forName("UTF-8");
/** This is a utility class not meant to be instantiated by others. */
private IOUtil() {
}
/**
* Appends text to a file. Creates the file and the needed directories if
* they do not exist.
* <p>
* UTF-8 is used for encoding the file's content.
*
* @param txt
* text appended to the file
* @param file
* file that is written to. Is created if it doesn't exist
* @return whether the writing was successful (true if no IOException
* occurred)
*/
public static boolean append(final String txt, final File file) {
boolean success = true;
final String containingDir = file.getParent();
// Make sure the directories leading to the file exist
makeDir(containingDir);
try {
Files.append(txt, file, Charsets.UTF_8);
} catch (final IOException e) {
LOG.error(COULD_NOT_WRITE_TO_FILE, file, e);
success = false;
}
return success;
}
/**
* Converts a byte array to a {@code file}.
*
* @param byteArr
* byte array containing the data
* @param file
* to be written to
* @return true if and only if the given byte array could be written into
* the given {@code file}
*/
public static boolean byteArrayToFile(final byte[] byteArr, final File file) {
boolean success = false;
if (file != null && byteArr != null) {
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(byteArr);
success = true;
} catch (final IOException e) {
LOG.warn(e.getMessage(), e);
}
}
return success;
}
/**
* Deletes a directory and all files and folders in it. If the argument is a
* file, the file is deleted.
*
* @param dirOrFile
* directory or the file to delete
*/
public static void deleteRecursively(final File dirOrFile) {
if (dirOrFile.isDirectory()) {
for (final File file : dirOrFile.listFiles()) {
deleteRecursively(file);
}
}
if (isEmptyDir(dirOrFile) || dirOrFile.isFile()) {
final boolean success = dirOrFile.delete();
if (!success) {
LOG.warn("{} was not deleted successfully", dirOrFile);
}
}
}
/**
* Converts a {@code file} to a byte array.
* <p>
* This will fail if the {@code file} is larger than two gigabytes.
*
* @param file
* whose data is converted to a byte array
* @return a byte array representing the data in the file; null if an error
* occurs
*/
public static byte[] fileToByteArray(final File file) {
byte[] ba = null;
if (file != null) {
try (final FileInputStream is = new FileInputStream(file);) {
ba = new byte[(int) file.length()];
is.read(ba);
} catch (final IOException e) {
LOG.warn(e.getMessage(), e);
}
}
return ba;
}
/**
* Gets the numbers of characters in a {@code file}.
*
* @param file
* {@link File} whose characters are counted
* @return the number of characters in this {@code file}
*/
public static int getCharCount(final File file) {
final String content = read(file);
return content.length();
}
/**
* @return the current directory as a {@code Path}
*/
public static Path getCurrDir() {
final String currDir = StandardSystemProperty.USER_DIR.value();
return FileSystems.getDefault().getPath(currDir, "");
}
/**
* Gets the name of a file from its path.
* <p>
* Also works with paths that don't exist on the file system.
*
* @param filePath
* path to a file or directory
* @return the filename from the path
*/
public static String getFileName(final String filePath) {
final File f = new File(filePath);
return f.getName();
}
/**
* Gets the file name from a {@link URL}.
*
* @param url
* {@link URL} pointing to the file
* @return the filename from the {@code url}
*/
public static String getFileName(final URL url) {
final String file = url.getFile();
return getFileName(file);
}
/**
* Gets the containing directory of a file or directory.
* <p>
* The directory is described as its absolute path in string from.
*
* @param fileOrDir
* the file or directory whose containing directory we we want to
* know
* @return the absolute path of the parent directory of the
* {@code fileOrDir} as a string or an absent {@link Optional} if
* the file doesn't exist or is null
*/
public static Optional<String> getParent(final File fileOrDir) {
String parentDirPath = null;
if (fileOrDir != null && fileOrDir.exists()) {
final File parent = fileOrDir.getAbsoluteFile().getParentFile();
if (parent == null) {
LOG.info("{} exists but has no parent directory.", fileOrDir);
} else {
parentDirPath = parent.getAbsolutePath();
}
}
return Optional.fromNullable(parentDirPath);
}
/**
* Loads a resource and returns it as a {@code File}.
*
* @param path
* path to the resource
* @param caller
* class of the caller
* @return the resource as a {@code File}
* @see Class#getResource(String)
*/
public static File getResource(final String path, final Class<?> caller) {
final URL resourceUrl = caller.getResource(path);
File resource;
if (resourceUrl == null) {
LOG.warn("Did not find resource at {}", path);
resource = new File("");
} else {
resource = new File(resourceUrl.getFile());
}
return resource;
}
/**
* Gets a variable value from a file.
*
* @param key
* name of the variable containing the value
* @param file
* file containing the variable
* @param varPattern
* regex pattern to identify lines with variables. Must remember
* the key and the value as one group each
* @return the value wrapped in an {@link Optional} in case the value wasn't
* found
* @throws KeyNotFoundException
* thrown when the {@code key} is not found in the {@code file}
*/
public static String getVal(final String key, final File file,
final Pattern varPattern) throws KeyNotFoundException {
String value = null;
try {
final List<String> lines = Files.readLines(file, Charsets.UTF_8);
for (final String line : lines) {
final Matcher m = varPattern.matcher(line);
if (m.matches()) {
// The zeroth item in the group is the whole match
final String varName = m.group(1);
final String varVal = m.group(2);
if (varName.equals(key)) {
value = varVal;
break;
}
}
}
} catch (final IOException e) {
LOG.error(COULD_NOT_READ_FILE, file, e);
}
if (value == null) {
throw new KeyNotFoundException(key, file);
} else {
return value;
}
}
/**
* Checks if the file is an empty directory, meaning it doesn't contain any
* files.
* <p>
* Return false if the {@code dir} is not a directory but a file.
*
* @param dir
* directory that may be empty
* @return whether the directory is empty, or false, if a file was passed
*/
public static boolean isEmptyDir(final File dir) {
boolean isEmpty = false;
if (dir.isDirectory()) {
isEmpty = dir.listFiles().length == 0;
}
return isEmpty;
}
/**
* Joins two paths and creates a {@link File} from the combined path.
* <p>
* This does not create a file or directory on the file system. The paths
* are separated by {@value #DIR_SEP}.
*
* @param path1
* first part of the combined path
* @param path2
* second part of the combined path
* @return {@code path1} and {@code path2} combined to a {@link File}
*/
public static File join(final String path1, final String path2) {
final String combinedPath = path1 + DIR_SEP + path2;
return new File(combinedPath);
}
/**
* Gets all file paths within a directory.
*
* @param startDir
* directory to start the listing of file paths
* @param recursive
* whether to descend into subfolders of the {@code startDir}
* @return the file paths in this directory
*/
public static List<Path> listFiles(final Path startDir,
final boolean recursive) {
return listFiles(startDir, recursive, MATCH_ALL);
}
/**
*
* Gets all file paths within a directory.
*
* @param startDir
* directory to start the listing of file paths
* @param recursive
* whether to descend into subfolders of the {@code startDir}
* @param filterRegex
* only return file paths that match this regular expression
*
* @return the file paths contained in {@code startDir}
*/
public static List<Path> listFiles(final Path startDir,
final boolean recursive, final String filterRegex) {
final List<Path> paths = new ArrayList<>();
listFiles(paths, startDir, recursive, filterRegex);
return paths;
}
/**
* Creates an empty directory. All parent directories are created if they
* don't exist.
*
* @param dirPath
* directory path. If it is null, this method returns
* {@code false}.
* @return whether a new directory was created (false if there already
* existed one with the same name)
*/
public static boolean makeDir(final String dirPath) {
boolean newDirWasCreated;
if (dirPath == null) {
newDirWasCreated = false;
} else {
// Return whether the directory is new
newDirWasCreated = new File(dirPath).mkdirs();
}
return newDirWasCreated;
}
/**
* Makes a deep copy from a serializable object.
*
* @param copyObject
* of which a deep copy is made
* @return the deep copy of the object
*/
public static Serializable objectDeepCopy(final Serializable copyObject) {
final OutputStream outputStream = new ByteArrayOutputStream();
SerializationUtils.serialize(copyObject, outputStream);
return (Serializable) outputStream;
}
/**
* Extracts a map of variable names and corresponding values from a file.
*
* @param file
* file to parse containing the variables
* @param pattern
* regular expression that describes a line with a variable name
* and its value. It is expected that the regex remembers the
* variable name as the first group and the variable value as the
* second group
* @return a mapping from the variable name to its value
*/
public static Map<String, String> parseVarsFromFile(final File file,
final Pattern pattern) {
final Map<String, String> varNamesAndValues = new HashMap<>();
try {
final List<String> lines = Files.readLines(file, Charsets.UTF_8);
for (final String line : lines) {
final Matcher m = pattern.matcher(line);
if (m.matches()) {
// The zeroth item in the group is the whole match
final String varName = m.group(1);
final String varVal = m.group(2);
varNamesAndValues.put(varName, varVal);
}
}
} catch (final IOException e) {
LOG.warn(COULD_NOT_PARSE_FILE, file, e);
}
return varNamesAndValues;
}
/**
* Reads a file line by line and returns its contents as a string.
* <p>
* Return the empty string if an {@link IOException} occurred. The character
* set used is {@code UTF-8}.
* <p>
* The lines are separated using the system-dependent {@link #EOL}
* character.
*
* @param file
* {@link File} to read
* @return the contents of the file or the empty string if the file could
* not be read
*/
public static String read(final File file) {
String contents = "";
if (file != null) {
try {
final List<String> lines = Files
.readLines(file, Charsets.UTF_8);
contents = Joiner.on(EOL).join(lines);
} catch (final IOException e) {
LOG.warn(COULD_NOT_READ_FILE, file.getAbsolutePath(), e);
}
}
return contents;
}
/**
* Reads a file line by line and returns them as a list of strings.
* <p>
* Return an empty list if an {@link IOException} occurred. The character
* set used is {@code UTF-8}.
*
* @param file
* {@link File} to read
* @return the contents of the file as a list of strings or an empty list if
* the file could not be read
*/
public static List<String> readLines(final File file) {
final List<String> lines = new ArrayList<>();
try {
lines.addAll(Files.readLines(file, Charsets.UTF_8));
} catch (final IOException e) {
LOG.warn(COULD_NOT_READ_FILE, file.getAbsolutePath(), e);
}
return lines;
}
/**
* Sets a value of a property within a file.
*
* @param property
* name of the property that gets the new value
* @param newVal
* new value the property has after the update
* @param file
* absolute path to the file containing the property
* @param propValPattern
* property/value {@link Pattern} that determines how a property
* and its value look like in the file. It's expected that the
* regex remembers the variable name as its first group and the
* variable value as its second
* @return whether the update was successful (the property must exist in the
* file)
*/
public static boolean setVal(final String property, final String newVal,
final File file, final Pattern propValPattern) {
// The lines of the updated file
final List<String> newLines = new ArrayList<>();
boolean propWasFound = false;
try {
final List<String> lines = Files.readLines(file, Charsets.UTF_8);
for (String line : lines) {
final Matcher m = propValPattern.matcher(line);
if (m.matches()) {
// The zeroth item in the group is the whole match
final String name = m.group(1);
final String oldVal = m.group(2);
if (property.equals(name)) {
// Update the value of the variable
final String strVal = String.valueOf(newVal);
line = line.replaceFirst(oldVal, strVal);
propWasFound = true;
}
}
newLines.add(line);
}
} catch (final IOException e) {
LOG.error(COULD_NOT_READ_FILE, file, e);
}
// Write the -maybe changed- contents to file
final String newFileContent = Joiner.on(EOL).join(newLines);
write(newFileContent, file);
return propWasFound;
}
/**
* Converts a string representing a file or directory to a {@link Path}.
*
* @param pathStr
* file path as a string
* @return file or directory as a {@link Path}
* @throws InvalidPathException
* thrown on Windows when {@code pathStr} does not represent a
* valid path (e.g., because it contains invalid characters like
* ?, |, or *)
*/
public static Path toPath(final String pathStr) {
final File pathAsFile = new File(pathStr);
final URI pathUri = pathAsFile.toURI();
return Paths.get(pathUri);
}
/**
* Writes a string to a file. Overwrites previous content. Creates the file
* and the needed directories if they do not exist.
* <p>
* UTF-8 is used for encoding the file's content.
*
* @param txt
* text that is written to the file
* @param file
* file that is written to. Is created if it doesn't exist
* @return whether the writing was successful (true if no IOException
* occurred)
*/
public static boolean write(final String txt, final File file) {
boolean success = true;
final String containingDir = file.getParent();
// Make sure the directories leading to the file exist
makeDir(containingDir);
try {
Files.write(txt, file, Charsets.UTF_8);
} catch (final IOException e) {
LOG.error(COULD_NOT_WRITE_TO_FILE, file, e);
success = false;
}
return success;
}
/**
* Gets all file paths in a directory. Calls itself to descend into
* directories.
*
* @param paths
* file paths of the directory are added to this list
* @param currDir
* current directory we are in
* @param recursive
* whether to visit other directories recursively
* @param regexFilter
* only add a file if it matches this regular expression
*/
private static void listFiles(final List<Path> paths, final Path currDir,
final boolean recursive, final String regexFilter) {
if (currDir.toFile().isDirectory()) {
try (DirectoryStream<Path> stream = java.nio.file.Files
.newDirectoryStream(currDir)) {
for (final Path path : stream) {
final String fileName = path.toString();
if (fileName.matches(regexFilter)) {
paths.add(path);
}
if (recursive) {
listFiles(paths, path, recursive, regexFilter);
}
}
} catch (final IOException e) {
LOG.error(COULD_NOT_OPEN_DIR, currDir, e);
}
}
}
}