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.SeekableByteChannel;
028import java.util.Arrays;
029import java.util.Objects;
030import java.util.concurrent.locks.ReentrantLock;
031
032import org.apache.commons.io.IOUtils;
033
034/**
035 * A {@link SeekableByteChannel} implementation backed by a byte array.
036 * <p>
037 * 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}
038 * 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
039 * accessed via {@link ByteArraySeekableByteChannel#array()}.
040 * </p>
041 *
042 * @since 2.21.0
043 */
044public class ByteArraySeekableByteChannel implements SeekableByteChannel {
045
046    private static final int RESIZE_LIMIT = Integer.MAX_VALUE >> 1;
047
048    /**
049     * Constructs a new channel backed directly by the given byte array.
050     *
051     * <p>The channel initially contains the full contents of the array, with its
052     * size set to {@code bytes.length} and its position set to {@code 0}.</p>
053     *
054     * <p>Reads and writes operate on the shared array.
055     * If a write operation extends beyond the current capacity, the channel will
056     * automatically allocate a larger backing array and copy the existing contents.</p>
057     *
058     * @param bytes The byte array to wrap, must not be {@code null}
059     * @return A new channel that uses the given array as its initial backing store
060     * @throws NullPointerException If {@code bytes} is {@code null}
061     * @see #array()
062     * @see ByteArrayInputStream#ByteArrayInputStream(byte[])
063     */
064    public static ByteArraySeekableByteChannel wrap(final byte[] bytes) {
065        Objects.requireNonNull(bytes, "bytes");
066        return new ByteArraySeekableByteChannel(bytes);
067    }
068
069    private byte[] data;
070    private volatile boolean closed;
071    private int position;
072    private int size;
073    private final ReentrantLock lock = new ReentrantLock();
074
075    /**
076     * Constructs a new instance, with a default internal buffer capacity.
077     * <p>
078     * The initial size and position of the channel are 0.
079     * </p>
080     *
081     * @see ByteArrayOutputStream#ByteArrayOutputStream()
082     */
083    public ByteArraySeekableByteChannel() {
084        this(IOUtils.DEFAULT_BUFFER_SIZE);
085    }
086
087    private ByteArraySeekableByteChannel(final byte[] data) {
088        this.data = data;
089        this.position = 0;
090        this.size = data.length;
091    }
092
093    /**
094     * Constructs a new instance, with an internal buffer of the given capacity, in bytes.
095     * <p>
096     * The initial size and position of the channel are 0.
097     * </p>
098     *
099     * @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}