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