View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    *     https://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  package org.apache.commons.io.input;
15  
16  import static org.apache.commons.io.IOUtils.EOF;
17  
18  import java.io.BufferedInputStream;
19  import java.io.File;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.nio.ByteBuffer;
23  import java.nio.channels.FileChannel;
24  import java.nio.file.Path;
25  import java.nio.file.StandardOpenOption;
26  
27  import org.apache.commons.io.IOUtils;
28  import org.apache.commons.io.build.AbstractStreamBuilder;
29  
30  /**
31   * {@link InputStream} implementation which uses direct buffer to read a file to avoid extra copy of data between Java and native memory which happens when
32   * using {@link BufferedInputStream}. Unfortunately, this is not something already available in JDK, {@code sun.nio.ch.ChannelInputStream} supports
33   * reading a file using NIO, but does not support buffering.
34   * <p>
35   * To build an instance, use {@link Builder}.
36   * </p>
37   * <p>
38   * This class was ported and adapted from Apache Spark commit 933dc6cb7b3de1d8ccaf73d124d6eb95b947ed19 where it was called {@code NioBufferedFileInputStream}.
39   * </p>
40   *
41   * @see Builder
42   * @since 2.9.0
43   */
44  public final class BufferedFileChannelInputStream extends InputStream {
45  
46      // @formatter:off
47      /**
48       * Builds a new {@link BufferedFileChannelInputStream}.
49       *
50       * <p>
51       * Using File IO:
52       * </p>
53       * <pre>{@code
54       * BufferedFileChannelInputStream s = BufferedFileChannelInputStream.builder()
55       *   .setFile(file)
56       *   .setBufferSize(4096)
57       *   .get();}
58       * </pre>
59       * <p>
60       * Using NIO Path:
61       * </p>
62       * <pre>{@code
63       * BufferedFileChannelInputStream s = BufferedFileChannelInputStream.builder()
64       *   .setPath(path)
65       *   .setBufferSize(4096)
66       *   .get();}
67       * </pre>
68       *
69       * @see #get()
70       * @since 2.12.0
71       */
72      // @formatter:on
73      public static class Builder extends AbstractStreamBuilder<BufferedFileChannelInputStream, Builder> {
74  
75          private FileChannel fileChannel;
76  
77          /**
78           * Constructs a new builder of {@link BufferedFileChannelInputStream}.
79           */
80          public Builder() {
81              // empty
82          }
83  
84          /**
85           * Builds a new {@link BufferedFileChannelInputStream}.
86           * <p>
87           * You must set an aspect that supports {@link #getInputStream()}, otherwise, this method throws an exception.
88           * </p>
89           * <p>
90           * This builder uses the following aspects:
91           * </p>
92           * <ul>
93           * <li>{@link FileChannel} takes precedence is set. </li>
94           * <li>{@link #getPath()} if the file channel is not set.</li>
95           * <li>{@link #getBufferSize()}</li>
96           * </ul>
97           *
98           * @return a new instance.
99           * @throws IllegalStateException         if the {@code origin} is {@code null}.
100          * @throws UnsupportedOperationException if the origin cannot be converted to a {@link Path}.
101          * @throws IOException                   if an I/O error occurs converting to an {@link Path} using {@link #getPath()}.
102          * @see #getPath()
103          * @see #getBufferSize()
104          * @see #getUnchecked()
105          */
106         @Override
107         public BufferedFileChannelInputStream get() throws IOException {
108             return new BufferedFileChannelInputStream(this);
109         }
110 
111         /**
112          * Sets the file channel.
113          * <p>
114          * This setting takes precedence over all others.
115          * </p>
116          *
117          * @param fileChannel the file channel.
118          * @return this instance.
119          * @since 2.18.0
120          */
121         public Builder setFileChannel(final FileChannel fileChannel) {
122             this.fileChannel = fileChannel;
123             return this;
124         }
125 
126     }
127 
128     /**
129      * Constructs a new {@link Builder}.
130      *
131      * @return a new {@link Builder}.
132      * @since 2.12.0
133      */
134     public static Builder builder() {
135         return new Builder();
136     }
137 
138     private final ByteBuffer byteBuffer;
139 
140     private final FileChannel fileChannel;
141 
142     @SuppressWarnings("resource")
143     private BufferedFileChannelInputStream(final Builder builder) throws IOException {
144         this.fileChannel = builder.fileChannel != null ? builder.fileChannel : FileChannel.open(builder.getPath(), StandardOpenOption.READ);
145         this.byteBuffer = ByteBuffer.allocateDirect(builder.getBufferSize());
146         this.byteBuffer.flip();
147     }
148 
149     /**
150      * Constructs a new instance for the given File.
151      *
152      * @param file The file to stream.
153      * @throws IOException If an I/O error occurs
154      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
155      */
156     @Deprecated
157     public BufferedFileChannelInputStream(final File file) throws IOException {
158         this(file, IOUtils.DEFAULT_BUFFER_SIZE);
159     }
160 
161     /**
162      * Constructs a new instance for the given File and buffer size.
163      *
164      * @param file       The file to stream.
165      * @param bufferSize buffer size.
166      * @throws IOException If an I/O error occurs
167      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
168      */
169     @Deprecated
170     public BufferedFileChannelInputStream(final File file, final int bufferSize) throws IOException {
171         this(file.toPath(), bufferSize);
172     }
173 
174     /**
175      * Constructs a new instance for the given Path.
176      *
177      * @param path The path to stream.
178      * @throws IOException If an I/O error occurs
179      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
180      */
181     @Deprecated
182     public BufferedFileChannelInputStream(final Path path) throws IOException {
183         this(path, IOUtils.DEFAULT_BUFFER_SIZE);
184     }
185 
186     /**
187      * Constructs a new instance for the given Path and buffer size.
188      *
189      * @param path       The path to stream.
190      * @param bufferSize buffer size.
191      * @throws IOException If an I/O error occurs
192      * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}
193      */
194     @Deprecated
195     public BufferedFileChannelInputStream(final Path path, final int bufferSize) throws IOException {
196         this(builder().setPath(path).setBufferSize(bufferSize));
197     }
198 
199     @Override
200     public synchronized int available() throws IOException {
201         if (!fileChannel.isOpen()) {
202             return 0;
203         }
204         if (!refill()) {
205             return 0;
206         }
207         return byteBuffer.remaining();
208     }
209 
210     /**
211      * Attempts to clean up a ByteBuffer if it is direct or memory-mapped. This uses an *unsafe* Sun API that will cause errors if one attempts to read from the
212      * disposed buffer. However, neither the bytes allocated to direct buffers nor file descriptors opened for memory-mapped buffers put pressure on the garbage
213      * collector. Waiting for garbage collection may lead to the depletion of off-heap memory or huge numbers of open files. There's unfortunately no standard
214      * API to manually dispose of these kinds of buffers.
215      *
216      * @param buffer the buffer to clean.
217      */
218     private void clean(final ByteBuffer buffer) {
219         if (buffer.isDirect()) {
220             cleanDirectBuffer(buffer);
221         }
222     }
223 
224     /**
225      * In Java 8, the type of {@code sun.nio.ch.DirectBuffer.cleaner()} was {@code sun.misc.Cleaner}, and it was possible to access the method
226      * {@code sun.misc.Cleaner.clean()} to invoke it. The type changed to {@code jdk.internal.ref.Cleaner} in later JDKs, and the {@code clean()} method is not
227      * accessible even with reflection. However {@code sun.misc.Unsafe} added an {@code invokeCleaner()} method in JDK 9+ and this is still accessible with
228      * reflection.
229      *
230      * @param buffer the buffer to clean. must be a DirectBuffer.
231      */
232     private void cleanDirectBuffer(final ByteBuffer buffer) {
233         if (ByteBufferCleaner.isSupported()) {
234             ByteBufferCleaner.clean(buffer);
235         }
236     }
237 
238     @Override
239     public synchronized void close() throws IOException {
240         try {
241             fileChannel.close();
242         } finally {
243             clean(byteBuffer);
244         }
245     }
246 
247     @Override
248     public synchronized int read() throws IOException {
249         if (!refill()) {
250             return EOF;
251         }
252         return byteBuffer.get() & 0xFF;
253     }
254 
255     @Override
256     public synchronized int read(final byte[] b, final int offset, int len) throws IOException {
257         if (offset < 0 || len < 0 || offset + len < 0 || offset + len > b.length) {
258             throw new IndexOutOfBoundsException();
259         }
260         if (!refill()) {
261             return EOF;
262         }
263         len = Math.min(len, byteBuffer.remaining());
264         byteBuffer.get(b, offset, len);
265         return len;
266     }
267 
268     /**
269      * Checks whether data is left to be read from the input stream.
270      *
271      * @return true if data is left, false otherwise
272      * @throws IOException if an I/O error occurs.
273      */
274     private boolean refill() throws IOException {
275         Input.checkOpen(fileChannel.isOpen());
276         if (!byteBuffer.hasRemaining()) {
277             byteBuffer.clear();
278             int nRead = 0;
279             while (nRead == 0) {
280                 nRead = fileChannel.read(byteBuffer);
281             }
282             byteBuffer.flip();
283             return nRead >= 0;
284         }
285         return true;
286     }
287 
288     @Override
289     public synchronized long skip(final long n) throws IOException {
290         if (n <= 0L) {
291             return 0L;
292         }
293         if (byteBuffer.remaining() >= n) {
294             // The buffered content is enough to skip
295             byteBuffer.position(byteBuffer.position() + (int) n);
296             return n;
297         }
298         final long skippedFromBuffer = byteBuffer.remaining();
299         final long toSkipFromFileChannel = n - skippedFromBuffer;
300         // Discard everything we have read in the buffer.
301         byteBuffer.position(0);
302         byteBuffer.flip();
303         return skippedFromBuffer + skipFromFileChannel(toSkipFromFileChannel);
304     }
305 
306     private long skipFromFileChannel(final long n) throws IOException {
307         final long currentFilePosition = fileChannel.position();
308         final long size = fileChannel.size();
309         if (n > size - currentFilePosition) {
310             fileChannel.position(size);
311             return size - currentFilePosition;
312         }
313         fileChannel.position(currentFilePosition + n);
314         return n;
315     }
316 
317 }