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