001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019
020package org.apache.commons.io.channels;
021
022import java.io.ByteArrayInputStream;
023import java.io.ByteArrayOutputStream;
024import java.io.IOException;
025import java.nio.ByteBuffer;
026import java.nio.channels.ClosedChannelException;
027import java.nio.channels.NonWritableChannelException;
028import java.nio.channels.SeekableByteChannel;
029import java.nio.file.OpenOption;
030import java.nio.file.StandardOpenOption;
031import java.util.Arrays;
032import java.util.Objects;
033import java.util.concurrent.locks.ReentrantLock;
034
035import org.apache.commons.io.IOUtils;
036import org.apache.commons.io.build.AbstractStreamBuilder;
037
038/**
039 * A {@link SeekableByteChannel} implementation backed by a byte array.
040 * <p>
041 * 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}
042 * 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
043 * accessed via {@link ByteArraySeekableByteChannel#array()}.
044 * </p>
045 * <p>
046 * Building a read-only channel from an existing byte array is supported with:
047 * </p>
048 * <pre>{@code
049 * try (ByteArraySeekableByteChannel channel = ByteArraySeekableByteChannel.builder()
050 *               .setByteArray(...)
051 *               .setOpenOptions(StandardOpenOption.READ)
052 *               .get()) {
053 *               // read from channel
054 * }
055 * }</pre>
056 *
057 * @since 2.21.0
058 */
059public class ByteArraySeekableByteChannel implements SeekableByteChannel {
060
061    /**
062     * Builds for {@link ByteArraySeekableByteChannel}.
063     * <p>
064     * Building a read-only channel from an existing byte array is supported with:
065     * </p>
066     * <pre>{@code
067     * try (ByteArraySeekableByteChannel channel = ByteArraySeekableByteChannel.builder()
068     *               .setByteArray(...)
069     *               .setOpenOptions(StandardOpenOption.READ)
070     *               .get()) {
071     *               // read from channel
072     * }
073     * }</pre>
074     *
075     * @since 2.22.0
076     */
077    public static class Builder extends AbstractStreamBuilder<ByteArraySeekableByteChannel, Builder> {
078
079        /**
080         * Constructs a new builder for {@link ByteArraySeekableByteChannel}.
081         */
082        public Builder() {
083            setByteArray(IOUtils.EMPTY_BYTE_ARRAY);
084        }
085
086        @Override
087        public ByteArraySeekableByteChannel get() throws IOException {
088            return new ByteArraySeekableByteChannel(this);
089        }
090    }
091
092    private static final int RESIZE_LIMIT = Integer.MAX_VALUE >> 1;
093
094    /**
095     * Constructs a new builder for {@link ByteArraySeekableByteChannel}.
096     *
097     * @return a new builder for {@link ByteArraySeekableByteChannel}.
098     * @since 2.22.0
099     */
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}