    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.io.input;
    019
    020import java.io.File;
    021import java.io.IOException;
    022import java.io.OutputStream;
    023import java.io.RandomAccessFile;
    024import java.util.Objects;
    025
    026import org.apache.commons.io.IOUtils;
    027import org.apache.commons.io.build.AbstractOrigin;
    028import org.apache.commons.io.build.AbstractStreamBuilder;
    029
    030/**
    031 * Streams data from a {@link RandomAccessFile} starting at its current position.
    032 * <p>
    033 * To build an instance, use {@link Builder}.
    034 * </p>
    035 *
    036 * @see Builder
    037 * @since 2.8.0
    038 */
    039public class RandomAccessFileInputStream extends AbstractInputStream {
    040
    041    // @formatter:off
    042    /**
    043     * Builds a new {@link RandomAccessFileInputStream}.
    044     *
    045     * <p>
    046     * For example:
    047     * </p>
    048     * <pre>{@code
    049     * RandomAccessFileInputStream s = RandomAccessFileInputStream.builder()
    050     *   .setPath(path)
    051     *   .setCloseOnClose(true)
    052     *   .get();}
    053     * </pre>
    054     *
    055     * @see #get()
    056     * @since 2.12.0
    057     */
    058    // @formatter:on
    059    public static class Builder extends AbstractStreamBuilder<RandomAccessFileInputStream, Builder> {
    060
    061        private boolean propagateClose;
    062
    063        /**
    064         * Constructs a new builder of {@link RandomAccessFileInputStream}.
    065         */
    066        public Builder() {
    067            // empty
    068        }
    069
    070        /**
    071         * Builds a new {@link RandomAccessFileInputStream}.
    072         * <p>
    073         * You must set an aspect that supports {@link RandomAccessFile} or {@link File}, otherwise, this method throws an exception. Only set one of
    074         * RandomAccessFile or an origin that can be converted to a File.
    075         * </p>
    076         * <p>
    077         * This builder uses the following aspects:
    078         * </p>
    079         * <ul>
    080         * <li>{@link RandomAccessFile} gets the target aspect.</li>
    081         * <li>{@link File}</li>
    082         * <li>closeOnClose</li>
    083         * </ul>
    084         *
    085         * @return a new instance.
    086         * @throws IllegalStateException         if the {@code origin} is {@code null}.
    087         * @throws IllegalStateException         if both RandomAccessFile and origin are set.
    088         * @throws UnsupportedOperationException if the origin cannot be converted to a {@link RandomAccessFile}.
    089         * @throws IOException                   if an I/O error occurs converting to an {@link RandomAccessFile} using {@link #getRandomAccessFile()}.
    090         * @see AbstractOrigin#getFile()
    091         * @see #getUnchecked()
    092         */
    093        @Override
    094        public RandomAccessFileInputStream get() throws IOException {
    095            return new RandomAccessFileInputStream(this);
    096        }
    097
    098        /**
    099         * Sets whether to close the underlying file when this stream is closed, defaults to false for compatibility.
    100         *
    101         * @param propagateClose Whether to close the underlying file when this stream is closed.
    102         * @return {@code this} instance.
    103         */
    104        public Builder setCloseOnClose(final boolean propagateClose) {
    105            this.propagateClose = propagateClose;
    106            return this;
    107        }
    108
    109        /**
    110         * Sets the RandomAccessFile to stream.
    111         *
    112         * @param randomAccessFile the RandomAccessFile to stream.
    113         * @return {@code this} instance.
    114         */
    115        @Override // MUST keep this method for binary compatibility since the super version of this method uses a generic which compiles to Object.
    116        public Builder setRandomAccessFile(final RandomAccessFile randomAccessFile) { // NOPMD see above.
    117            return super.setRandomAccessFile(randomAccessFile);
    118        }
    119
    120    }
    121
    122    /**
    123     * Constructs a new {@link Builder}.
    124     *
    125     * @return a new {@link Builder}.
    126     * @since 2.12.0
    127     */
    128    public static Builder builder() {
    129        return new Builder();
    130    }
    131
    132    private final boolean propagateClose;
    133    private final RandomAccessFile randomAccessFile;
    134
    135    @SuppressWarnings("resource") // caller closes.
    136    private RandomAccessFileInputStream(final Builder builder) throws IOException {
    137        this(builder.getRandomAccessFile(), builder.propagateClose);
    138    }
    139
    140    /**
    141     * Constructs a new instance configured to leave the underlying file open when this stream is closed.
    142     *
    143     * @param file The file to stream.
    144     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    145     */
    146    @Deprecated
    147    public RandomAccessFileInputStream(final RandomAccessFile file) {
    148        this(file, false);
    149    }
    150
    151    /**
    152     * Constructs a new instance.
    153     *
    154     * @param file         The file to stream.
    155     * @param propagateClose Whether to close the underlying file when this stream is closed.
    156     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
    157     */
    158    @Deprecated
    159    public RandomAccessFileInputStream(final RandomAccessFile file, final boolean propagateClose) {
    160        this.randomAccessFile = Objects.requireNonNull(file, "file");
    161        this.propagateClose = propagateClose;
    162    }
    163
    164    /**
    165     * Gets an estimate of the number of bytes that can be read (or skipped over) from this input stream.
    166     * <p>
    167     * If there are more than {@link Integer#MAX_VALUE} bytes available, return {@link Integer#MAX_VALUE}.
    168     * </p>
    169     *
    170     * @return An estimate of the number of bytes that can be read.
    171     * @throws IOException If an I/O error occurs.
    172     */
    173    @Override
    174    public int available() throws IOException {
    175        return Math.toIntExact(Math.min(availableLong(), Integer.MAX_VALUE));
    176    }
    177
    178    /**
    179     * Gets the number of bytes that can be read (or skipped over) from this input stream.
    180     *
    181     * @return The number of bytes that can be read.
    182     * @throws IOException If an I/O error occurs.
    183     */
    184    public long availableLong() throws IOException {
    185        return isClosed() ? 0 : randomAccessFile.length() - randomAccessFile.getFilePointer();
    186    }
    187
    188    @Override
    189    public void close() throws IOException {
    190        super.close();
    191        if (propagateClose) {
    192            randomAccessFile.close();
    193        }
    194    }
    195
    196    /**
    197     * Copies our bytes from the given starting position for a size to the output stream.
    198     *
    199     * @param pos  Where to start copying. The offset position, measured in bytes from the beginning of the file, at which to set the file pointer.
    200     * @param size The number of bytes to copy.
    201     * @param os   Where to copy.
    202     * @return The number of bytes copied..
    203     * @throws IOException if {@code pos} is less than {@code 0} or if an I/O error occurs.
    204     * @since 2.19.0
    205     */
    206    public long copy(final long pos, final long size, final OutputStream os) throws IOException {
    207        randomAccessFile.seek(pos);
    208        return IOUtils.copyLarge(this, os, 0, size);
    209    }
    210
    211    /**
    212     * Gets the underlying file.
    213     *
    214     * @return the underlying file.
    215     */
    216    public RandomAccessFile getRandomAccessFile() {
    217        return randomAccessFile;
    218    }
    219
    220    /**
    221     * Tests whether to close the underlying file when this stream is closed, defaults to false for compatibility.
    222     *
    223     * @return Whether to close the underlying file when this stream is closed.
    224     */
    225    public boolean isCloseOnClose() {
    226        return propagateClose;
    227    }
    228
    229    @Override
    230    public int read() throws IOException {
    231        return randomAccessFile.read();
    232    }
    233
    234    @Override
    235    public int read(final byte[] bytes) throws IOException {
    236        return randomAccessFile.read(bytes);
    237    }
    238
    239    @Override
    240    public int read(final byte[] bytes, final int offset, final int length) throws IOException {
    241        return randomAccessFile.read(bytes, offset, length);
    242    }
    243
    244    @Override
    245    public long skip(final long skipCount) throws IOException {
    246        if (skipCount <= 0) {
    247            return 0;
    248        }
    249        final long filePointer = randomAccessFile.getFilePointer();
    250        final long fileLength = randomAccessFile.length();
    251        if (filePointer >= fileLength) {
    252            return 0;
    253        }
    254        final long targetPos = filePointer + skipCount;
    255        final long newPos = targetPos > fileLength ? fileLength - 1 : targetPos;
    256        if (newPos > 0) {
    257            randomAccessFile.seek(newPos);
    258        }
    259        return randomAccessFile.getFilePointer() - filePointer;
    260    }
    261}