/*
* 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.server.io;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.DirectoryNotEmptyException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Collections;
import java.util.Iterator;
import java.util.Set;
import com.cinchapi.concourse.util.FileOps;
import com.cinchapi.concourse.util.Logger;
import com.cinchapi.concourse.util.ReadOnlyIterator;
import com.google.common.base.Throwables;
import com.google.common.collect.Sets;
import static com.google.common.base.Preconditions.checkState;
/**
* Interface to the underlying filesystem which provides methods to perform file
* based operations without having to deal with the annoyance of checked
* exceptions or the awkward {@link Path} API. Using this class will help
* produce more streamlined and readable code.
*
* <p>
* This class makes a lot of assumptions that are particular to Concourse
* Server, so it isn't suitable as a strictly generic utility. {@link FileOps}
* is a parent class that does contain file based utility functions that are
* applicable in situations outside of Concourse Server.
*
* @author Jeff Nelson
*/
public final class FileSystem extends FileOps {
/**
* Close the {@code channel} without throwing a checked exception. If, for
* some reason, this can't be done the underlying IOException will be
* re-thrown as a runtime exception.
*
* @param channel
*/
public static void closeFileChannel(FileChannel channel) {
try {
channel.close();
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Copy all the bytes {@code from} one file to {to} another.
*
* @param from
* @param to
*/
public static void copyBytes(String from, String to) {
try {
Files.copy(Paths.get(from), Files.newOutputStream(Paths.get(to)));
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Delete {@code directory}. If files are added to the directory while its
* being deleted, this method will make a best effort to delete those files
* as well.
*
* @param directory
*/
public static void deleteDirectory(String directory) {
try (DirectoryStream<Path> stream = Files
.newDirectoryStream(Paths.get(directory))) {
for (Path path : stream) {
if(Files.isDirectory(path)) {
deleteDirectory(path.toString());
}
else {
Files.delete(path);
}
}
Files.delete(Paths.get(directory));
}
catch (IOException e) {
if(e.getClass() == DirectoryNotEmptyException.class) {
Logger.warn("It appears that data was added to directory "
+ "{} while trying to perform a deletion. "
+ "Trying again...", directory);
deleteDirectory(directory);
}
else {
throw Throwables.propagate(e);
}
}
}
/**
* Delete the {@code file}.
*
* @param file
*/
public static void deleteFile(String file) {
try {
java.nio.file.Files.delete(Paths.get(file));
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Return an {@link Iterator} to traverse over all of the flat files (e.g.
* non subdirectores) in {@code directory}.
*
* @param directory
* @return the iterator
*/
public static Iterator<String> fileOnlyIterator(final String directory) {
return new ReadOnlyIterator<String>() {
private final File[] files = new File(directory).listFiles();
private int position = 0;
private File next = null;
{
findNext();
}
@Override
public boolean hasNext() {
return next != null;
}
@Override
public String next() {
File file = next;
findNext();
return file.getAbsolutePath();
}
/**
* Find the next element to be returned from {@link #next()}.
*/
private void findNext() {
if(files != null) {
File file = null;
while (file == null || file.isDirectory()) {
if(position >= files.length) {
file = null;
break;
}
else {
file = files[position];
position++;
}
}
next = file;
}
}
};
}
/**
* Return the random access {@link FileChannel} for {@code file}. The
* channel will be opened for reading and writing.
*
* @param file
* @return the FileChannel for {@code file}
*/
@SuppressWarnings("resource") // NOTE: can't close the file channel here
// because others depend on it
public static FileChannel getFileChannel(String file) {
try {
return new RandomAccessFile(openFile(file), "rwd").getChannel();
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Return the size of {@code file}. This method will automatically create
* {@code file} if it does not already exist.
*
* @param file
* @return the size in bytes
*/
public static long getFileSize(String file) {
try {
openFile(file);
return Files.size(Paths.get(file));
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Return the simple filename without path information or extension. This
* method assumes that the filename only contains one extension.
*
* @param filename
* @return the simple file name
*/
public static String getSimpleName(String filename) {
String[] placeholder;
return (placeholder = (placeholder = filename
.split("\\."))[placeholder.length - 2]
.split(File.separator))[placeholder.length - 1];
}
/**
* Look through {@code dir} and return all the sub directories.
*
* @param dir
* @return the sub directories under {@code dir}.
*/
public static Set<String> getSubDirs(String dir) {
File directory = new File(dir);
File[] files = directory.listFiles();
if(files != null) {
Set<String> subDirs = Sets.newHashSet();
for (File file : files) {
if(Files.isDirectory(Paths.get(file.getAbsolutePath()))) {
subDirs.add(file.getName());
}
}
return subDirs;
}
else {
return Collections.emptySet();
}
}
/**
* Return {@code true} in the filesystem contains {@code dir} and it is
* a directory.
*
* @param dir
* @return {@code true} if {@code dir} exists
*/
public static boolean hasDir(String dir) {
Path path = Paths.get(dir);
return Files.exists(path) && Files.isDirectory(path);
}
/**
* Return {@code true} in the filesystem contains {@code file} and it is not
* a directory.
*
* @param file
* @return {@code true} if {@code file} exists
*/
public static boolean hasFile(String file) {
Path path = Paths.get(file);
return Files.exists(path) && !Files.isDirectory(path);
}
/**
* Lock the file or directory specified in {@code path} for use in this JVM
* process. If the lock cannot be acquired, an exception is thrown.
*
* @param path
*/
public static void lock(String path) {
if(Files.isDirectory(Paths.get(path))) {
lock(path + File.separator + "concourse.lock");
}
else {
try {
checkState(getFileChannel(path).tryLock() != null,
"Unable to grab lock for %s because another "
+ "Concourse Server process is using it",
path);
}
catch (OverlappingFileLockException e) {
Logger.warn("Trying to lock {}, but the current "
+ "JVM is already the owner", path);
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
}
/**
* Create a valid path that contains separators in the appropriate places
* by joining all the {@link parts} together with the {@link File#separator}
*
* @param parts
* @return the path
*/
public static String makePath(String... parts) {
StringBuilder path = new StringBuilder();
for (String part : parts) {
path.append(part);
if(!part.endsWith(File.separator)) {
path.append(File.separator);
}
}
return path.toString();
}
/**
* Return a {@link MappedByteBuffer} for {@code file} in {@code mode}
* starting at {@code position} and continuing for {@code size} bytes. This
* method will automatically create {@code file} if it does not already
* exist.
*
* @param file
* @param mode
* @param position
* @param size
* @return the MappedByteBuffer
*/
public static MappedByteBuffer map(String file, MapMode mode, long position,
long size) {
FileChannel channel = getFileChannel(file);
try {
return channel.map(mode, position, size).load();
}
catch (IOException e) {
throw Throwables.propagate(e);
}
finally {
closeFileChannel(channel);
}
}
/**
* Open {@code file} and return a {@link File} handle. This method will
* create a new file if and only if it does not already exist.
*
* @param file
*/
public static File openFile(String file) {
try {
File f = new File(file);
if(f.getParentFile() != null) {
f.getParentFile().mkdirs();
}
f.createNewFile();
return f;
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Read bytes from {@code file} <em>sequentially</em> and return the content
* as a <strong>read only</strong> {@link ByteBuffer}.
*
* @param file
* @return the read only ByteBuffer with the content of {@code file}
*/
public static ByteBuffer readBytes(String file) {
FileChannel channel = getFileChannel(file);
try {
MappedByteBuffer data = channel.map(MapMode.READ_ONLY, 0,
channel.size());
return data;
}
catch (IOException e) {
throw Throwables.propagate(e);
}
finally {
closeFileChannel(channel);
}
}
/**
* Replace the content of {@code original} with that of {@code replacement}
* and delete {@code replacement} in a single atomic operation.
*
* @param original
* @param replacement
*/
public static void replaceFile(String original, String replacement) {
try {
java.nio.file.Files.move(Paths.get(replacement),
Paths.get(original), StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* Return an {@link Iterator} to traverse over all of the sub directories
* (e.g. no flat files) in {@code directory}.
*
* @param directory
* @return the iterator
*/
public static Iterator<String> subDirectoryOnlyIterator(
final String directory) {
return getSubDirs(directory).iterator();
}
/**
* Attempt to force the unmapping of {@code buffer}. This method should be
* used with <strong>EXTREME CAUTION</strong>. If {@code buffer} is used
* after this method is invoked, it is likely that the JVM will crash.
*
* @param buffer
*/
public static void unmap(MappedByteBuffer buffer) {
Cleaners.freeMappedByteBuffer(buffer);
}
/**
* Write the {@code bytes} to {@code file} starting at the beginning. This
* method will perform and fsync.
*
* @param bytes
* @param file
*/
public static void writeBytes(ByteBuffer bytes, String file) {
writeBytes(bytes, file, 0);
}
/**
* Write the {@code bytes} to {@code file} starting {@code position}. This
* method will perform an fsync.
*
* @param bytes
* @param file
* @param position
*/
public static void writeBytes(ByteBuffer bytes, String file, int position) {
FileChannel channel = getFileChannel(file);
try {
channel.position(position);
channel.write(bytes);
channel.force(true);
}
catch (IOException e) {
throw Throwables.propagate(e);
}
finally {
closeFileChannel(channel);
}
}
private FileSystem() {/* noop */}
}