Skip to content

Commit

Permalink
Add org.apache.commons.io.input.CircularInputStream.
Browse files Browse the repository at this point in the history
[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.
  • Loading branch information
garydgregory committed Jul 1, 2020
1 parent 7371285 commit 97ae01c
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 38 deletions.
7 changes: 3 additions & 4 deletions pom.xml
Expand Up @@ -24,7 +24,7 @@
<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>
Expand Down Expand Up @@ -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>
Expand All @@ -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>
Expand Down
11 changes: 10 additions & 1 deletion src/changes/changes.xml
Expand Up @@ -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>
Expand All @@ -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.">
Expand Down
87 changes: 87 additions & 0 deletions src/main/java/org/apache/commons/io/input/CircularInputStream.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;

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;
}

}
Expand Up @@ -16,37 +16,29 @@
*/
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);
}

}
110 changes: 110 additions & 0 deletions src/test/java/org/apache/commons/io/input/CircularInputStreamTest.java
@@ -0,0 +1,110 @@
/*
* 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);
}

}

0 comments on commit 97ae01c

Please sign in to comment.