View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.io.input.buffer;
18  
19  import java.util.Objects;
20  
21  import org.apache.commons.io.IOUtils;
22  
23  /**
24   * A buffer, which doesn't need reallocation of byte arrays, because it
25   * reuses a single byte array. This works particularly well, if reading
26   * from the buffer takes place at the same time than writing to. Such is the
27   * case, for example, when using the buffer within a filtering input stream,
28   * like the {@link CircularBufferInputStream}.
29   *
30   * @since 2.7
31   */
32  public class CircularByteBuffer {
33  
34      private final byte[] buffer;
35      private int startOffset;
36      private int endOffset;
37      private int currentNumberOfBytes;
38  
39      /**
40       * Constructs a new instance with a reasonable default buffer size ({@link IOUtils#DEFAULT_BUFFER_SIZE}).
41       */
42      public CircularByteBuffer() {
43          this(IOUtils.DEFAULT_BUFFER_SIZE);
44      }
45  
46      /**
47       * Constructs a new instance with the given buffer size.
48       *
49       * @param size the size of buffer to create
50       */
51      public CircularByteBuffer(final int size) {
52          buffer = IOUtils.byteArray(size);
53          startOffset = 0;
54          endOffset = 0;
55          currentNumberOfBytes = 0;
56      }
57  
58      /**
59       * Adds a new byte to the buffer, which will eventually be returned by following
60       * invocations of {@link #read()}.
61       *
62       * @param value The byte, which is being added to the buffer.
63       * @throws IllegalStateException The buffer is full. Use {@link #hasSpace()},
64       *                               or {@link #getSpace()}, to prevent this exception.
65       */
66      public void add(final byte value) {
67          if (currentNumberOfBytes >= buffer.length) {
68              throw new IllegalStateException("No space available");
69          }
70          buffer[endOffset] = value;
71          ++currentNumberOfBytes;
72          if (++endOffset == buffer.length) {
73              endOffset = 0;
74          }
75      }
76  
77      /**
78       * Adds the given bytes to the buffer. This is the same as invoking {@link #add(byte)}
79       * for the bytes at offsets {@code offset+0}, {@code offset+1}, ...,
80       * {@code offset+length-1} of byte array {@code targetBuffer}.
81       *
82       * @param targetBuffer the buffer to copy
83       * @param offset start offset
84       * @param length length to copy
85       * @throws IllegalStateException    The buffer doesn't have sufficient space. Use
86       *                                  {@link #getSpace()} to prevent this exception.
87       * @throws IllegalArgumentException Either of {@code offset}, or {@code length} is negative.
88       * @throws NullPointerException     The byte array {@code pBuffer} is null.
89       */
90      public void add(final byte[] targetBuffer, final int offset, final int length) {
91          Objects.requireNonNull(targetBuffer, "Buffer");
92          if (offset < 0 || offset >= targetBuffer.length) {
93              throw new IllegalArgumentException("Illegal offset: " + offset);
94          }
95          if (length < 0) {
96              throw new IllegalArgumentException("Illegal length: " + length);
97          }
98          if (currentNumberOfBytes + length > buffer.length) {
99              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 }