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.SeekableByteChannel;
28  import java.util.Arrays;
29  import java.util.Objects;
30  import java.util.concurrent.locks.ReentrantLock;
31  
32  import org.apache.commons.io.IOUtils;
33  
34  /**
35   * A {@link SeekableByteChannel} implementation backed by a byte array.
36   * <p>
37   * 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}
38   * 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
39   * accessed via {@link ByteArraySeekableByteChannel#array()}.
40   * </p>
41   *
42   * @since 2.21.0
43   */
44  public class ByteArraySeekableByteChannel implements SeekableByteChannel {
45  
46      private static final int RESIZE_LIMIT = Integer.MAX_VALUE >> 1;
47  
48      /**
49       * Constructs a new channel backed directly by the given byte array.
50       *
51       * <p>The channel initially contains the full contents of the array, with its
52       * size set to {@code bytes.length} and its position set to {@code 0}.</p>
53       *
54       * <p>Reads and writes operate on the shared array.
55       * If a write operation extends beyond the current capacity, the channel will
56       * automatically allocate a larger backing array and copy the existing contents.</p>
57       *
58       * @param bytes The byte array to wrap, must not be {@code null}
59       * @return A new channel that uses the given array as its initial backing store
60       * @throws NullPointerException If {@code bytes} is {@code null}
61       * @see #array()
62       * @see ByteArrayInputStream#ByteArrayInputStream(byte[])
63       */
64      public static ByteArraySeekableByteChannel wrap(final byte[] bytes) {
65          Objects.requireNonNull(bytes, "bytes");
66          return new ByteArraySeekableByteChannel(bytes);
67      }
68  
69      private byte[] data;
70      private volatile boolean closed;
71      private int position;
72      private int size;
73      private final ReentrantLock lock = new ReentrantLock();
74  
75      /**
76       * Constructs a new instance, with a default internal buffer capacity.
77       * <p>
78       * The initial size and position of the channel are 0.
79       * </p>
80       *
81       * @see ByteArrayOutputStream#ByteArrayOutputStream()
82       */
83      public ByteArraySeekableByteChannel() {
84          this(IOUtils.DEFAULT_BUFFER_SIZE);
85      }
86  
87      private ByteArraySeekableByteChannel(final byte[] data) {
88          this.data = data;
89          this.position = 0;
90          this.size = data.length;
91      }
92  
93      /**
94       * Constructs a new instance, with an internal buffer of the given capacity, in bytes.
95       * <p>
96       * The initial size and position of the channel are 0.
97       * </p>
98       *
99       * @param size Capacity of the internal buffer to allocate, in bytes.
100      * @see ByteArrayOutputStream#ByteArrayOutputStream(int)
101      */
102     public ByteArraySeekableByteChannel(final int size) {
103         if (size < 0) {
104             throw new IllegalArgumentException("Size must be non-negative");
105         }
106         this.data = new byte[size];
107         this.position = 0;
108         this.size = 0;
109     }
110 
111     /**
112      * Gets the raw byte array backing this channel, <em>this is not a copy</em>.
113      * <p>
114      * NOTE: The returned buffer is not aligned with containing data, use {@link #size()} to obtain the size of data stored in the buffer.
115      * </p>
116      *
117      * @return internal byte array.
118      */
119     public byte[] array() {
120         return data;
121     }
122 
123     private void checkOpen() throws ClosedChannelException {
124         if (!isOpen()) {
125             throw new ClosedChannelException();
126         }
127     }
128 
129     private int checkRange(final long newSize, final String method) {
130         if (newSize < 0L || newSize > IOUtils.SOFT_MAX_ARRAY_LENGTH) {
131             throw new IllegalArgumentException(String.format("%s must be in range [0..%,d]: %,d", method, IOUtils.SOFT_MAX_ARRAY_LENGTH, newSize));
132         }
133         return (int) newSize;
134     }
135 
136     @Override
137     public void close() {
138         closed = true;
139     }
140 
141     /**
142      * Like {@link #size()} but never throws {@link ClosedChannelException}.
143      *
144      * @return See {@link #size()}.
145      */
146     public long getSize() {
147         return size;
148     }
149 
150     @Override
151     public boolean isOpen() {
152         return !closed;
153     }
154 
155     @Override
156     public long position() throws ClosedChannelException {
157         checkOpen();
158         lock.lock();
159         try {
160             return position;
161         } finally {
162             lock.unlock();
163         }
164     }
165 
166     @Override
167     public SeekableByteChannel position(final long newPosition) throws IOException {
168         checkOpen();
169         final int intPos = checkRange(newPosition, "position()");
170         lock.lock();
171         try {
172             position = intPos;
173         } finally {
174             lock.unlock();
175         }
176         return this;
177     }
178 
179     @Override
180     public int read(final ByteBuffer buf) throws IOException {
181         checkOpen();
182         lock.lock();
183         try {
184             int wanted = buf.remaining();
185             final int possible = size - position;
186             if (possible <= 0) {
187                 return IOUtils.EOF;
188             }
189             if (wanted > possible) {
190                 wanted = possible;
191             }
192             buf.put(data, position, wanted);
193             position += wanted;
194             return wanted;
195         } finally {
196             lock.unlock();
197         }
198     }
199 
200     private void resize(final int newLength) {
201         int len = data.length;
202         if (len == 0) {
203             len = 1;
204         }
205         if (newLength < RESIZE_LIMIT) {
206             while (len < newLength) {
207                 len <<= 1;
208             }
209         } else { // avoid overflow
210             len = newLength;
211         }
212         data = Arrays.copyOf(data, len);
213     }
214 
215     @Override
216     public long size() throws ClosedChannelException {
217         checkOpen();
218         lock.lock();
219         try {
220             return size;
221         } finally {
222             lock.unlock();
223         }
224     }
225 
226     /**
227      * Gets a copy of the data stored in this channel.
228      * <p>
229      * The returned array is a copy of the internal buffer, sized to the actual data stored in this channel.
230      * </p>
231      *
232      * @return a new byte array containing the data stored in this channel.
233      */
234     public byte[] toByteArray() {
235         return Arrays.copyOf(data, size);
236     }
237 
238     @Override
239     public SeekableByteChannel truncate(final long newSize) throws ClosedChannelException {
240         checkOpen();
241         final int intSize = checkRange(newSize, "truncate()");
242         lock.lock();
243         try {
244             if (size > intSize) {
245                 size = intSize;
246             }
247             if (position > intSize) {
248                 position = intSize;
249             }
250         } finally {
251             lock.unlock();
252         }
253         return this;
254     }
255 
256     @Override
257     public int write(final ByteBuffer b) throws IOException {
258         checkOpen();
259         lock.lock();
260         try {
261             final int wanted = b.remaining();
262             final int possibleWithoutResize = Math.max(0, size - position);
263             if (wanted > possibleWithoutResize) {
264                 final int newSize = position + wanted;
265                 if (newSize < 0 || newSize > IOUtils.SOFT_MAX_ARRAY_LENGTH) { // overflow
266                     throw new OutOfMemoryError("required array size " + Integer.toUnsignedString(newSize) + " too large");
267                 }
268                 resize(newSize);
269             }
270             b.get(data, position, wanted);
271             position += wanted;
272             if (size < position) {
273                 size = position;
274             }
275             return wanted;
276         } finally {
277             lock.unlock();
278         }
279     }
280 }