Repository: commons-io Updated Branches: refs/heads/master 34d6eea40 -> 58b0f795b
Adding the PeekableInputStream. Project: http://git-wip-us.apache.org/repos/asf/commons-io/repo Commit: http://git-wip-us.apache.org/repos/asf/commons-io/commit/559de2c4 Tree: http://git-wip-us.apache.org/repos/asf/commons-io/tree/559de2c4 Diff: http://git-wip-us.apache.org/repos/asf/commons-io/diff/559de2c4 Branch: refs/heads/master Commit: 559de2c461e94ab636c959149c775bb27111fb48 Parents: 7b813b6 Author: Jochen Wiedmann (jwi) <jochen.wiedm...@softwareag.com> Authored: Fri Jul 27 16:43:35 2018 +0200 Committer: Jochen Wiedmann (jwi) <jochen.wiedm...@softwareag.com> Committed: Fri Jul 27 16:44:05 2018 +0200 ---------------------------------------------------------------------- src/changes/changes.xml | 3 + .../input/buffer/CircularBufferInputStream.java | 126 ++++++++++ .../io/input/buffer/CircularByteBuffer.java | 237 +++++++++++++++++++ .../io/input/buffer/PeekableInputStream.java | 87 +++++++ .../buffer/CircularBufferInputStreamTest.java | 83 +++++++ 5 files changed, 536 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/commons-io/blob/559de2c4/src/changes/changes.xml ---------------------------------------------------------------------- diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 9a140c0..fecb959 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -47,6 +47,9 @@ The <action> type attribute can be add,update,fix,remove. <body> <!-- The release date is the date RC is cut --> <release version="2.7" date="tba" description="tba"> + <action dev="jochen" type="add"> + Adding the CircularBufferInputStream, and the PeekableInputStream. + </action> <action issue="IO-582" dev="jochen" type="fix" due-to="Bruno Palos"> Make methods in ObservableInputStream.Obsever public. </action> http://git-wip-us.apache.org/repos/asf/commons-io/blob/559de2c4/src/main/java/org/apache/commons/io/input/buffer/CircularBufferInputStream.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/io/input/buffer/CircularBufferInputStream.java b/src/main/java/org/apache/commons/io/input/buffer/CircularBufferInputStream.java new file mode 100644 index 0000000..7298e5b --- /dev/null +++ b/src/main/java/org/apache/commons/io/input/buffer/CircularBufferInputStream.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input.buffer; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + + +/** + * Implementation of a buffered input stream, which is internally based on the + * {@link CircularByteBuffer}. Unlike the {@link BufferedInputStream}, this one + * doesn't need to reallocate byte arrays internally. + */ +public class CircularBufferInputStream extends InputStream { + protected final InputStream in; + protected final CircularByteBuffer buffer; + protected final int bufferSize; + private boolean eofSeen; + + /** Creates a new instance, which filters the given input stream, and + * uses the given buffer size. + * @param pIn The input stream, which is being buffered. + * @param pBufferSize The size of the {@link CircularByteBuffer}, which is + * used internally. + */ + public CircularBufferInputStream(InputStream pIn, int pBufferSize) { + Objects.requireNonNull(pIn, "InputStream"); + if (pBufferSize <= 0) { + throw new IllegalArgumentException("Invalid buffer size: " + pBufferSize); + } + in = pIn; + buffer = new CircularByteBuffer(pBufferSize); + bufferSize = pBufferSize; + eofSeen = false; + } + + /** Creates a new instance, which filters the given input stream, and + * uses a reasonable default buffer size (8192). + * @param pIn The input stream, which is being buffered. + * @param pBufferSize The size of the {@link CircularByteBuffer}, which is + * used internally. + */ + public CircularBufferInputStream(InputStream pIn) { + this(pIn, 8192); + } + + protected void fillBuffer() throws IOException { + if (eofSeen) { + return; + } + int space = buffer.getSpace(); + final byte[] buf = new byte[space]; + while (space > 0) { + final int res = in.read(buf, 0, space); + if (res == -1) { + eofSeen = true; + return; + } else if (res > 0) { + buffer.add(buf, 0, res); + space -= res; + } + } + } + + protected boolean haveBytes(int pNumber) throws IOException { + if (buffer.getCurrentNumberOfBytes() < pNumber) { + fillBuffer(); + } + return buffer.hasBytes(); + } + + @Override + public int read() throws IOException { + if (!haveBytes(1)) { + return -1; + } + return buffer.read(); + } + + @Override + public int read(byte[] pBuffer) throws IOException { + return read(pBuffer, 0, pBuffer.length); + } + + @Override + public int read(byte[] pBuffer, int pOffset, int pLength) throws IOException { + Objects.requireNonNull(pBuffer, "Buffer"); + if (pOffset < 0) { + throw new IllegalArgumentException("Offset must not be negative"); + } + if (pLength < 0) { + throw new IllegalArgumentException("Length must not be negative"); + } + if (!haveBytes(pLength)) { + return -1; + } + final int result = Math.min(pLength, buffer.getCurrentNumberOfBytes()); + for (int i = 0; i < result; i++) { + pBuffer[pOffset+i] = buffer.read(); + } + return result; + } + + @Override + public void close() throws IOException { + in.close(); + eofSeen = true; + buffer.clear(); + } +} http://git-wip-us.apache.org/repos/asf/commons-io/blob/559de2c4/src/main/java/org/apache/commons/io/input/buffer/CircularByteBuffer.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/io/input/buffer/CircularByteBuffer.java b/src/main/java/org/apache/commons/io/input/buffer/CircularByteBuffer.java new file mode 100644 index 0000000..52d84bb --- /dev/null +++ b/src/main/java/org/apache/commons/io/input/buffer/CircularByteBuffer.java @@ -0,0 +1,237 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input.buffer; + +import java.util.Objects; + +/** + * A buffer, which doesn't need reallocation of byte arrays, because it + * reuses a single byte array. This works particularly well, if reading + * from the buffer takes place at the same time than writing to. Such is the + * case, for example, when using the buffer within a filtering input stream, + * like the {@link CircularBufferInputStream}. + */ +public class CircularByteBuffer { + private final byte[] buffer; + private int startOffset, endOffset, currentNumberOfBytes; + + /** + * Creates a new instance with the given buffer size. + */ + public CircularByteBuffer(int pSize) { + buffer = new byte[pSize]; + startOffset = 0; + endOffset = 0; + currentNumberOfBytes = 0; + } + + /** + * Creates a new instance with a reasonable default buffer size (8192). + */ + public CircularByteBuffer() { + this(8192); + } + + /** + * Returns the next byte from the buffer, removing it at the same time, so + * that following invocations won't return it again. + * @return The byte, which is being returned. + * @throws IllegalStateException The buffer is empty. Use {@link #hasBytes()}, + * or {@link #getCurrentNumberOfBytes()}, to prevent this exception. + */ + public byte read() { + if (currentNumberOfBytes <= 0) { + throw new IllegalStateException("No bytes available."); + } + final byte b = buffer[startOffset]; + --currentNumberOfBytes; + if (++startOffset == buffer.length) { + startOffset = 0; + } + return b; + } + + /** + * Returns the given number of bytes from the buffer by storing them in + * the given byte array at the given offset. + * @param pBuffer The byte array, where to add bytes. + * @param pOffset The offset, where to store bytes in the byte array. + * @param pLength The number of bytes to return. + * @throws NullPointerException The byte array {@code pBuffer} is null. + * @throws IllegalArgumentException Either of {@code pOffset}, or {@code pLength} is negative, + * or the length of the byte array {@code pBuffer} is too small. + * @throws IllegalStateException The buffer doesn't hold the given number + * of bytes. Use {@link #getCurrentNumberOfBytes()} to prevent this + * exception. + */ + public void read(byte[] pBuffer, int pOffset, int pLength) { + Objects.requireNonNull(pBuffer); + if (pOffset < 0 || pOffset >= pBuffer.length) { + throw new IllegalArgumentException("Invalid offset: " + pOffset); + } + if (pLength < 0 || pLength > buffer.length) { + throw new IllegalArgumentException("Invalid length: " + pLength); + } + if (pOffset+pLength > pBuffer.length) { + throw new IllegalArgumentException("The supplied byte array contains only " + + pBuffer.length + " bytes, but offset, and length would require " + + (pOffset+pLength-1)); + } + if (currentNumberOfBytes < pLength) { + throw new IllegalStateException("Currently, there are only " + currentNumberOfBytes + + "in the buffer, not " + pLength); + } + int offset = pOffset; + for (int i = 0; i < pLength; i++) { + pBuffer[offset++] = buffer[startOffset]; + --currentNumberOfBytes; + if (++startOffset == buffer.length) { + startOffset = 0; + } + } + } + + /** + * Adds a new byte to the buffer, which will eventually be returned by following + * invocations of {@link #read()}. + * @param pByte The byte, which is being added to the buffer. + * @throws IllegalStateException The buffer is full. Use {@link #hasSpace()}, + * or {@link #getSpace()}, to prevent this exception. + */ + public void add(byte pByte) { + if (currentNumberOfBytes >= buffer.length) { + throw new IllegalStateException("No space available"); + } + buffer[endOffset] = pByte; + ++currentNumberOfBytes; + if (++endOffset == buffer.length) { + endOffset = 0; + } + } + + /** + * Returns, whether the next bytes in the buffer are exactly those, given by + * {@code pBuffer}, {@code pOffset}, and {@code pLength}. No bytes are being + * removed from the buffer. If the result is true, then the following invocations + * of {@link #read()} are guaranteed to return exactly those bytes. + * @return True, if the next invocations of {@link #read()} will return the + * bytes at offsets {@code pOffset}+0, {@code pOffset}+1, ..., + * @code{pOffset}+@code{pLength}-1 of byte array {@code pBuffer}. + * @throws IllegalArgumentException Either of {@code pOffset}, or {@code pLength} is negative. + * @throws NullPointerException The byte array {@code pBuffer} is null. + */ + public boolean peek(byte[] pBuffer, int pOffset, int pLength) { + Objects.requireNonNull(pBuffer, "Buffer"); + if (pOffset < 0 || pOffset >= pBuffer.length) { + throw new IllegalArgumentException("Invalid offset: " + pOffset); + } + if (pLength < 0 || pLength > buffer.length) { + throw new IllegalArgumentException("Invalid length: " + pLength); + } + if (pLength < currentNumberOfBytes) { + return false; + } + int offset = startOffset; + for (int i = 0; i < pLength; i++) { + if (buffer[offset] != pBuffer[i+pOffset]) { + return false; + } + if (++offset == buffer.length) { + offset = 0; + } + } + return true; + } + + /** + * Adds the given bytes to the buffer. This is the same as invoking {@link #add(byte)} + * for the bytes at offsets {@code pOffset}+0, {@code pOffset}+1, ..., + * @code{pOffset}+@code{pLength}-1 of byte array {@code pBuffer}. + * @throws IllegalStateException The buffer doesn't have sufficient space. Use + * {@link #getSpace()} to prevent this exception. + * @throws IllegalArgumentException Either of {@code pOffset}, or {@code pLength} is negative. + * @throws NullPointerException The byte array {@code pBuffer} is null. + */ + public void add(byte[] pBuffer, int pOffset, int pLength) { + Objects.requireNonNull(pBuffer, "Buffer"); + if (pOffset < 0 || pOffset >= pBuffer.length) { + throw new IllegalArgumentException("Invalid offset: " + pOffset); + } + if (pLength < 0) { + throw new IllegalArgumentException("Invalid length: " + pLength); + } + if (currentNumberOfBytes+pLength > buffer.length) { + throw new IllegalStateException("No space available"); + } + for (int i = 0; i < pLength; i++) { + buffer[endOffset] = pBuffer[pOffset+i]; + if (++endOffset == buffer.length) { + endOffset = 0; + } + } + currentNumberOfBytes += pLength; + } + + /** + * Returns, whether there is currently room for a single byte in the buffer. + * Same as {@link #hasSpace(int) hasSpace(1)}. + * @see #hasSpace(int) + * @see #getSpace() + */ + public boolean hasSpace() { + return currentNumberOfBytes < buffer.length; + } + + /** + * Returns, whether there is currently room for the given number of bytes in the buffer. + * @see #hasSpace() + * @see #getSpace() + */ + public boolean hasSpace(int pBytes) { + return currentNumberOfBytes+pBytes <= buffer.length; + } + + /** + * Returns, whether the buffer is currently holding, at least, a single byte. + */ + public boolean hasBytes() { + return currentNumberOfBytes > 0; + } + + /** + * Returns the number of bytes, that can currently be added to the buffer. + */ + public int getSpace() { + return buffer.length - currentNumberOfBytes; + } + + /** + * Returns the number of bytes, that are currently present in the buffer. + */ + public int getCurrentNumberOfBytes() { + return currentNumberOfBytes; + } + + /** + * Removes all bytes from the buffer. + */ + public void clear() { + startOffset = 0; + endOffset = 0; + currentNumberOfBytes = 0; + } +} http://git-wip-us.apache.org/repos/asf/commons-io/blob/559de2c4/src/main/java/org/apache/commons/io/input/buffer/PeekableInputStream.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/commons/io/input/buffer/PeekableInputStream.java b/src/main/java/org/apache/commons/io/input/buffer/PeekableInputStream.java new file mode 100644 index 0000000..e56c378 --- /dev/null +++ b/src/main/java/org/apache/commons/io/input/buffer/PeekableInputStream.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input.buffer; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Objects; + + +/** Implementation of a buffered input stream, which allows to peek into + * the buffers first bytes. This comes in handy when manually implementing + * scanners, lexers, parsers, or the like. + */ +public class PeekableInputStream extends CircularBufferInputStream { + /** Creates a new instance, which filters the given input stream, and + * uses the given buffer size. + * @param pIn The input stream, which is being buffered. + * @param pBufferSize The size of the {@link CircularByteBuffer}, which is + * used internally. + */ + public PeekableInputStream(InputStream pIn, int pBufferSize) { + super(pIn, pBufferSize); + } + + /** Creates a new instance, which filters the given input stream, and + * uses a reasonable default buffer size (8192). + * @param pIn The input stream, which is being buffered. + * @param pBufferSize The size of the {@link CircularByteBuffer}, which is + * used internally. + */ + public PeekableInputStream(InputStream pIn) { + super(pIn); + } + + /** + * Returns, whether the next bytes in the buffer are as given by + * {@code pBuffer}. This is equivalent to {@link #peek(byte[],int,int)} + * with {@code pOffset} == 0, and {@code pLength} == {@code pBuffer.length} + * @param pBuffer + * @return + * @throws IOException Refilling the buffer failed. + */ + public boolean peek(byte[] pBuffer) throws IOException { + Objects.requireNonNull(pBuffer, "Buffer"); + if (pBuffer.length > bufferSize) { + throw new IllegalArgumentException("Peek request size of " + pBuffer.length + + " bytes exceeds buffer size of " + bufferSize + " bytes"); + } + if (buffer.getCurrentNumberOfBytes() < pBuffer.length) { + fillBuffer(); + } + return buffer.peek(pBuffer, 0, pBuffer.length); + } + + /** + * Returns, whether the next bytes in the buffer are as given by + * {@code pBuffer}, {code pOffset}, and {@code pLength}. + * @param pBuffer + * @return + * @throws IOException + */ + public boolean peek(byte[] pBuffer, int pOffset, int pLength) throws IOException { + Objects.requireNonNull(pBuffer, "Buffer"); + if (pBuffer.length > bufferSize) { + throw new IllegalArgumentException("Peek request size of " + pBuffer.length + + " bytes exceeds buffer size of " + bufferSize + " bytes"); + } + if (buffer.getCurrentNumberOfBytes() < pBuffer.length) { + fillBuffer(); + } + return buffer.peek(pBuffer, pOffset, pLength); + } +} http://git-wip-us.apache.org/repos/asf/commons-io/blob/559de2c4/src/test/java/org/apache/commons/io/input/buffer/CircularBufferInputStreamTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/commons/io/input/buffer/CircularBufferInputStreamTest.java b/src/test/java/org/apache/commons/io/input/buffer/CircularBufferInputStreamTest.java new file mode 100644 index 0000000..311baa0 --- /dev/null +++ b/src/test/java/org/apache/commons/io/input/buffer/CircularBufferInputStreamTest.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.input.buffer; + +import java.io.ByteArrayInputStream; +import java.util.Random; + +import org.junit.Test; + + +public class CircularBufferInputStreamTest { + private final Random rnd = new Random(1530960934483l); // System.currentTimeMillis(), when this test was written. + // Always using the same seed should ensure a reproducable test. + + @Test + public void testRandomRead() throws Exception { + final byte[] inputBuffer = newInputBuffer(); + final byte[] bufferCopy = new byte[inputBuffer.length]; + final ByteArrayInputStream bais = new ByteArrayInputStream(inputBuffer); + @SuppressWarnings("resource") + final CircularBufferInputStream cbis = new CircularBufferInputStream(bais, 253); + int offset = 0; + final byte[] readBuffer = new byte[256]; + while (offset < bufferCopy.length) { + switch (rnd.nextInt(2)) { + case 0: + { + final int res = cbis.read(); + if (res == -1) { + throw new IllegalStateException("Unexpected EOF at offset " + offset); + } + if (inputBuffer[offset] != res) { + throw new IllegalStateException("Expected " + inputBuffer[offset] + " at offset " + offset + ", got " + res); + } + ++offset; + break; + } + case 1: + { + final int res = cbis.read(readBuffer, 0, rnd.nextInt(readBuffer.length+1)); + if (res == -1) { + throw new IllegalStateException("Unexpected EOF at offset " + offset); + } else if (res == 0) { + throw new IllegalStateException("Unexpected zero-byte-result at offset " + offset); + } else { + for (int i = 0; i < res; i++) { + if (inputBuffer[offset] != readBuffer[i]) { + throw new IllegalStateException("Expected " + inputBuffer[offset] + " at offset " + offset + ", got " + readBuffer[i]); + } + ++offset; + } + } + break; + } + default: + throw new IllegalStateException("Unexpected random choice value"); + } + } + } + + /** + * Create a large, but random input buffer. + */ + private byte[] newInputBuffer() { + final byte[] buffer = new byte[16*512+rnd.nextInt(512)]; + rnd.nextBytes(buffer); + return buffer; + } +}