Java tutorial
/* Copyright (C) 2013-2014 Ian Teune <ian.teune@gmail.com> * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.tinspx.util.io.callbacks; import com.google.common.base.Function; import com.google.common.base.Optional; import static com.google.common.base.Preconditions.*; import com.google.common.collect.BoundType; import com.google.common.collect.Lists; import com.google.common.collect.Range; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import com.tinspx.util.base.BasicError; import com.tinspx.util.base.ContentCallback; import com.tinspx.util.base.Errors; import com.tinspx.util.base.NumberUtils; import com.tinspx.util.io.ByteUtils; import com.tinspx.util.io.ChannelSource; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.Collections; import java.util.List; import javax.annotation.Nullable; import javax.annotation.concurrent.NotThreadSafe; import lombok.Getter; import lombok.NonNull; /** * Writes all content to a {@link FileChannel}. The {@code FileChannel} can be * generated from an arbitrary function using {@link #from(Function)}. When * setting the file, an optional {@link Range Range<Long>} may be specified. * This {@code Range} specifies the byte range in the file that should be * modified. Content will be written to the file starting at the lower endpoint * and continue until the upper endpoint is reached, at which point all * additional content will be discarded. * <p> * The {@code FileChannel} is closed when a write operation throws an * {@code IOException}, {@link #onContentComplete(Object)} is called, or * {@link #close()} is called, whichever come first. Use {@link #hasErrors()} * and {@link #errors()} to determine if any errors occurred writing content * or closing the channel. * * @see FileCallback * @see RandomAccessFileCallback * @author Ian */ @NotThreadSafe public class FileChannelCallback implements ContentCallback<Object, ByteBuffer>, Closeable { public static FileChannelCallback from(Function<? super File, ? extends FileChannel> fileChannelFunction) { return new FileChannelCallback(fileChannelFunction); } public static FileChannelCallback fromFileOutputStream() { return new FileChannelCallback(ChannelSource.fromFileOutputStream()); } public static FileChannelCallback fromFileOutputStream(boolean append) { return new FileChannelCallback(ChannelSource.fromFileOutputStream(append)); } /** * Generates the {@link FileChannel} from a * {@link java.io.RandomAccessFile RandomAccessFile} opened in "rw" mode. */ public static FileChannelCallback fromRandomAccessFile() { return new FileChannelCallback(ChannelSource.fromRandomAccessFile()); } /** * Generates the {@link FileChannel} from a * {@link java.io.RandomAccessFile RandomAccessFile} opened in the specified * mode. {@code mode} must be "rw", "rws", or "rwd". */ public static FileChannelCallback fromRandomAccessFile(String mode) { if (!RandomAccessFileCallback.VALID_MODES.contains(mode)) { throw new IllegalArgumentException("invalid mode: " + mode); } return new FileChannelCallback(ChannelSource.fromRandomAccessFile(mode)); } final Function<? super File, ? extends FileChannel> channelFunction; Range<Long> range; File file; FileChannel channel; /** * Determines if any content has been received through * {@link #onContent(Object, ByteBuffer)}. Once started, the File can no * longer be set through any {@link #setFile(File)} variant. */ @Getter boolean started; /** * Determines if this instance has been closed through * {@link #onContentComplete(Object) onContentComplete} or * {@link #close() close}. This will also be true if an error has been * encountered. */ @Getter boolean closed; /** * all encountered errors */ List<BasicError> errors; final SettableFuture<FileChannelCallback> completionFuture = SettableFuture.create(); private FileChannelCallback(Function<? super File, ? extends FileChannel> channelFunction) { this.channelFunction = checkNotNull(channelFunction); } /** * Sets the {@code File} to write. * * @param pathname pathname string of the file to write to * @throws IOException if {@code pathname} is a directory * @throws IllegalStateException is {@link #isStarted()} is {@code true} */ public FileChannelCallback file(String pathname) throws IOException { return setFileImpl(new File(pathname), null); } /** * Sets the {@code File} to write. * * @param pathname pathname string of the file to write to * @param range the byte range of the file to write. no bytes outside this * range will be modified. * @throws IOException if {@code pathname} is a directory * @throws IllegalStateException is {@link #isStarted()} is {@code true} * @throws IllegalArgumentException if {@code range} is invalid */ public FileChannelCallback file(String pathname, Range<? extends Number> range) throws IOException { return setFileImpl(new File(pathname), NumberUtils.toLongRange(range)); } /** * Sets the {@code File} to write. * * @param file the file to write to * @throws IOException if {@code pathname} is a directory * @throws IllegalStateException is {@link #isStarted()} is {@code true} */ public FileChannelCallback file(File file) throws IOException { return setFileImpl(file, null); } /** * Sets the {@code File} to write. * * @param file the file to write to * @param range the byte range of the file to write. no bytes outside this * range will be modified. * @throws IOException if {@code pathname} is a directory * @throws IllegalStateException is {@link #isStarted()} is {@code true} * @throws IllegalArgumentException if {@code range} is invalid */ public FileChannelCallback file(File file, Range<? extends Number> range) throws IOException { return setFileImpl(file, NumberUtils.toLongRange(range)); } private FileChannelCallback setFileImpl(@NonNull File file, @Nullable Range<Long> range) throws IOException { checkState(!started, "cannot set File once started"); if (file.isDirectory()) { throw new IOException(file + " is a directory"); } this.range = range != null ? checkAndNormalize(range) : Range.<Long>all(); this.file = file; return this; } private void addError(@Nullable Object context, BasicError error) { addError(context, error, true); } private void addError(@Nullable Object context, BasicError error, boolean send) { if (errors == null) { errors = Lists.newArrayListWithCapacity(4); } errors.add(error); if (send) { Errors.sendOrLog(context, error); } } @Override public boolean onContentStart(Object context) { return !closed; } @Override public boolean onContent(Object context, ByteBuffer content) { if (!started && !closed) { started = true; boolean error = false; try { if (file != null) { try { channel = ChannelSource.apply(file, channelFunction); if (range.hasLowerBound()) { channel.position(range.lowerEndpoint()); } } catch (IOException ex) { error = true; addError(context, Errors.create(this, ex, "opening %s with %s for range %s", file, channelFunction, range)); } } else { error = true; addError(context, Errors.create(this, "no File set")); } } finally { if (error) { closeQuietly(context); } } } if (channel != null && !closed) { boolean done = false; try { if (range.hasUpperBound()) { long remaining = range.upperEndpoint() - channel.position(); if (remaining <= content.remaining()) { done = true; if (remaining <= 0) { return false; } content.limit(content.position() + (int) remaining); } } ByteUtils.copy(content, channel); } catch (IOException ex) { done = true; addError(context, Errors.create(this, ex, "writing %s to %s", content, file)); } finally { if (done) { closeQuietly(context); } } } return !closed; } @Override public void onContentComplete(Object context) { closeQuietly(context); } private void closeQuietly(Object context) { try { close(context, true); } catch (IOException ex) { throw new AssertionError(ex); } } /** * explicitly closes the underlying {@link FileChannel}. */ @Override public void close() throws IOException { close(null, false); } private void close(@Nullable Object context, boolean quietly) throws IOException { if (closed) { return; } closed = true; try { if (channel != null) { channel.close(); } } catch (IOException ex) { addError(context, Errors.create(this, ex, "closing %s", file), quietly); if (!quietly) { throw ex; } } finally { completionFuture.set(this); } } @Override public void onError(BasicError error) { } public boolean hasErrors() { return errors != null; } public List<BasicError> errors() { return errors != null ? Collections.unmodifiableList(errors) : Collections.<BasicError>emptyList(); } public boolean hasFile() { return file != null; } public Optional<File> file() { return Optional.fromNullable(file); } public Optional<Range<Long>> range() { return Optional.fromNullable(range); } public boolean hasFileChannel() { return channel != null; } public Optional<FileChannel> fileChannel() { return Optional.fromNullable(channel); } /** * Returns a Future that will always successfully complete when this * {@code FileChannelCallback} is complete. Although the Future will always * succeed, {@link #hasErrors()} should be checked to determine if any * errors occurred. */ public ListenableFuture<FileChannelCallback> completionFuture() { return completionFuture; } /** * Normalizes lower bound to a closed bound type (if exists) and the * upper bound to an open bound type (if exists). Missing bounds remain * missing. Lower bound must be non-negative and upper bound must be * positive. The range cannot be empty. */ static Range<Long> checkAndNormalize(Range<Long> range) { checkArgument(!range.isEmpty(), "range %s is empty", range); boolean make = false; long lower = -1; long upper = -1; if (range.hasLowerBound()) { lower = range.lowerEndpoint(); if (range.lowerBoundType() == BoundType.OPEN) { make = true; lower++; } checkArgument(lower >= 0, "closed lower bound (%s) may not be negative", lower); } if (range.hasUpperBound()) { upper = range.upperEndpoint(); if (range.upperBoundType() == BoundType.CLOSED) { make = true; upper++; } checkArgument(upper > 0, "open upper bound (%s) must be positive", upper); } if (make) { if (lower >= 0) { if (upper > 0) { range = Range.closedOpen(lower, upper); } else { range = Range.atLeast(lower); } } else { assert upper > 0 : upper; range = Range.lessThan(upper); } checkArgument(!range.isEmpty(), "normalized range %s is empty", range); } return range; } }