Add org.apache.commons.io.input.CircularInputStream.
InfiniteCircularInputStream is not infinite if its input buffer
contains -1.
InfiniteCircularInputStream throws a divide-by-zero exception
when reading if its input buffer is size 0.
Update version from 2.7.1-SNAPSHOT to 2.8-SNAPSHOT since we are adding a
new public class.
MODIFY
pom.xml
pom.xml
<modelVersion>4.0.0</modelVersion>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
- <version>2.7.1-SNAPSHOT</version>
+ <version>2.8-SNAPSHOT</version>
<name>Apache Commons IO</name>
<inceptionYear>2002</inceptionYear>
@@ -268,8 +268,8 @@ file comparators, endian transformation classes, and much more.
<commons.componentid>io</commons.componentid>
<commons.module.name>org.apache.commons.io</commons.module.name>
<commons.rc.version>RC1</commons.rc.version>
- <commons.bc.version>2.6</commons.bc.version>
- <commons.release.version>2.7</commons.release.version>
+ <commons.bc.version>2.7</commons.bc.version>
+ <commons.release.version>2.8</commons.release.version>
<commons.release.desc>(requires Java 8)</commons.release.desc>
<commons.jira.id>IO</commons.jira.id>
<commons.jira.pid>12310477</commons.jira.pid>
@@ -296,7 +296,6 @@ file comparators, endian transformation classes, and much more.
<commons.japicmp.version>0.14.3</commons.japicmp.version>
<japicmp.skip>false</japicmp.skip>
<jacoco.skip>${env.JACOCO_SKIP}</jacoco.skip>
- <commons.bc.version>2.6</commons.bc.version>
<commons.release.isDistModule>true</commons.release.isDistModule>
<commons.releaseManagerName>Gary Gregory</commons.releaseManagerName>
<commons.releaseManagerKey>86fdc7e2a11262cb</commons.releaseManagerKey>

MODIFY
src/changes/changes.xml
src/changes/changes.xml
@@ -46,7 +46,7 @@ The <action> type attribute can be add,update,fix,remove.
<body>
<!-- The release date is the date RC is cut -->
- <release version="2.7.1" date="2020-MM-DD" description="Java 8 required.">
+ <release version="2.8" date="2020-MM-DD" description="Java 8 required.">
<action issue="IO-669" dev="ggregory" type="fix" due-to="XenoAmess, Gary Gregory">
Fix code smells; fix typos #115.
</action>
@@ -56,6 +56,15 @@ The <action> type attribute can be add,update,fix,remove.
<action issue="IO-673" dev="ggregory" type="fix" due-to="Jerome Wolff">
Make some simplifications #121.
</action>
+ <action dev="ggregory" type="fix" due-to="Gary Gregory">
+ Add org.apache.commons.io.input.CircularInputStream.
+ </action>
+ <action issue="IO-674" dev="ggregory" type="fix" due-to="Gary Gregory">
+ InfiniteCircularInputStream is not infinite if its input buffer contains -1.
+ </action>
+ <action issue="IO-675" dev="ggregory" type="fix" due-to="Gary Gregory">
+ InfiniteCircularInputStream throws a divide-by-zero exception when reading if its input buffer is size 0.
+ </action>
</release>
<!-- The release date is the date RC is cut -->
<release version="2.7" date="2020-05-24" description="Java 8 required.">

ADD
src/main/java/org/apache/commons/io/input/CircularInputStream.java
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+
+import org.apache.commons.io.IOUtils;
+
+/**
+ *
+ * An {@link InputStream} that repeats provided bytes for given target byte count.
+ * <p>
+ * Closing this input stream has no effect. The methods in this class can be called after the stream has been closed
+ * without generating an {@link IOException}.
+ * </p>
+ *
+ * @see InfiniteCircularInputStream
+ * @since 2.8
+ */
+public class CircularInputStream extends InputStream {
+
+ /**
+ * Throws an {@link IllegalArgumentException} if the input contains -1.
+ *
+ * @param repeatContent input to validate.
+ * @return the input.
+ */
+ private static byte[] validate(final byte[] repeatContent) {
+ Objects.requireNonNull(repeatContent, "repeatContent");
+ for (final byte b : repeatContent) {
+ if (b == IOUtils.EOF) {
+ throw new IllegalArgumentException("repeatContent contains the end-of-stream marker " + IOUtils.EOF);
+ }
+ }
+ return repeatContent;
+ }
+ private long byteCount;
+ private int position = -1;
+ private final byte[] repeatedContent;
+ private final long targetByteCount;
+
+ /**
+ * Creates an instance from the specified array of bytes.
+ *
+ * @param repeatContent Input buffer to be repeated this buffer is not copied.
+ * @param targetByteCount How many bytes the read. A negative number means an infinite target count.
+ */
+ public CircularInputStream(final byte[] repeatContent, final long targetByteCount) {
+ this.repeatedContent = validate(repeatContent);
+ if (repeatContent.length == 0) {
+ throw new IllegalArgumentException("repeatContent is empty.");
+ }
+ this.targetByteCount = targetByteCount;
+ }
+
+ @Override
+ public int read() {
+ if (repeatedContent.length == 0) {
+ return IOUtils.EOF;
+ }
+ if (targetByteCount >= 0) {
+ if (byteCount == targetByteCount) {
+ return IOUtils.EOF;
+ }
+ byteCount++;
+ }
+ position = (position + 1) % repeatedContent.length;
+ return repeatedContent[position] & 0xff;
+ }
+
+}

MODIFY
src/main/java/org/apache/commons/io/input/InfiniteCircularInputStream.java
src/main/java/org/apache/commons/io/input/InfiniteCircularInputStream.java
*/
package org.apache.commons.io.input;
+import java.io.IOException;
import java.io.InputStream;
/**
*
- * An {@link InputStream} that infinitely repeats provided bytes.
+ * An {@link InputStream} that infinitely repeats the provided bytes.
* <p>
- * Closing a <code>InfiniteCircularInputStream</code> has no effect. The methods in
- * this class can be called after the stream has been closed without generating
- * an <code>IOException</code>.
+ * Closing this input stream has no effect. The methods in this class can be called after the stream has been closed
+ * without generating an {@link IOException}.
* </p>
+ *
* @since 2.6
*/
-public class InfiniteCircularInputStream extends InputStream {
-
- final private byte[] repeatedContent;
- private int position = -1;
+public class InfiniteCircularInputStream extends CircularInputStream {
/**
- * Creates a InfiniteCircularStream from the specified array of chars.
+ * Creates an instance from the specified array of bytes.
*
- * @param repeatedContent
- * Input buffer to be repeated (not copied)
+ * @param repeatContent Input buffer to be repeated this buffer is not copied.
*/
- public InfiniteCircularInputStream(final byte[] repeatedContent) {
- this.repeatedContent = repeatedContent;
- }
-
- @Override
- public int read() {
- position = (position + 1) % repeatedContent.length;
- return repeatedContent[position] & 0xff;
+ public InfiniteCircularInputStream(final byte[] repeatContent) {
+ // A negative number means an infinite target count.
+ super(repeatContent, -1);
}
}

ADD
src/test/java/org/apache/commons/io/input/CircularInputStreamTest.java
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link CircularInputStream}.
+ */
+public class CircularInputStreamTest {
+
+ private void assertStreamOutput(final byte[] toCycle, final byte[] expected) throws IOException {
+ final byte[] actual = new byte[expected.length];
+
+ try (InputStream infStream = createInputStream(toCycle, -1)) {
+ final int actualReadBytes = infStream.read(actual);
+
+ assertArrayEquals(expected, actual);
+ assertEquals(expected.length, actualReadBytes);
+ }
+ }
+
+ private InputStream createInputStream(final byte[] repeatContent, final long targetByteCount) {
+ return new CircularInputStream(repeatContent, targetByteCount);
+ }
+
+ @Test
+ public void testContainsEofInputSize0() {
+ assertThrows(IllegalArgumentException.class, () -> createInputStream(new byte[] { -1 }, 0));
+ }
+
+ @Test
+ public void testCount0() throws IOException {
+ try (InputStream in = createInputStream(new byte[] { 1, 2 }, 0)) {
+ assertEquals(IOUtils.EOF, in.read());
+ }
+ }
+
+ @Test
+ public void testCount0InputSize0() {
+ assertThrows(IllegalArgumentException.class, () -> createInputStream(new byte[] {}, 0));
+ }
+
+ @Test
+ public void testCount0InputSize1() throws IOException {
+ try (InputStream in = createInputStream(new byte[] { 1 }, 0)) {
+ assertEquals(IOUtils.EOF, in.read());
+ }
+ }
+
+ @Test
+ public void testCount1InputSize1() throws IOException {
+ try (InputStream in = createInputStream(new byte[] { 1 }, 1)) {
+ assertEquals(1, in.read());
+ assertEquals(IOUtils.EOF, in.read());
+ }
+ }
+
+ @Test
+ public void testCycleBytes() throws IOException {
+ final byte[] input = new byte[] { 1, 2 };
+ final byte[] expected = new byte[] { 1, 2, 1, 2, 1 };
+
+ assertStreamOutput(input, expected);
+ }
+
+ @Test
+ public void testNullInputSize0() {
+ assertThrows(NullPointerException.class, () -> createInputStream(null, 0));
+ }
+
+ @Test
+ public void testWholeRangeOfBytes() throws IOException {
+ final int size = Byte.MAX_VALUE - Byte.MIN_VALUE + 1;
+ final byte[] contentToCycle = new byte[size];
+ byte value = Byte.MIN_VALUE;
+ for (int i = 0; i < contentToCycle.length; i++) {
+ contentToCycle[i] = value == IOUtils.EOF ? 0 : value;
+ value++;
+ }
+
+ final byte[] expectedOutput = Arrays.copyOf(contentToCycle, size);
+
+ assertStreamOutput(contentToCycle, expectedOutput);
+ }
+
+}

MODIFY
src/test/java/org/apache/commons/io/input/InfiniteCircularInputStreamTest.java
src/test/java/org/apache/commons/io/input/InfiniteCircularInputStreamTest.java
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
+import org.apache.commons.io.IOUtils;
import org.junit.jupiter.api.Test;
+/**
+ * Tests {@link InfiniteCircularInputStream}.
+ */
public class InfiniteCircularInputStreamTest {
+ private void assertStreamOutput(final byte[] toCycle, final byte[] expected) throws IOException {
+ final byte[] actual = new byte[expected.length];
+
+ try (InputStream infStream = new InfiniteCircularInputStream(toCycle)) {
+ final int actualReadBytes = infStream.read(actual);
+
+ assertArrayEquals(expected, actual);
+ assertEquals(expected.length, actualReadBytes);
+ }
+ }
+
+ private InputStream createInputStream(final byte[] repeatContent) {
+ return new InfiniteCircularInputStream(repeatContent);
+ }
+
+ @Test
+ public void testContainsEofInputSize0() {
+ assertThrows(IllegalArgumentException.class, () -> createInputStream(new byte[] { -1 }));
+ }
+
+ @Test
+ public void testCount0InputSize0() {
+ assertThrows(IllegalArgumentException.class, () -> createInputStream(new byte[] {}));
+ }
+
+ @Test
+ public void testCount0InputSize1() throws IOException {
+ try (InputStream in = createInputStream(new byte[] { 1 })) {
+ // empty
+ }
+ }
+
@Test
- public void shouldCycleBytes() throws IOException {
+ public void testCount1InputSize1() throws IOException {
+ try (InputStream in = createInputStream(new byte[] { 1 })) {
+ assertEquals(1, in.read());
+ assertEquals(1, in.read());
+ }
+ }
+
+ @Test
+ public void testCycleBytes() throws IOException {
final byte[] input = new byte[] { 1, 2 };
final byte[] expected = new byte[] { 1, 2, 1, 2, 1 };
@@ -36,12 +81,18 @@ public void shouldCycleBytes() throws IOException {
}
@Test
- public void shouldHandleWholeRangeOfBytes() throws IOException {
+ public void testNullInputSize0() {
+ assertThrows(NullPointerException.class, () -> createInputStream(null));
+ }
+
+ @Test
+ public void testWholeRangeOfBytes() throws IOException {
final int size = Byte.MAX_VALUE - Byte.MIN_VALUE + 1;
final byte[] contentToCycle = new byte[size];
byte value = Byte.MIN_VALUE;
for (int i = 0; i < contentToCycle.length; i++) {
- contentToCycle[i] = value++;
+ contentToCycle[i] = value == IOUtils.EOF ? 0 : value;
+ value++;
}
final byte[] expectedOutput = Arrays.copyOf(contentToCycle, size);
@@ -49,15 +100,4 @@ public void shouldHandleWholeRangeOfBytes() throws IOException {
assertStreamOutput(contentToCycle, expectedOutput);
}
- private void assertStreamOutput(final byte[] toCycle, final byte[] expected) throws IOException {
- final byte[] actual = new byte[expected.length];
-
- try (InputStream infStream = new InfiniteCircularInputStream(toCycle)) {
- final int actualReadBytes = infStream.read(actual);
-
- assertArrayEquals(expected, actual);
- assertEquals(expected.length, actualReadBytes);
- }
- }
-
}

Add org.apache.commons.io.input.CircularInputStream.

[IO-674] InfiniteCircularInputStream is not infinite if its input buffer
contains -1.

[IO-675] InfiniteCircularInputStream throws a divide-by-zero exception
when reading if its input buffer is size 0.

Update version from 2.7.1-SNAPSHOT to 2.8-SNAPSHOT since we are adding a
new public class.