/*
* Copyright (C) 2015 SoftIndex LLC.
*
* 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 io.datakernel.file;
import io.datakernel.annotation.Nullable;
import io.datakernel.async.*;
import io.datakernel.bytebuf.ByteBuf;
import io.datakernel.bytebuf.ByteBufPool;
import io.datakernel.eventloop.Eventloop;
import io.datakernel.eventloop.RunnableWithException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousFileChannel;
import java.nio.channels.CompletionHandler;
import java.nio.file.CopyOption;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.attribute.FileAttribute;
import java.util.Arrays;
import java.util.HashSet;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import static io.datakernel.util.Preconditions.checkNotNull;
import static java.nio.file.StandardOpenOption.*;
/**
* An abstract representation of file. Actions with this file are non-blocking
*/
public final class AsyncFile {
private final Eventloop eventloop;
private final ExecutorService executor;
private final AsynchronousFileChannel channel;
private final Path path;
/**
* Creates a new instance of AsyncFile
*
* @param eventloop event loop in which a file will be used
* @param executor executor for running tasks in other thread
* @param channel an asynchronous channel for reading, writing, and manipulating a file.
* @param path path of the file
*/
private AsyncFile(Eventloop eventloop, ExecutorService executor, AsynchronousFileChannel channel, Path path) {
this.eventloop = checkNotNull(eventloop);
this.executor = checkNotNull(executor);
this.channel = checkNotNull(channel);
this.path = checkNotNull(path);
}
/**
* Opens file in a blocking manner
*
* @param eventloop event loop in which a file will be used
* @param executor executor for running tasks in other thread
* @param path the path of the file to open or create
* @param openOptions options specifying how the file is opened
*/
public static AsyncFile open(final Eventloop eventloop, final ExecutorService executor,
final Path path, final OpenOption[] openOptions) throws IOException {
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path, new HashSet<>(Arrays.asList(openOptions)), executor);
return new AsyncFile(eventloop, executor, channel, path);
}
/**
* Asynchronous opens file
*
* @param eventloop event loop in which a file will be used
* @param executor executor for running tasks in other thread
* @param path the path of the file to open or create
* @param openOptions options specifying how the file is opened
* @param callback callback which will be called after opening
*/
public static void open(final Eventloop eventloop, final ExecutorService executor,
final Path path, final OpenOption[] openOptions, ResultCallback<AsyncFile> callback) {
eventloop.callConcurrently(executor, new Callable<AsyncFile>() {
@Override
public AsyncFile call() throws Exception {
return open(eventloop, executor, path, openOptions);
}
}, callback);
}
/**
* Deletes the file in new thread
*
* @param eventloop event loop in which a file will be used
* @param executor @param path the path of the file to open or create
* @param callback callback which will be called after opening
*/
public static void delete(Eventloop eventloop, ExecutorService executor,
final Path path, CompletionCallback callback) {
eventloop.runConcurrently(executor, new RunnableWithException() {
@Override
public void runWithException() throws Exception {
Files.delete(path);
}
}, callback);
}
public static void length(Eventloop eventloop, ExecutorService executor, final Path path,
ResultCallback<Long> callback) {
eventloop.callConcurrently(executor, new Callable<Long>() {
@Override
public Long call() throws Exception {
File file = path.toFile();
if (!file.exists() || file.isDirectory()) {
return -1L;
} else {
return file.length();
}
}
}, callback);
}
/**
* Moves or renames a file to a target file.
*
* @param eventloop event loop in which a file will be used
* @param executor @param source the path to the file to move
* @param target the path to the target file (may be associated with a different provider to the source path)
* @param options options specifying how the move should be done
* @param callback callback which will be called after moving
*/
public static void move(Eventloop eventloop, ExecutorService executor,
final Path source, final Path target, final CopyOption[] options, CompletionCallback callback) {
eventloop.runConcurrently(executor, new RunnableWithException() {
@Override
public void runWithException() throws Exception {
Files.move(source, target, options);
}
}, callback);
}
/**
* Creates a new directory.
*
* @param eventloop event loop in which a file will be used
* @param executor @param dir the directory to create
* @param attrs an optional list of file attributes to set atomically when creating the directory
* @param callback callback which will be called after creating
*/
public static void createDirectory(Eventloop eventloop, ExecutorService executor,
final Path dir, @Nullable final FileAttribute<?>[] attrs, CompletionCallback callback) {
eventloop.runConcurrently(executor, new RunnableWithException() {
@Override
public void runWithException() throws Exception {
Files.createDirectory(dir, attrs == null ? new FileAttribute<?>[0] : attrs);
}
}, callback);
}
/**
* Creates a directory by creating all nonexistent parent directories first.
*
* @param eventloop event loop in which a file will be used
* @param executor @param dir the directory to create
* @param attrs an optional list of file attributes to set atomically when creating the directory
* @param callback callback which will be called after creating
*/
public static void createDirectories(Eventloop eventloop, ExecutorService executor,
final Path dir, @Nullable final FileAttribute<?>[] attrs, CompletionCallback callback) {
eventloop.runConcurrently(executor, new RunnableWithException() {
@Override
public void runWithException() throws Exception {
Files.createDirectories(dir, attrs == null ? new FileAttribute<?>[0] : attrs);
}
}, callback);
}
/**
* Reads all sequence of bytes from this channel into the given buffer.
*
* @param eventloop event loop in which a file will be used
* @param executor @param path the path of the file to read
* @param callback which will be called after complete
*/
public static void readFile(Eventloop eventloop, ExecutorService executor,
Path path, final ResultCallback<ByteBuf> callback) {
open(eventloop, executor, path, new OpenOption[]{READ}, new ForwardingResultCallback<AsyncFile>(callback) {
@Override
public void onResult(final AsyncFile file) {
file.readFully(new ResultCallback<ByteBuf>() {
@Override
public void onResult(ByteBuf buf) {
file.close(IgnoreCompletionCallback.create());
callback.setResult(buf);
}
@Override
public void onException(Exception e) {
file.close(IgnoreCompletionCallback.create());
callback.setException(e);
}
});
}
});
}
/**
* Creates new file and writes a sequence of bytes to this file from the given buffer, starting at the given file
* position
*
* @param eventloop event loop in which a file will be used
* @param executor @param path the path of the file to create and write
* @param buf the buffer from which bytes are to be transferred byteBuffer
* @param callback which will be called after complete
*/
public static void createNewAndWriteFile(Eventloop eventloop, ExecutorService executor,
Path path, final ByteBuf buf, final CompletionCallback callback) {
open(eventloop, executor, path, new OpenOption[]{WRITE, CREATE_NEW}, new ForwardingResultCallback<AsyncFile>(callback) {
@Override
public void onResult(AsyncFile file) {
file.writeFully(buf, 0L, new CompletionCallback() {
@Override
public void onComplete() {
buf.recycle();
callback.setComplete();
}
@Override
public void onException(Exception exception) {
buf.recycle();
callback.setException(exception);
}
});
}
});
}
/**
* Writes a sequence of bytes to this file from the given buffer, starting at the given file
* position.
*
* @param buf the buffer from which bytes are to be transferred
* @param position the file position at which the transfer is to begin; must be non-negative
* @param callback callback which will be called after complete
*/
public void write(final ByteBuf buf, long position, final ResultCallback<Integer> callback) {
final Eventloop.ConcurrentOperationTracker tracker = eventloop.startConcurrentOperation();
final ByteBuffer byteBuffer = buf.toReadByteBuffer();
channel.write(byteBuffer, position, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(final Integer result, Object attachment) {
buf.ofReadByteBuffer(byteBuffer);
eventloop.execute(new Runnable() {
@Override
public void run() {
tracker.complete();
callback.setResult(result);
}
});
}
@Override
public void failed(final Throwable exc, Object attachment) {
eventloop.execute(new Runnable() {
@Override
public void run() {
tracker.complete();
callback.setException(exc instanceof Exception ? (Exception) exc : new Exception(exc));
}
});
}
});
}
/**
* Reads a sequence of bytes from this channel into the given buffer, starting at the given file position.
*
* @param buf the buffer into which bytes are to be transferred
* @param position the file position at which the transfer is to begin; must be non-negative
* @param callback which will be called after complete
*/
public void read(final ByteBuf buf, long position, final ResultCallback<Integer> callback) {
final Eventloop.ConcurrentOperationTracker tracker = eventloop.startConcurrentOperation();
final ByteBuffer byteBuffer = buf.toWriteByteBuffer();
channel.read(byteBuffer, position, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(final Integer result, Object attachment) {
buf.ofWriteByteBuffer(byteBuffer);
eventloop.execute(new Runnable() {
@Override
public void run() {
tracker.complete();
callback.setResult(result);
}
});
}
@Override
public void failed(final Throwable exc, Object attachment) {
eventloop.execute(new Runnable() {
@Override
public void run() {
tracker.complete();
callback.setException(exc instanceof Exception ? (Exception) exc : new Exception(exc));
}
});
}
});
}
private void writeFully(final ByteBuf buf, final long position, final Eventloop.ConcurrentOperationTracker tracker,
final AtomicBoolean cancelled, final CompletionCallback callback) {
final ByteBuffer byteBuffer = buf.toReadByteBuffer();
channel.write(byteBuffer, position, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
buf.ofReadByteBuffer(byteBuffer);
if (buf.readRemaining() == 0) {
eventloop.execute(new Runnable() {
@Override
public void run() {
buf.recycle();
tracker.complete();
callback.setComplete();
}
});
} else {
if (cancelled.get()) {
tracker.complete();
return;
}
writeFully(buf, position + result, tracker, cancelled, callback);
}
}
@Override
public void failed(final Throwable exc, Object attachment) {
eventloop.execute(new Runnable() {
@Override
public void run() {
buf.recycle();
tracker.complete();
callback.setException(exc instanceof Exception ? (Exception) exc : new Exception(exc));
}
});
}
});
}
/**
* Writes a sequence of bytes to this file from the given buffer, starting at the given file
* position. Writes in other thread.
*
* @param byteBuf the buffer from which bytes are to be transferred
* @param position the file position at which the transfer is to begin; must be non-negative
* @param callback callback which will be called after complete
*/
public AsyncCancellable writeFully(ByteBuf byteBuf, long position, CompletionCallback callback) {
Eventloop.ConcurrentOperationTracker tracker = eventloop.startConcurrentOperation();
final AtomicBoolean cancelled = new AtomicBoolean();
writeFully(byteBuf, position, tracker, cancelled, callback);
return new AsyncCancellable() {
@Override
public void cancel() {
cancelled.set(true);
}
};
}
private void readFully(final ByteBuf buf, final long position, final long size,
final Eventloop.ConcurrentOperationTracker tracker,
final AtomicBoolean cancelled, final CompletionCallback callback) {
final ByteBuffer byteBuffer = buf.toWriteByteBuffer();
channel.read(byteBuffer, position, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(Integer result, Object attachment) {
buf.ofWriteByteBuffer(byteBuffer);
if (buf.readRemaining() == size || result == -1) {
eventloop.execute(new Runnable() {
@Override
public void run() {
try {
channel.close();
tracker.complete();
callback.setComplete();
} catch (IOException e) {
tracker.complete();
callback.setException(e);
}
}
});
} else {
if (cancelled.get()) {
try {
channel.close();
} catch (IOException ignore) {
}
tracker.complete();
return;
}
readFully(buf, position, size, tracker, cancelled, callback);
}
}
@Override
public void failed(final Throwable exc, Object attachment) {
eventloop.execute(new Runnable() {
@Override
public void run() {
try {
channel.close();
} catch (IOException ignore) {
}
tracker.complete();
callback.setException(exc instanceof Exception ? (Exception) exc : new Exception(exc));
}
});
}
});
}
/**
* Reads a sequence of bytes from this channel into the given buffer, starting at the given file position.
* Reads in other thread.
*
* @param buf the buffer into which bytes are to be transferred
* @param position the file position at which the transfer is to begin; must be non-negative
* @param callback which will be called after complete
*/
public AsyncCancellable readFully(ByteBuf buf, long position, CompletionCallback callback) {
long size;
try {
size = channel.size();
} catch (IOException e) {
callback.setException(e);
return new AsyncCancellable() {
@Override
public void cancel() {
// do nothing
}
};
}
Eventloop.ConcurrentOperationTracker tracker = eventloop.startConcurrentOperation();
final AtomicBoolean cancelled = new AtomicBoolean();
readFully(buf, position, size, tracker, cancelled, callback);
return new AsyncCancellable() {
@Override
public void cancel() {
cancelled.set(true);
}
};
}
/**
* Reads all sequence of bytes from this channel into buffer and sends this buffer to {@code callback}
*
* @param callback which will be called after complete
*/
public void readFully(final ResultCallback<ByteBuf> callback) {
long size;
try {
size = channel.size();
} catch (IOException e) {
callback.setException(e);
return;
}
final ByteBuf buf = ByteBufPool.allocate((int) size);
readFully(buf, 0, new CompletionCallback() {
@Override
public void onComplete() {
callback.setResult(buf);
}
@Override
public void onException(Exception e) {
buf.recycle();
callback.setException(e);
}
});
}
public void forceAndClose(CompletionCallback callback) {
eventloop.runConcurrently(executor, new RunnableWithException() {
@Override
public void runWithException() throws Exception {
channel.force(true);
channel.close();
}
}, callback);
}
/**
* Closes the channel
*
* @param callback which will be called after complete
*/
public void close(CompletionCallback callback) {
eventloop.runConcurrently(executor, new RunnableWithException() {
@Override
public void runWithException() throws Exception {
channel.close();
}
}, callback);
}
/**
* Truncates this file to the given size.
*
* @param size the new size, a non-negative byte count
* @param callback which will be called after complete
*/
public void truncate(final long size, CompletionCallback callback) {
eventloop.runConcurrently(executor, new RunnableWithException() {
@Override
public void runWithException() throws Exception {
channel.truncate(size);
}
}, callback);
}
/**
* Forces any updates to this file to be written to the storage device that contains it.
*
* @param metaData if true then this method is required to force changes to both the file's
* content and metadata to be written to storage; otherwise, it need only force content changes to be written
* @param callback which will be called after complete
*/
public void force(final boolean metaData, CompletionCallback callback) {
eventloop.runConcurrently(executor, new RunnableWithException() {
@Override
public void runWithException() throws Exception {
channel.force(metaData);
}
}, callback);
}
public Eventloop getEventloop() {
return eventloop;
}
public ExecutorService getExecutor() {
return executor;
}
public AsynchronousFileChannel getChannel() {
return channel;
}
public boolean isOpen() {
return channel.isOpen();
}
@Override
public String toString() {
return path.toString();
}
}