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 * https://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 targetBuffer} 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 sourceBuffer}+0, {@code offset}+1, ...,
181 * {@code offset}+{@code length}-1 of byte array {@code sourceBuffer}.
182 * @throws IllegalArgumentException Either of {@code sourceBuffer}, or {@code length} is negative.
183 * @throws NullPointerException The byte array {@code sourceBuffer} 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 targetBuffer} is null.
236 * @throws IllegalArgumentException Either of {@code targetOffset}, 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 }