com.tinspx.util.io.callbacks.FileChannelCallback.java Source code

Java tutorial

Introduction

Here is the source code for com.tinspx.util.io.callbacks.FileChannelCallback.java

Source

/* 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;
    }
}