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 {@code 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() || !refill()) {
202 return 0;
203 }
204 return byteBuffer.remaining();
205 }
206
207 /**
208 * 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
209 * disposed buffer. However, neither the bytes allocated to direct buffers nor file descriptors opened for memory-mapped buffers put pressure on the garbage
210 * 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
211 * API to manually dispose of these kinds of buffers.
212 *
213 * @param buffer the buffer to clean.
214 */
215 private void clean(final ByteBuffer buffer) {
216 if (buffer.isDirect()) {
217 cleanDirectBuffer(buffer);
218 }
219 }
220
221 /**
222 * 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
223 * {@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
224 * accessible even with reflection. However {@code sun.misc.Unsafe} added an {@code invokeCleaner()} method in JDK 9+ and this is still accessible with
225 * reflection.
226 *
227 * @param buffer the buffer to clean. must be a DirectBuffer.
228 */
229 private void cleanDirectBuffer(final ByteBuffer buffer) {
230 if (ByteBufferCleaner.isSupported()) {
231 ByteBufferCleaner.clean(buffer);
232 }
233 }
234
235 @Override
236 public synchronized void close() throws IOException {
237 try {
238 fileChannel.close();
239 } finally {
240 clean(byteBuffer);
241 }
242 }
243
244 @Override
245 public synchronized int read() throws IOException {
246 if (!refill()) {
247 return EOF;
248 }
249 return byteBuffer.get() & 0xFF;
250 }
251
252 @Override
253 public synchronized int read(final byte[] b, final int offset, int len) throws IOException {
254 IOUtils.checkFromIndexSize(b, offset, len);
255 if (len == 0) {
256 return 0;
257 }
258 if (!refill()) {
259 return EOF;
260 }
261 len = Math.min(len, byteBuffer.remaining());
262 byteBuffer.get(b, offset, len);
263 return len;
264 }
265
266 /**
267 * Checks whether data is left to be read from the input stream.
268 *
269 * @return true if data is left, false otherwise.
270 * @throws IOException if an I/O error occurs.
271 */
272 private boolean refill() throws IOException {
273 Input.checkOpen(fileChannel.isOpen());
274 if (!byteBuffer.hasRemaining()) {
275 byteBuffer.clear();
276 int nRead = 0;
277 while (nRead == 0) {
278 nRead = fileChannel.read(byteBuffer);
279 }
280 byteBuffer.flip();
281 return nRead >= 0;
282 }
283 return true;
284 }
285
286 @Override
287 public synchronized long skip(final long n) throws IOException {
288 if (n <= 0L) {
289 return 0L;
290 }
291 if (byteBuffer.remaining() >= n) {
292 // The buffered content is enough to skip
293 byteBuffer.position(byteBuffer.position() + (int) n);
294 return n;
295 }
296 final long skippedFromBuffer = byteBuffer.remaining();
297 final long toSkipFromFileChannel = n - skippedFromBuffer;
298 // Discard everything we have read in the buffer.
299 byteBuffer.position(0);
300 byteBuffer.flip();
301 return skippedFromBuffer + skipFromFileChannel(toSkipFromFileChannel);
302 }
303
304 private long skipFromFileChannel(final long n) throws IOException {
305 final long currentFilePosition = fileChannel.position();
306 final long size = fileChannel.size();
307 if (n > size - currentFilePosition) {
308 fileChannel.position(size);
309 return size - currentFilePosition;
310 }
311 fileChannel.position(currentFilePosition + n);
312 return n;
313 }
314
315 }