001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *   http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.compress.utils;
019
020import java.io.IOException;
021import java.nio.ByteBuffer;
022import java.nio.channels.ClosedChannelException;
023import java.nio.channels.SeekableByteChannel;
024import java.util.Arrays;
025import java.util.concurrent.atomic.AtomicBoolean;
026
027/**
028 * A {@link SeekableByteChannel} implementation that wraps a byte[].
029 *
030 * <p>When this channel is used for writing an internal buffer grows to accommodate incoming data. The natural size
031 * limit is the value of {@link Integer#MAX_VALUE} and it is not possible to {@link #position(long) set the position} or
032 * {@link #truncate truncate} to a value bigger than that.  Internal buffer can be accessed via {@link
033 * SeekableInMemoryByteChannel#array()}.</p>
034 *
035 * @since 1.13
036 * @NotThreadSafe
037 */
038public class SeekableInMemoryByteChannel implements SeekableByteChannel {
039
040    private static final int NAIVE_RESIZE_LIMIT = Integer.MAX_VALUE >> 1;
041
042    private byte[] data;
043    private final AtomicBoolean closed = new AtomicBoolean();
044    private int position, size;
045
046    /**
047     * Parameterless constructor - allocates internal buffer by itself.
048     */
049    public SeekableInMemoryByteChannel() {
050        this(ByteUtils.EMPTY_BYTE_ARRAY);
051    }
052
053    /**
054     * Constructor taking a byte array.
055     *
056     * <p>This constructor is intended to be used with pre-allocated buffer or when
057     * reading from a given byte array.</p>
058     *
059     * @param data input data or pre-allocated array.
060     */
061    public SeekableInMemoryByteChannel(final byte[] data) {
062        this.data = data;
063        this.size = data.length;
064    }
065
066    /**
067     * Constructor taking a size of storage to be allocated.
068     *
069     * <p>Creates a channel and allocates internal storage of a given size.</p>
070     *
071     * @param size size of internal buffer to allocate, in bytes.
072     */
073    public SeekableInMemoryByteChannel(final int size) {
074        this(new byte[size]);
075    }
076
077    /**
078     * Obtains the array backing this channel.
079     *
080     * <p>NOTE:
081     * The returned buffer is not aligned with containing data, use
082     * {@link #size()} to obtain the size of data stored in the buffer.</p>
083     *
084     * @return internal byte array.
085     */
086    public byte[] array() {
087        return data;
088    }
089
090    @Override
091    public void close() {
092        closed.set(true);
093    }
094
095    private void ensureOpen() throws ClosedChannelException {
096        if (!isOpen()) {
097            throw new ClosedChannelException();
098        }
099    }
100
101    @Override
102    public boolean isOpen() {
103        return !closed.get();
104    }
105
106    /**
107     * Returns this channel's position.
108     *
109     * <p>This method violates the contract of {@link SeekableByteChannel#position()} as it will not throw any exception
110     * when invoked on a closed channel. Instead it will return the position the channel had when close has been
111     * called.</p>
112     */
113    @Override
114    public long position() {
115        return position;
116    }
117
118    @Override
119    public SeekableByteChannel position(final long newPosition) throws IOException {
120        ensureOpen();
121        if (newPosition < 0L || newPosition > Integer.MAX_VALUE) {
122            throw new IOException("Position has to be in range 0.. " + Integer.MAX_VALUE);
123        }
124        position = (int) newPosition;
125        return this;
126    }
127
128    @Override
129    public int read(final ByteBuffer buf) throws IOException {
130        ensureOpen();
131        int wanted = buf.remaining();
132        final int possible = size - position;
133        if (possible <= 0) {
134            return -1;
135        }
136        if (wanted > possible) {
137            wanted = possible;
138        }
139        buf.put(data, position, wanted);
140        position += wanted;
141        return wanted;
142    }
143
144    private void resize(final int newLength) {
145        int len = data.length;
146        if (len <= 0) {
147            len = 1;
148        }
149        if (newLength < NAIVE_RESIZE_LIMIT) {
150            while (len < newLength) {
151                len <<= 1;
152            }
153        } else { // avoid overflow
154            len = newLength;
155        }
156        data = Arrays.copyOf(data, len);
157    }
158
159    /**
160     * Returns the current size of entity to which this channel is connected.
161     *
162     * <p>This method violates the contract of {@link SeekableByteChannel#size} as it will not throw any exception when
163     * invoked on a closed channel. Instead it will return the size the channel had when close has been called.</p>
164     */
165    @Override
166    public long size() {
167        return size;
168    }
169
170    /**
171     * Truncates the entity, to which this channel is connected, to the given size.
172     *
173     * <p>This method violates the contract of {@link SeekableByteChannel#truncate} as it will not throw any exception when
174     * invoked on a closed channel.</p>
175     *
176     * @throws IllegalArgumentException if size is negative or bigger than the maximum of a Java integer
177     */
178    @Override
179    public SeekableByteChannel truncate(final long newSize) {
180        if (newSize < 0L || newSize > Integer.MAX_VALUE) {
181            throw new IllegalArgumentException("Size has to be in range 0.. " + Integer.MAX_VALUE);
182        }
183        if (size > newSize) {
184            size = (int) newSize;
185        }
186        if (position > newSize) {
187            position = (int) newSize;
188        }
189        return this;
190    }
191
192    @Override
193    public int write(final ByteBuffer b) throws IOException {
194        ensureOpen();
195        int wanted = b.remaining();
196        final int possibleWithoutResize = size - position;
197        if (wanted > possibleWithoutResize) {
198            final int newSize = position + wanted;
199            if (newSize < 0) { // overflow
200                resize(Integer.MAX_VALUE);
201                wanted = Integer.MAX_VALUE - position;
202            } else {
203                resize(newSize);
204            }
205        }
206        b.get(data, position, wanted);
207        position += wanted;
208        if (size < position) {
209            size = position;
210        }
211        return wanted;
212    }
213
214}