1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * https://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20 package org.apache.commons.io.channels;
21
22 import java.io.ByteArrayInputStream;
23 import java.io.ByteArrayOutputStream;
24 import java.io.IOException;
25 import java.nio.ByteBuffer;
26 import java.nio.channels.ClosedChannelException;
27 import java.nio.channels.NonWritableChannelException;
28 import java.nio.channels.SeekableByteChannel;
29 import java.nio.file.OpenOption;
30 import java.nio.file.StandardOpenOption;
31 import java.util.Arrays;
32 import java.util.Objects;
33 import java.util.concurrent.locks.ReentrantLock;
34
35 import org.apache.commons.io.IOUtils;
36 import org.apache.commons.io.build.AbstractStreamBuilder;
37
38 /**
39 * A {@link SeekableByteChannel} implementation backed by a byte array.
40 * <p>
41 * When used for writing, the internal buffer grows to accommodate incoming data. The natural size limit is the value of {@link IOUtils#SOFT_MAX_ARRAY_LENGTH}
42 * and it's not possible to {@link #position(long) set the position} or {@link #truncate(long) truncate} to a value bigger than that. The raw internal buffer is
43 * accessed via {@link ByteArraySeekableByteChannel#array()}.
44 * </p>
45 * <p>
46 * Building a read-only channel from an existing byte array is supported with:
47 * </p>
48 * <pre>{@code
49 * try (ByteArraySeekableByteChannel channel = ByteArraySeekableByteChannel.builder()
50 * .setByteArray(...)
51 * .setOpenOptions(StandardOpenOption.READ)
52 * .get()) {
53 * // read from channel
54 * }
55 * }</pre>
56 *
57 * @since 2.21.0
58 */
59 public class ByteArraySeekableByteChannel implements SeekableByteChannel {
60
61 /**
62 * Builds for {@link ByteArraySeekableByteChannel}.
63 * <p>
64 * Building a read-only channel from an existing byte array is supported with:
65 * </p>
66 * <pre>{@code
67 * try (ByteArraySeekableByteChannel channel = ByteArraySeekableByteChannel.builder()
68 * .setByteArray(...)
69 * .setOpenOptions(StandardOpenOption.READ)
70 * .get()) {
71 * // read from channel
72 * }
73 * }</pre>
74 *
75 * @since 2.22.0
76 */
77 public static class Builder extends AbstractStreamBuilder<ByteArraySeekableByteChannel, Builder> {
78
79 /**
80 * Constructs a new builder for {@link ByteArraySeekableByteChannel}.
81 */
82 public Builder() {
83 setByteArray(IOUtils.EMPTY_BYTE_ARRAY);
84 }
85
86 @Override
87 public ByteArraySeekableByteChannel get() throws IOException {
88 return new ByteArraySeekableByteChannel(this);
89 }
90 }
91
92 private static final int RESIZE_LIMIT = Integer.MAX_VALUE >> 1;
93
94 /**
95 * Constructs a new builder for {@link ByteArraySeekableByteChannel}.
96 *
97 * @return a new builder for {@link ByteArraySeekableByteChannel}.
98 * @since 2.22.0
99 */
100 public static Builder builder() {
101 return new Builder();
102 }
103
104 /**
105 * Constructs a new channel backed directly by the given byte array.
106 *
107 * <p>
108 * The channel initially contains the full contents of the array, with its size set to {@code bytes.length} and its position set to {@code 0}.
109 * </p>
110 *
111 * <p>
112 * Reads and writes operate on the shared array. If a write operation extends beyond the current capacity, the channel will automatically allocate a larger
113 * backing array and copy the existing contents.
114 * </p>
115 *
116 * @param bytes The byte array to wrap, must not be {@code null}
117 * @return A new channel that uses the given array as its initial backing store.
118 * @throws NullPointerException If {@code bytes} is {@code null}
119 * @see #array()
120 * @see ByteArrayInputStream#ByteArrayInputStream(byte[])
121 */
122 public static ByteArraySeekableByteChannel wrap(final byte[] bytes) {
123 Objects.requireNonNull(bytes, "bytes");
124 return new ByteArraySeekableByteChannel(bytes);
125 }
126 private byte[] data;
127 private volatile boolean closed;
128 private long position;
129 private int size;
130 private final boolean isWritable;
131 private final ReentrantLock lock = new ReentrantLock();
132
133 /**
134 * Constructs a new instance, with a default internal buffer capacity.
135 * <p>
136 * The initial size and position of the channel are 0.
137 * </p>
138 *
139 * @see ByteArrayOutputStream#ByteArrayOutputStream()
140 */
141 public ByteArraySeekableByteChannel() {
142 this(IOUtils.DEFAULT_BUFFER_SIZE);
143 }
144
145 private ByteArraySeekableByteChannel(final Builder builder) throws IOException {
146 this.data = builder.getByteArray();
147 this.size = data.length;
148 final OpenOption[] openOptions = builder.getOpenOptions();
149 Arrays.sort(openOptions);
150 this.isWritable = openOptions.length == 0 || Arrays.binarySearch(openOptions, StandardOpenOption.WRITE) >= 0
151 || Arrays.binarySearch(openOptions, StandardOpenOption.APPEND) >= 0;
152 }
153
154 private ByteArraySeekableByteChannel(final byte[] data) {
155 this.data = data;
156 this.size = data.length;
157 this.isWritable = true;
158 }
159
160 /**
161 * Constructs a new instance, with an internal buffer of the given capacity, in bytes.
162 * <p>
163 * The initial size and position of the channel are 0.
164 * </p>
165 *
166 * @param size Capacity of the internal buffer to allocate, in bytes.
167 * @see ByteArrayOutputStream#ByteArrayOutputStream(int)
168 */
169 public ByteArraySeekableByteChannel(final int size) {
170 if (size < 0) {
171 throw new IllegalArgumentException("Size must be non-negative");
172 }
173 this.data = new byte[size];
174 this.isWritable = true;
175 }
176
177 /**
178 * Gets the raw byte array backing this channel, <em>this is not a copy</em>.
179 * <p>
180 * NOTE: The returned buffer is not aligned with containing data, use {@link #size()} to obtain the size of data stored in the buffer.
181 * </p>
182 *
183 * @return internal byte array.
184 */
185 public byte[] array() {
186 return data;
187 }
188
189 private void checkOpen() throws ClosedChannelException {
190 if (!isOpen()) {
191 throw new ClosedChannelException();
192 }
193 }
194
195 private void checkRange(final long newSize, final String method) {
196 if (newSize < 0L) {
197 throw new IllegalArgumentException(String.format("%s must be positive: %,d", method, newSize));
198 }
199 }
200
201 private void checkWritable() {
202 if (!isWritable) {
203 throw new NonWritableChannelException();
204 }
205 }
206
207 @Override
208 public void close() {
209 closed = true;
210 }
211
212 /**
213 * Like {@link #size()} but never throws {@link ClosedChannelException}.
214 *
215 * @return See {@link #size()}.
216 */
217 public long getSize() {
218 return size;
219 }
220
221 @Override
222 public boolean isOpen() {
223 return !closed;
224 }
225
226 @Override
227 public long position() throws ClosedChannelException {
228 checkOpen();
229 lock.lock();
230 try {
231 return position;
232 } finally {
233 lock.unlock();
234 }
235 }
236
237 @Override
238 public SeekableByteChannel position(final long newPosition) throws IOException {
239 checkOpen();
240 checkRange(newPosition, "position()");
241 lock.lock();
242 try {
243 position = newPosition;
244 } finally {
245 lock.unlock();
246 }
247 return this;
248 }
249
250 @Override
251 public int read(final ByteBuffer buf) throws IOException {
252 checkOpen();
253 lock.lock();
254 try {
255 if (position > Integer.MAX_VALUE) {
256 return IOUtils.EOF;
257 }
258 int wanted = buf.remaining();
259 final int possible = size - (int) position;
260 if (possible <= 0) {
261 return IOUtils.EOF;
262 }
263 if (wanted > possible) {
264 wanted = possible;
265 }
266 buf.put(data, (int) position, wanted);
267 position += wanted;
268 return wanted;
269 } finally {
270 lock.unlock();
271 }
272 }
273
274 private void resize(final int newLength) {
275 int len = data.length;
276 if (len == 0) {
277 len = 1;
278 }
279 if (newLength < RESIZE_LIMIT) {
280 while (len < newLength) {
281 len <<= 1;
282 }
283 } else { // avoid overflow
284 len = newLength;
285 }
286 data = Arrays.copyOf(data, len);
287 }
288
289 @Override
290 public long size() throws ClosedChannelException {
291 checkOpen();
292 lock.lock();
293 try {
294 return size;
295 } finally {
296 lock.unlock();
297 }
298 }
299
300 /**
301 * Gets a copy of the data stored in this channel.
302 * <p>
303 * The returned array is a copy of the internal buffer, sized to the actual data stored in this channel.
304 * </p>
305 *
306 * @return a new byte array containing the data stored in this channel.
307 */
308 public byte[] toByteArray() {
309 return Arrays.copyOf(data, size);
310 }
311
312 @Override
313 public SeekableByteChannel truncate(final long newSize) throws ClosedChannelException {
314 checkOpen();
315 checkWritable();
316 checkRange(newSize, "truncate()");
317 lock.lock();
318 try {
319 if (size > newSize) {
320 size = (int) newSize;
321 }
322 if (position > newSize) {
323 position = newSize;
324 }
325 } finally {
326 lock.unlock();
327 }
328 return this;
329 }
330
331 @Override
332 public int write(final ByteBuffer b) throws IOException {
333 checkOpen();
334 checkWritable();
335 //
336 if (position > Integer.MAX_VALUE) {
337 throw new IOException("position > Integer.MAX_VALUE");
338 }
339 lock.lock();
340 try {
341 final int wanted = b.remaining();
342 // intPos <= Integer.MAX_VALUE
343 final int intPos = (int) position;
344 final long newPosition = position + wanted;
345 if (newPosition > IOUtils.SOFT_MAX_ARRAY_LENGTH) {
346 throw new IOException(String.format("Requested array size %,d is too large.", newPosition));
347 }
348 if (newPosition > size) {
349 final int newPositionInt = (int) newPosition;
350 // Ensure that newPositionInt ≤ data.length
351 resize(newPositionInt);
352 size = newPositionInt;
353 }
354 b.get(data, intPos, wanted);
355 position = newPosition;
356 if (size < intPos) {
357 size = intPos;
358 }
359 return wanted;
360 } finally {
361 lock.unlock();
362 }
363 }
364 }