From 96ed96ef9308f091906e905c1ecc7d47476d385e Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Sun, 26 May 2013 18:00:37 +0200 Subject: [PATCH] Add output stream wrapper that always writes a multiple of a fixed number of bytes. --- pom.xml | 7 ++ .../sonitus/io/IntegralWriteOutputStream.java | 98 +++++++++++++++ .../sonitus/io/IntegralWriteOutputStreamTest.java | 134 +++++++++++++++++++++ 3 files changed, 239 insertions(+) create mode 100644 src/main/java/net/pterodactylus/sonitus/io/IntegralWriteOutputStream.java create mode 100644 src/test/java/net/pterodactylus/sonitus/io/IntegralWriteOutputStreamTest.java diff --git a/pom.xml b/pom.xml index d4d3e25..b99b01c 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,13 @@ 6.8 test + + org.mockito + mockito-core + 1.9.5 + test + + diff --git a/src/main/java/net/pterodactylus/sonitus/io/IntegralWriteOutputStream.java b/src/main/java/net/pterodactylus/sonitus/io/IntegralWriteOutputStream.java new file mode 100644 index 0000000..7804c0b --- /dev/null +++ b/src/main/java/net/pterodactylus/sonitus/io/IntegralWriteOutputStream.java @@ -0,0 +1,98 @@ +/* + * Sonitus - IntegralWriteOutputStream.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sonitus.io; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * {@link OutputStream} wrapper that always writes a multiple of a fixed amount + * of bytes at once. + * + * @author David ‘Bombe’ Roden + */ +public class IntegralWriteOutputStream extends FilterOutputStream { + + /** The current in-process values. */ + private final byte[] buffer; + + /** The next position of the buffer that will be written to. */ + private int bufferPosition; + + /** + * Creates a new integral write output stream. + * + * @param outputStream + * The output stream to wrap + * @param integralSize + * The number of bytes to write at once + */ + public IntegralWriteOutputStream(OutputStream outputStream, int integralSize) { + super(outputStream); + buffer = new byte[integralSize]; + } + + // + // OUTPUTSTREAM METHODS + // + + @Override + public void write(int data) throws IOException { + buffer[bufferPosition++] = (byte) data; + if (bufferPosition == buffer.length) { + bufferPosition = 0; + out.write(buffer); + } + } + + @Override + public void write(byte[] buffer) throws IOException { + write(buffer, 0, buffer.length); + } + + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + /* are the some bytes in the current buffer? */ + int sourceOffset = 0; + if (bufferPosition != 0) { + int bytesToCopy = Math.min(length, this.buffer.length - bufferPosition); + System.arraycopy(buffer, offset, this.buffer, bufferPosition, bytesToCopy); + sourceOffset += bytesToCopy; + bufferPosition += bytesToCopy; + if (bufferPosition == this.buffer.length) { + bufferPosition = 0; + out.write(this.buffer); + } + } + + /* write the largest possible chunk at once. */ + int integralBytesLeft = (int) ((length - sourceOffset) / this.buffer.length) * this.buffer.length; + if (integralBytesLeft != 0) { + out.write(buffer, offset + sourceOffset, integralBytesLeft); + } + + /* are there some bytes left? */ + sourceOffset += integralBytesLeft; + if (sourceOffset < length) { + System.arraycopy(buffer, offset + sourceOffset, this.buffer, 0, length - sourceOffset); + bufferPosition = length - sourceOffset; + } + } + +} diff --git a/src/test/java/net/pterodactylus/sonitus/io/IntegralWriteOutputStreamTest.java b/src/test/java/net/pterodactylus/sonitus/io/IntegralWriteOutputStreamTest.java new file mode 100644 index 0000000..41ffd74 --- /dev/null +++ b/src/test/java/net/pterodactylus/sonitus/io/IntegralWriteOutputStreamTest.java @@ -0,0 +1,134 @@ +/* + * Sonitus - IntegralWriteOutputStreamTest.java - Copyright © 2013 David Roden + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package net.pterodactylus.sonitus.io; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Random; + +import org.testng.annotations.Test; + +/** + * Unit tests for {@link IntegralWriteOutputStream}. + * + * @author David ‘Bombe’ Roden + */ +public class IntegralWriteOutputStreamTest { + + @Test + public void testSingleWritesWithSize2() throws IOException { + OutputStream outputStream = mock(OutputStream.class); + + IntegralWriteOutputStream integralWriteOutputStream = new IntegralWriteOutputStream(outputStream, 2); + integralWriteOutputStream.write(1); + integralWriteOutputStream.write(2); + integralWriteOutputStream.write(3); + integralWriteOutputStream.write(4); + integralWriteOutputStream.write(5); + + verify(outputStream, never()).write(anyInt()); + verify(outputStream, times(2)).write((byte[]) any()); + } + + @Test + public void testLargeWritesWithSize3() throws IOException { + ByteArrayOutputStream byteArrayOutputStream = getOutputStream(3); + IntegralWriteOutputStream integralWriteOutputStream = new IntegralWriteOutputStream(byteArrayOutputStream, 3); + + byte[] buffer = generateData(18); + integralWriteOutputStream.write(buffer, 0, 7); + integralWriteOutputStream.write(buffer, 7, 7); + integralWriteOutputStream.write(buffer, 14, 4); + + assertThat(byteArrayOutputStream.toByteArray(), is(buffer)); + } + + @Test + public void testLargeWritesWithSize4() throws IOException { + ByteArrayOutputStream byteArrayOutputStream = getOutputStream(1024); + IntegralWriteOutputStream integralWriteOutputStream = new IntegralWriteOutputStream(byteArrayOutputStream, 1024); + + byte[] buffer = generateData(32768); + integralWriteOutputStream.write(buffer, 0, 123); + integralWriteOutputStream.write(buffer, 123, 768); + integralWriteOutputStream.write(buffer, 123 + 768, 6285); + integralWriteOutputStream.write(buffer, 123 + 768 + 6285, 1234); + integralWriteOutputStream.write(buffer, 123 + 768 + 6285 + 1234, 21111); + integralWriteOutputStream.write(buffer, 123 + 768 + 6285 + 1234 + 21111, 32768 - (123 + 768 + 6285 + 1234 + 21111)); + + assertThat(byteArrayOutputStream.toByteArray(), is(buffer)); + } + + // + // PRIVATE METHODS + // + + /** + * Creates a {@link ByteArrayOutputStream} that will throw an {@link + * IllegalArgumentException} if its {@link OutputStream#write(byte[], int, + * int)} method is called with {@code len} not being a multiple of {@code + * integralSize}, or if {@link OutputStream#write(int)} is called. + * + * @param integralSize + * The number of bytes to write multiples of + * @return The created output stream + */ + private static ByteArrayOutputStream getOutputStream(final int integralSize) { + return new ByteArrayOutputStream() { + + @Override + public synchronized void write(byte[] b, int off, int len) { + if ((len % integralSize) != 0) { + throw new IllegalArgumentException(String.format("%d is not a multiple of %d.", len, integralSize)); + } + super.write(b, off, len); + } + + @Override + public synchronized void write(int b) { + throw new IllegalArgumentException("write(int) called."); + } + + }; + } + + /** + * Generates a random amount of data. + * + * @param length + * The length of the data + * @return The generated random data + */ + private static byte[] generateData(int length) { + Random random = new Random(); + byte[] buffer = new byte[length]; + random.nextBytes(buffer); + return buffer; + } + +} -- 2.7.4