View Javadoc
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 }