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 */
017package org.apache.commons.io.input.buffer;
018
019import java.util.Objects;
020
021import org.apache.commons.io.IOUtils;
022
023/**
024 * A buffer, which doesn't need reallocation of byte arrays, because it
025 * reuses a single byte array. This works particularly well, if reading
026 * from the buffer takes place at the same time than writing to. Such is the
027 * case, for example, when using the buffer within a filtering input stream,
028 * like the {@link CircularBufferInputStream}.
029 */
030public class CircularByteBuffer {
031    private final byte[] buffer;
032    private int startOffset, endOffset, currentNumberOfBytes;
033
034    /**
035     * Creates a new instance with the given buffer size.
036     *
037     * @param pSize the size of buffer to create
038     */
039    public CircularByteBuffer(final int pSize) {
040        buffer = new byte[pSize];
041        startOffset = 0;
042        endOffset = 0;
043        currentNumberOfBytes = 0;
044    }
045
046    /**
047     * Creates a new instance with a reasonable default buffer size ({@link IOUtils#DEFAULT_BUFFER_SIZE}).
048     */
049    public CircularByteBuffer() {
050        this(IOUtils.DEFAULT_BUFFER_SIZE);
051    }
052
053    /**
054     * Returns the next byte from the buffer, removing it at the same time, so
055     * that following invocations won't return it again.
056     *
057     * @return The byte, which is being returned.
058     * @throws IllegalStateException The buffer is empty. Use {@link #hasBytes()},
059     *                               or {@link #getCurrentNumberOfBytes()}, to prevent this exception.
060     */
061    public byte read() {
062        if (currentNumberOfBytes <= 0) {
063            throw new IllegalStateException("No bytes available.");
064        }
065        final byte b = buffer[startOffset];
066        --currentNumberOfBytes;
067        if (++startOffset == buffer.length) {
068            startOffset = 0;
069        }
070        return b;
071    }
072
073    /**
074     * Returns the given number of bytes from the buffer by storing them in
075     * the given byte array at the given offset.
076     *
077     * @param targetBuffer The byte array, where to add bytes.
078     * @param targetOffset The offset, where to store bytes in the byte array.
079     * @param length The number of bytes to return.
080     * @throws NullPointerException     The byte array {@code pBuffer} is null.
081     * @throws IllegalArgumentException Either of {@code pOffset}, or {@code pLength} is negative,
082     *                                  or the length of the byte array {@code pBuffer} is too small.
083     * @throws IllegalStateException    The buffer doesn't hold the given number
084     *                                  of bytes. Use {@link #getCurrentNumberOfBytes()} to prevent this
085     *                                  exception.
086     */
087    public void read(final byte[] targetBuffer, final int targetOffset, final int length) {
088        Objects.requireNonNull(targetBuffer);
089        if (targetOffset < 0 || targetOffset >= targetBuffer.length) {
090            throw new IllegalArgumentException("Invalid offset: " + targetOffset);
091        }
092        if (length < 0 || length > buffer.length) {
093            throw new IllegalArgumentException("Invalid length: " + length);
094        }
095        if (targetOffset + length > targetBuffer.length) {
096            throw new IllegalArgumentException("The supplied byte array contains only "
097                    + targetBuffer.length + " bytes, but offset, and length would require "
098                    + (targetOffset + length - 1));
099        }
100        if (currentNumberOfBytes < length) {
101            throw new IllegalStateException("Currently, there are only " + currentNumberOfBytes
102                    + "in the buffer, not " + length);
103        }
104        int offset = targetOffset;
105        for (int i = 0; i < length; i++) {
106            targetBuffer[offset++] = buffer[startOffset];
107            --currentNumberOfBytes;
108            if (++startOffset == buffer.length) {
109                startOffset = 0;
110            }
111        }
112    }
113
114    /**
115     * Adds a new byte to the buffer, which will eventually be returned by following
116     * invocations of {@link #read()}.
117     *
118     * @param value The byte, which is being added to the buffer.
119     * @throws IllegalStateException The buffer is full. Use {@link #hasSpace()},
120     *                               or {@link #getSpace()}, to prevent this exception.
121     */
122    public void add(final byte value) {
123        if (currentNumberOfBytes >= buffer.length) {
124            throw new IllegalStateException("No space available");
125        }
126        buffer[endOffset] = value;
127        ++currentNumberOfBytes;
128        if (++endOffset == buffer.length) {
129            endOffset = 0;
130        }
131    }
132
133    /**
134     * Returns, whether the next bytes in the buffer are exactly those, given by
135     * {@code sourceBuffer}, {@code offset}, and {@code length}. No bytes are being
136     * removed from the buffer. If the result is true, then the following invocations
137     * of {@link #read()} are guaranteed to return exactly those bytes.
138     *
139     * @param sourceBuffer the buffer to compare against
140     * @param offset start offset
141     * @param length length to compare
142     * @return True, if the next invocations of {@link #read()} will return the
143     * bytes at offsets {@code pOffset}+0, {@code pOffset}+1, ...,
144     * {@code pOffset}+{@code pLength}-1 of byte array {@code pBuffer}.
145     * @throws IllegalArgumentException Either of {@code pOffset}, or {@code pLength} is negative.
146     * @throws NullPointerException     The byte array {@code pBuffer} is null.
147     */
148    public boolean peek(final byte[] sourceBuffer, final int offset, final int length) {
149        Objects.requireNonNull(sourceBuffer, "Buffer");
150        if (offset < 0 || offset >= sourceBuffer.length) {
151            throw new IllegalArgumentException("Invalid offset: " + offset);
152        }
153        if (length < 0 || length > buffer.length) {
154            throw new IllegalArgumentException("Invalid length: " + length);
155        }
156        if (length < currentNumberOfBytes) {
157            return false;
158        }
159        int localOffset = startOffset;
160        for (int i = 0; i < length; i++) {
161            if (buffer[localOffset] != sourceBuffer[i + offset]) {
162                return false;
163            }
164            if (++localOffset == buffer.length) {
165                localOffset = 0;
166            }
167        }
168        return true;
169    }
170
171    /**
172     * Adds the given bytes to the buffer. This is the same as invoking {@link #add(byte)}
173     * for the bytes at offsets {@code offset+0}, {@code offset+1}, ...,
174     * {@code offset+length-1} of byte array {@code targetBuffer}.
175     *
176     * @param targetBuffer the buffer to copy
177     * @param offset start offset
178     * @param length length to copy
179     * @throws IllegalStateException    The buffer doesn't have sufficient space. Use
180     *                                  {@link #getSpace()} to prevent this exception.
181     * @throws IllegalArgumentException Either of {@code pOffset}, or {@code pLength} is negative.
182     * @throws NullPointerException     The byte array {@code pBuffer} is null.
183     */
184    public void add(final byte[] targetBuffer, final int offset, final int length) {
185        Objects.requireNonNull(targetBuffer, "Buffer");
186        if (offset < 0 || offset >= targetBuffer.length) {
187            throw new IllegalArgumentException("Invalid offset: " + offset);
188        }
189        if (length < 0) {
190            throw new IllegalArgumentException("Invalid length: " + length);
191        }
192        if (currentNumberOfBytes + length > buffer.length) {
193            throw new IllegalStateException("No space available");
194        }
195        for (int i = 0; i < length; i++) {
196            buffer[endOffset] = targetBuffer[offset + i];
197            if (++endOffset == buffer.length) {
198                endOffset = 0;
199            }
200        }
201        currentNumberOfBytes += length;
202    }
203
204    /**
205     * Returns, whether there is currently room for a single byte in the buffer.
206     * Same as {@link #hasSpace(int) hasSpace(1)}.
207     *
208     * @return true if there is space for a byte
209     * @see #hasSpace(int)
210     * @see #getSpace()
211     */
212    public boolean hasSpace() {
213        return currentNumberOfBytes < buffer.length;
214    }
215
216    /**
217     * Returns, whether there is currently room for the given number of bytes in the buffer.
218     *
219     * @param count the byte count
220     * @return true if there is space for the given number of bytes
221     * @see #hasSpace()
222     * @see #getSpace()
223     */
224    public boolean hasSpace(final int count) {
225        return currentNumberOfBytes + count <= buffer.length;
226    }
227
228    /**
229     * Returns, whether the buffer is currently holding, at least, a single byte.
230     *
231     * @return true if the buffer is not empty
232     */
233    public boolean hasBytes() {
234        return currentNumberOfBytes > 0;
235    }
236
237    /**
238     * Returns the number of bytes, that can currently be added to the buffer.
239     *
240     * @return the number of bytes that can be added
241     */
242    public int getSpace() {
243        return buffer.length - currentNumberOfBytes;
244    }
245
246    /**
247     * Returns the number of bytes, that are currently present in the buffer.
248     *
249     * @return the number of bytes
250     */
251    public int getCurrentNumberOfBytes() {
252        return currentNumberOfBytes;
253    }
254
255    /**
256     * Removes all bytes from the buffer.
257     */
258    public void clear() {
259        startOffset = 0;
260        endOffset = 0;
261        currentNumberOfBytes = 0;
262    }
263}