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