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 *      http://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.RandomAccessFile;
023import java.util.Objects;
024
025import org.apache.commons.io.RandomAccessFileMode;
026import org.apache.commons.io.build.AbstractOrigin;
027import org.apache.commons.io.build.AbstractStreamBuilder;
028
029/**
030 * Streams data from a {@link RandomAccessFile} starting at its current position.
031 * <p>
032 * To build an instance, use {@link Builder}.
033 * </p>
034 *
035 * @see Builder
036 * @since 2.8.0
037 */
038public class RandomAccessFileInputStream extends AbstractInputStream {
039
040    // @formatter:off
041    /**
042     * Builds a new {@link RandomAccessFileInputStream}.
043     *
044     * <p>
045     * For example:
046     * </p>
047     * <pre>{@code
048     * RandomAccessFileInputStream s = RandomAccessFileInputStream.builder()
049     *   .setPath(path)
050     *   .setCloseOnClose(true)
051     *   .get();}
052     * </pre>
053     *
054     * @see #get()
055     * @since 2.12.0
056     */
057    // @formatter:on
058    public static class Builder extends AbstractStreamBuilder<RandomAccessFileInputStream, Builder> {
059
060        private RandomAccessFile randomAccessFile;
061        private boolean propagateClose;
062
063        /**
064         * Builds a new {@link RandomAccessFileInputStream}.
065         * <p>
066         * You must set input that supports {@link RandomAccessFile} or {@link File}, otherwise, this method throws an exception. Only set one of
067         * RandomAccessFile or an origin that can be converted to a File.
068         * </p>
069         * <p>
070         * This builder use the following aspects:
071         * </p>
072         * <ul>
073         * <li>{@link RandomAccessFile}</li>
074         * <li>{@link File}</li>
075         * <li>closeOnClose</li>
076         * </ul>
077         *
078         * @return a new instance.
079         * @throws IllegalStateException         if the {@code origin} is {@code null}.
080         * @throws IllegalStateException         if both RandomAccessFile and origin are set.
081         * @throws UnsupportedOperationException if the origin cannot be converted to a {@link File}.
082         * @see AbstractOrigin#getFile()
083         */
084        @SuppressWarnings("resource") // Caller closes depending on settings
085        @Override
086        public RandomAccessFileInputStream get() throws IOException {
087            if (randomAccessFile != null) {
088                if (getOrigin() != null) {
089                    throw new IllegalStateException(String.format("Only set one of RandomAccessFile (%s) or origin (%s)", randomAccessFile, getOrigin()));
090                }
091                return new RandomAccessFileInputStream(randomAccessFile, propagateClose);
092            }
093            return new RandomAccessFileInputStream(RandomAccessFileMode.READ_ONLY.create(checkOrigin().getFile()), propagateClose);
094        }
095
096        /**
097         * Sets whether to close the underlying file when this stream is closed.
098         *
099         * @param propagateClose Whether to close the underlying file when this stream is closed.
100         * @return {@code this} instance.
101         */
102        public Builder setCloseOnClose(final boolean propagateClose) {
103            this.propagateClose = propagateClose;
104            return this;
105        }
106
107        /**
108         * Sets the RandomAccessFile to stream.
109         *
110         * @param randomAccessFile the RandomAccessFile to stream.
111         * @return {@code this} instance.
112         */
113        public Builder setRandomAccessFile(final RandomAccessFile randomAccessFile) {
114            this.randomAccessFile = randomAccessFile;
115            return this;
116        }
117
118    }
119
120    /**
121     * Constructs a new {@link Builder}.
122     *
123     * @return a new {@link Builder}.
124     * @since 2.12.0
125     */
126    public static Builder builder() {
127        return new Builder();
128    }
129
130    private final boolean propagateClose;
131    private final RandomAccessFile randomAccessFile;
132
133    /**
134     * Constructs a new instance configured to leave the underlying file open when this stream is closed.
135     *
136     * @param file The file to stream.
137     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
138     */
139    @Deprecated
140    public RandomAccessFileInputStream(final RandomAccessFile file) {
141        this(file, false);
142    }
143
144    /**
145     * Constructs a new instance.
146     *
147     * @param file         The file to stream.
148     * @param propagateClose Whether to close the underlying file when this stream is closed.
149     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
150     */
151    @Deprecated
152    public RandomAccessFileInputStream(final RandomAccessFile file, final boolean propagateClose) {
153        this.randomAccessFile = Objects.requireNonNull(file, "file");
154        this.propagateClose = propagateClose;
155    }
156
157    /**
158     * Returns an estimate of the number of bytes that can be read (or skipped over) from this input stream.
159     *
160     * If there are more than {@link Integer#MAX_VALUE} bytes available, return {@link Integer#MAX_VALUE}.
161     *
162     * @return An estimate of the number of bytes that can be read.
163     * @throws IOException If an I/O error occurs.
164     */
165    @Override
166    public int available() throws IOException {
167        final long avail = availableLong();
168        if (avail > Integer.MAX_VALUE) {
169            return Integer.MAX_VALUE;
170        }
171        return (int) avail;
172    }
173
174    /**
175     * Returns the number of bytes that can be read (or skipped over) from this input stream.
176     *
177     * @return The number of bytes that can be read.
178     * @throws IOException If an I/O error occurs.
179     */
180    public long availableLong() throws IOException {
181        return isClosed() ? 0 : randomAccessFile.length() - randomAccessFile.getFilePointer();
182    }
183
184    @Override
185    public void close() throws IOException {
186        super.close();
187        if (propagateClose) {
188            randomAccessFile.close();
189        }
190    }
191
192    /**
193     * Gets the underlying file.
194     *
195     * @return the underlying file.
196     */
197    public RandomAccessFile getRandomAccessFile() {
198        return randomAccessFile;
199    }
200
201    /**
202     * Returns whether to close the underlying file when this stream is closed.
203     *
204     * @return Whether to close the underlying file when this stream is closed.
205     */
206    public boolean isCloseOnClose() {
207        return propagateClose;
208    }
209
210    @Override
211    public int read() throws IOException {
212        return randomAccessFile.read();
213    }
214
215    @Override
216    public int read(final byte[] bytes) throws IOException {
217        return randomAccessFile.read(bytes);
218    }
219
220    @Override
221    public int read(final byte[] bytes, final int offset, final int length) throws IOException {
222        return randomAccessFile.read(bytes, offset, length);
223    }
224
225    @Override
226    public long skip(final long skipCount) throws IOException {
227        if (skipCount <= 0) {
228            return 0;
229        }
230        final long filePointer = randomAccessFile.getFilePointer();
231        final long fileLength = randomAccessFile.length();
232        if (filePointer >= fileLength) {
233            return 0;
234        }
235        final long targetPos = filePointer + skipCount;
236        final long newPos = targetPos > fileLength ? fileLength - 1 : targetPos;
237        if (newPos > 0) {
238            randomAccessFile.seek(newPos);
239        }
240        return randomAccessFile.getFilePointer() - filePointer;
241    }
242}