/*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* Licensed 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.
*/
package com.cinchapi.concourse.util;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.Watchable;
import java.util.AbstractList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.imca_cat.pollingwatchservice.PollingWatchService;
import com.cinchapi.common.base.CheckedExceptions;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.sun.nio.file.SensitivityWatchEventModifier;
/**
* Generic file utility methods that compliment and expand upon those found in
* {@link java.nio.file.Files} and {@link com.google.common.io.Files}.
*
* @author Jeff Nelson
*/
public class FileOps {
/**
* Write the String {@code content} to the end of the {@code file},
* preserving anything that was previously there.
*
* @param content the data to write
* @param file the path to the file
*/
public static void append(String content, String file) {
try {
Files.append(content, new File(file), StandardCharsets.UTF_8);
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Cause the current thread to block while waiting for a change to
* {@code file}.
* <p>
* Because of limitations of most underlying file systems, this method can
* only guarantee changes that occur at least 1 second after this method is
* invoked. For changes that occur less than 1 second of method invocation,
* the method will return immediately; however, there is a chance that such
* a return is indicative of a false positive case where the file changed
* before this method was invoked, but within the same second of the
* invocation.
* </p>
* <p>
* If protection against that kind of false positive is important, the
* caller should check the contents of the underlying file is this method
* returns immediately.
* </p>
*
* @param file the path to a regular file
*/
public static void awaitChange(String file) {
try {
awaitChangeInterruptibly(file);
}
catch (InterruptedException e) {
throw CheckedExceptions.throwAsRuntimeException(e);
}
}
/**
* Cause the current thread to block while waiting for a change to
* {@code file}.
* <p>
* Because of limitations of most underlying file systems, this method can
* only guarantee changes that occur at least 1 second after this method is
* invoked. For changes that occur less than 1 second of method invocation,
* the method will return immediately; however, there is a chance that such
* a return is indicative of a false positive case where the file changed
* before this method was invoked, but within the same second of the
* invocation.
* </p>
* <p>
* If protection against that kind of false positive is important, the
* caller should check the contents of the underlying file is this method
* returns immediately.
* </p>
*
* @param file the path to a regular file
*/
public static void awaitChangeInterruptibly(String file)
throws InterruptedException {
if(!IS_WATCH_SERVICE_SETUP) {
try {
// Add a PollingWatchService to use as a backup in case the
// default watch service is causing issues (i.e. on Linux the
// max number of inotify watches may be reached, in which case
// we can use the backup as a fail safe.)
PollingWatchService pollingWatchService = new PollingWatchService(
Runtime.getRuntime().availableProcessors(), 1000,
TimeUnit.MILLISECONDS);
pollingWatchService.start();
FILE_CHANGE_WATCHERS.add(pollingWatchService);
FILE_CHANGE_WATCHERS
.add(FileSystems.getDefault().newWatchService());
}
catch (Exception e) {
// NOTE: Cannot re-throw the exception because it will prevent
// the class from being loaded...
e.printStackTrace();
}
FILE_CHANGE_WATCHERS.forEach((watcher) -> {
setupWatchService(watcher);
});
IS_WATCH_SERVICE_SETUP = true;
}
long methodStartTime = System.currentTimeMillis();
methodStartTime = TimeUnit.SECONDS.convert(methodStartTime,
TimeUnit.MILLISECONDS);
Path path = Paths.get(expandPath(file));
Preconditions.checkArgument(java.nio.file.Files.isRegularFile(path));
String mutex = path.toString().intern();
synchronized (mutex) {
Watchable parent = path.getParent().toAbsolutePath();
if(!REGISTERED_WATCHER_PATHS.contains(parent)) {
for (int i = 0; i < FILE_CHANGE_WATCHERS.size(); ++i) {
WatchService watcher = FILE_CHANGE_WATCHERS.get(i);
try {
if(watcher instanceof PollingWatchService) {
((PollingWatchService) watcher).register(
(Path) parent, WATCH_EVENT_KINDS,
WATCH_EVENT_MODIFIERS);
}
else {
parent.register(watcher, WATCH_EVENT_KINDS,
WATCH_EVENT_MODIFIERS);
}
break;
}
catch (IOException e) {
// If an error occurs while trying to register a
// path with a watch service, cycle through the list
// in order to see if we can find one that will
// accept it.
if(i < FILE_CHANGE_WATCHERS.size()) {
continue;
}
else {
throw CheckedExceptions.throwAsRuntimeException(e);
}
}
}
REGISTERED_WATCHER_PATHS.add(parent);
}
try {
long modified = java.nio.file.Files.getLastModifiedTime(path)
.toMillis();
modified = TimeUnit.SECONDS.convert(modified,
TimeUnit.MILLISECONDS);
if(modified >= methodStartTime) {
// A modification occurred after the method start time,
// so return in order to allow the caller to move on
// since the file did indeed change after method
// invocation
return;
}
else {
mutex.wait();
}
}
catch (IOException e) {
throw CheckedExceptions.throwAsRuntimeException(e);
}
}
}
/**
* Expand the given {@code path} so that it contains completely normalized
* components (e.g. ".", "..", and "~" are resolved to the correct absolute
* paths).
*
* @param path
* @return the expanded path
*/
public static String expandPath(String path) {
return expandPath(path, null);
}
/**
* Expand the given {@code path} so that it contains completely normalized
* components (e.g. ".", "..", and "~" are resolved to the correct absolute
* paths).
*
* @param path
* @param cwd
* @return the expanded path
*/
public static String expandPath(String path, String cwd) {
path = path.replaceAll("~", USER_HOME);
Path base = com.google.common.base.Strings.isNullOrEmpty(cwd)
? BASE_PATH : FileSystems.getDefault().getPath(cwd);
return base.resolve(path).normalize().toString();
}
/**
* Return the home directory of the user of the parent process for this JVM.
*
* @return the home directory
*/
public static String getUserHome() {
return USER_HOME;
}
/**
* Get the working directory of this JVM, which is the directory from which
* the process is launched.
*
* @return the working directory
*/
public static String getWorkingDirectory() {
return WORKING_DIRECTORY;
}
/**
* Return {@code true} if the specified {@code path} is that of a directory
* and not a flat file.
*
* @param path the path to check
* @return {@code true} if the {@code path} is that of a directory
*/
public static boolean isDirectory(String path) {
return java.nio.file.Files.isDirectory(Paths.get(path));
}
/**
* Create the directories named by {@code path}, including any necessary,
* but nonexistent parent directories.
* <p>
* <strong>NOTE:</strong> If this operation fails, it may have succeeded in
* creating some of the necessary parent directories.
* </p>
*
* @param path the path of directories to create
* @return {@code true} if entire {@code path} was created
*/
public static void mkdirs(String path) {
try {
java.nio.file.Files.createDirectories(Paths.get(path));
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Read the contents of {@code file} into a UTF-8 string.
*
* @param file
* @return the file content
*/
public static String read(String file) {
try {
return com.google.common.io.Files.toString(new File(file),
StandardCharsets.UTF_8);
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Return a list that lazily accumulates lines in the underlying
* {@code file}.
* <p>
* This method is really just syntactic sugar for reading lines from a file,
* so the returned list doesn't actually allow any operations other than
* forward iteration.
* </p>
*
* @param file
* @return a "list" of lines in the file
*/
public static List<String> readLines(final String file) {
return readLines(file, null);
}
/**
* Return a list that lazily accumulates lines in the underlying
* {@code file}.
* <p>
* This method is really just syntactic sugar for reading lines from a file,
* so the returned list doesn't actually allow any operations other than
* forward iteration.
* </p>
*
* @param file
* @param cwd
* @return a "list" of lines in the file
*/
public static List<String> readLines(final String file, String cwd) {
final String rwd = MoreObjects.firstNonNull(cwd, WORKING_DIRECTORY);
return new AbstractList<String>() {
@Override
public String get(int index) {
throw new UnsupportedOperationException();
}
@Override
public Iterator<String> iterator() {
return new ReadOnlyIterator<String>() {
String line = null;
BufferedReader reader;
{
try {
reader = new BufferedReader(new FileReader(
FileOps.expandPath(file, rwd)));
line = reader.readLine();
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
@Override
public boolean hasNext() {
return this.line != null;
}
@Override
public String next() {
String result = line;
try {
line = reader.readLine();
if(line == null) {
reader.close();
}
return result;
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
};
}
@Override
public int size() {
int size = 0;
Iterator<String> it = iterator();
while (it.hasNext()) {
size += 1;
it.next();
}
return size;
}
};
}
/**
* Create a temporary directory with the specified {@code prefix}.
*
* @param prefix the directory name prefix
* @return the path to the temporary directory
*/
public static String tempDir(String prefix) {
try {
return java.nio.file.Files.createTempDirectory(prefix).toString();
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Create a temporary file that is likely to be deleted some time after this
* JVM terminates, but definitely not before.
*
* @return the absolute path where the temp file is stored
*/
public static String tempFile() {
return tempFile("cnch", null);
}
/**
* Create a temporary file that is likely to be deleted some time after this
* JVM terminates, but definitely not before.
*
* @param prefix the prefix for the temp file
* @return the absolute path where the temp file is stored
*/
public static String tempFile(String prefix) {
return tempFile(prefix, null);
}
/**
* Create a temporary file that is likely to be deleted some time after this
* JVM terminates, but definitely not before.
*
* @param prefix the prefix for the temp file
* @param suffix the suffix for the temp file
* @return the absolute path where the temp file is stored
*/
public static String tempFile(String prefix, String suffix) {
return tempFile(null, prefix, suffix);
}
/**
* Create a temporary file that is likely to be deleted some time after this
* JVM terminates, but definitely not before.
*
* @param dir the absolute path to the directory in which the temp file
* should be created
* @param prefix the prefix for the temp file
* @param suffix the suffix for the temp file
* @return the absolute path where the temp file is stored
*/
public static String tempFile(String dir, String prefix, String suffix) {
prefix = prefix == null ? "cnch" : prefix;
prefix = prefix.trim();
while (prefix.length() < 3) { // java enforces prefixes of >= 3
// characters
prefix = prefix + Random.getString().charAt(0);
}
try {
if(dir != null) {
FileOps.mkdirs(dir);
}
return dir == null
? java.nio.file.Files.createTempFile(prefix, suffix)
.toAbsolutePath().toString()
: java.nio.file.Files
.createTempFile(Paths.get(dir), prefix, suffix)
.toAbsolutePath().toString();
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Create an empty file or update the last updated timestamp on the same as
* the unix command of the same name.
*
* @param file the path of the file to touch
* @return the value of {@code file} in case it needs to be passed to a
* super constructor
*/
public static String touch(String file) {
try {
Files.touch(new File(file));
return file;
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* A shortcut for getting a {@link URL} instance from a file path.
*
* @param path the path to the file or directory
* @return the {@link URL} that corresponds to {@code path}
*/
public static URL toURL(String path) {
try {
return new File(path).toURI().toURL();
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Write the String {@code content} to the {@code file}, overwriting
* anything that was previously there.
*
* @param content the data to write
* @param file the path to the file
*/
public static void write(String content, String file) {
try {
Files.write(content, new File(file), StandardCharsets.UTF_8);
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Configure the watch {@code service} to notify listeners about changes to
* any of the {@link #REGISTERED_WATCHER_PATHS} that have been sent to the
* service.
*
* @param service the {@link WatchService} to setup
*/
private static void setupWatchService(WatchService service) {
Thread t = new Thread(() -> {
try {
while (true) {
WatchKey key;
try {
key = service.take();
for (WatchEvent<?> event : key.pollEvents()) {
Path parent = (Path) key.watchable();
WatchEvent.Kind<?> kind = event.kind();
if(kind == StandardWatchEventKinds.ENTRY_MODIFY) {
Path abspath = parent
.resolve((Path) event.context())
.toAbsolutePath();
String sync = abspath.toString().intern();
synchronized (sync) {
sync.notifyAll();
}
}
}
key.reset();
}
catch (InterruptedException e) {
throw Throwables.propagate(e);
}
}
}
finally {
try {
service.close();
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
});
t.setName("watch-service-daemon-" + service.getClass());
t.setDaemon(true);
t.start();
}
/**
* A flag that indicates whether the watch service(s) have been setup.
*/
private static boolean IS_WATCH_SERVICE_SETUP = false;
/**
* A service that watches directories for operations on files.
* <p>
* Java's {@link WatchService} API is designed to handle directories instead
* of individual files. So, when {@link #awaitChange(String)} is called, we
* register the parent path (e.g. the housing directory) with the watch
* service and check the {@link WatchEvent watch event's}
* {@link WatchEvent#context() context} to determine whether an individual
* file has changed.
* </p>
*/
private static List<WatchService> FILE_CHANGE_WATCHERS = Lists
.newArrayListWithCapacity(2);
/**
* A collection of {@link Watchable} paths that have already been registered
* with the {@link #FILE_CHANGE_WATCHER}.
*/
@VisibleForTesting
protected static Set<Watchable> REGISTERED_WATCHER_PATHS = Sets
.newConcurrentHashSet();
/**
* The user's home directory, which is used to expand path names with "~"
* (tilde).
*/
private static String USER_HOME = System.getProperty("user.home");
/**
* The kind of notifications we care about from the {@link WatchService}s.
*/
private static WatchEvent.Kind<?>[] WATCH_EVENT_KINDS = {
StandardWatchEventKinds.ENTRY_MODIFY };
/**
* The modifiers that are supplied when registering a path with one the
* {@link WatchService}s.
*/
private static SensitivityWatchEventModifier[] WATCH_EVENT_MODIFIERS = {
SensitivityWatchEventModifier.HIGH };
/**
* The working directory from which the current JVM process was launched.
*/
private static String WORKING_DIRECTORY = System.getProperty("user.dir");
/**
* The base path that is used to resolve and normalize other relative paths.
*/
private static Path BASE_PATH = FileSystems.getDefault()
.getPath(WORKING_DIRECTORY);
protected FileOps() {/* noop */}
}