Author: kkolinko Date: Fri Nov 4 14:06:24 2011 New Revision: 1197578 URL: http://svn.apache.org/viewvc?rev=1197578&view=rev Log: FlushableGZIPOutputStream: Reserve one byte of real data and perform standard write() call. This uses more standard code: deflate() is never called directly and empty byte array trick is not used.
TestFlushableGZIPOutputStream: Add a check that the decompressed stream equals to the original one. Modified: tomcat/trunk/java/org/apache/coyote/http11/filters/FlushableGZIPOutputStream.java tomcat/trunk/test/org/apache/coyote/http11/filters/TestFlushableGZIPOutputStream.java Modified: tomcat/trunk/java/org/apache/coyote/http11/filters/FlushableGZIPOutputStream.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/filters/FlushableGZIPOutputStream.java?rev=1197578&r1=1197577&r2=1197578&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/filters/FlushableGZIPOutputStream.java (original) +++ tomcat/trunk/java/org/apache/coyote/http11/filters/FlushableGZIPOutputStream.java Fri Nov 4 14:06:24 2011 @@ -35,69 +35,74 @@ public class FlushableGZIPOutputStream e super(os); } - private static final byte[] EMPTYBYTEARRAY = new byte[0]; - private boolean hasData = false; - private boolean needReenableCompression = false; - /** - * Here we make sure we have received data, so that the header has been for - * sure written to the output stream already. + * It is used to reserve one byte of real data so that it can be used when + * flushing the stream. */ + private byte[] lastByte = new byte[1]; + private boolean hasLastByte = false; + + @Override + public synchronized void write(byte[] bytes) throws IOException { + write(bytes, 0, bytes.length); + } + @Override public synchronized void write(byte[] bytes, int offset, int length) throws IOException { if (length > 0) { - prepareForOutput(); - super.write(bytes, offset, length); + flushLastByte(); + if (length > 1) { + super.write(bytes, offset, length - 1); + } + rememberLastByte(bytes[offset + length - 1]); } } @Override public synchronized void write(int i) throws IOException { - prepareForOutput(); - super.write(i); + flushLastByte(); + rememberLastByte((byte) (i & 0xFF)); } @Override - public synchronized void write(byte[] bytes) throws IOException { - write(bytes, 0, bytes.length); + public void close() throws IOException { + flushLastByte(); + super.close(); } - private void prepareForOutput() throws IOException { - hasData = true; - if (needReenableCompression) { - def.setLevel(Deflater.DEFAULT_COMPRESSION); - deflate(); - needReenableCompression = false; + private void rememberLastByte(byte b) { + lastByte[0] = b; + hasLastByte = true; + } + + private void flushLastByte() throws IOException { + if (hasLastByte) { + super.write(lastByte, 0, 1); + hasLastByte = false; } } @Override public synchronized void flush() throws IOException { - if (!hasData) { - return; // do not allow the gzip header to be flushed on its own - } - - // trick the deflater to flush - /** - * Now this is tricky: We force the Deflater to flush its data by - * switching compression level. As yet, a perplexingly simple workaround - * for - * http://developer.java.sun.com/developer/bugParade/bugs/4255743.html - */ - if (!def.finished()) { - def.setInput(EMPTYBYTEARRAY, 0, 0); - - def.setLevel(Deflater.NO_COMPRESSION); - deflate(); - - // Cannot reenable compression now. Must wait for data. - needReenableCompression = true; - - out.flush(); + if (hasLastByte) { + // - do not allow the gzip header to be flushed on its own + // - do not do anything if there is no data to send + + // trick the deflater to flush + /** + * Now this is tricky: We force the Deflater to flush its data by + * switching compression level. As yet, a perplexingly simple workaround + * for + * http://developer.java.sun.com/developer/bugParade/bugs/4255743.html + */ + if (!def.finished()) { + def.setLevel(Deflater.NO_COMPRESSION); + flushLastByte(); + def.setLevel(Deflater.DEFAULT_COMPRESSION); + } } - - hasData = false; // no more data to flush + out.flush(); } /* Modified: tomcat/trunk/test/org/apache/coyote/http11/filters/TestFlushableGZIPOutputStream.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/coyote/http11/filters/TestFlushableGZIPOutputStream.java?rev=1197578&r1=1197577&r2=1197578&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/coyote/http11/filters/TestFlushableGZIPOutputStream.java (original) +++ tomcat/trunk/test/org/apache/coyote/http11/filters/TestFlushableGZIPOutputStream.java Fri Nov 4 14:06:24 2011 @@ -20,10 +20,16 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.InputStream; +import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; import java.util.zip.GZIPInputStream; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + import org.junit.Test; import org.apache.catalina.util.IOTools; @@ -38,32 +44,25 @@ public class TestFlushableGZIPOutputStre public void testBug52121() throws Exception { ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(); - ByteArrayOutputStream sink = new ByteArrayOutputStream(); - OutputStream output = new FlushableGZIPOutputStream(byteOutStream); File sourcesDir = new File("test/org/apache/coyote/http11/filters/"); - InputStream input; + List<byte[]> parts = new ArrayList<byte[]>(); + byte[] part; - input = new FileInputStream(new File(sourcesDir, "bug52121-part1")); - try { - IOTools.flow(input, output); - } finally { - input.close(); - } + part = loadFile(new File(sourcesDir, "bug52121-part1")); + parts.add(part); + flowBytes(part, output); output.flush(); - input = new FileInputStream(new File(sourcesDir, "bug52121-part2")); - try { - IOTools.flow(input, output); - } finally { - input.close(); - } + part = loadFile(new File(sourcesDir, "bug52121-part2")); + parts.add(part); + flowBytes(part, output); output.flush(); - // Using "data" may trigger a JVM crash - // output.write("data".getBytes()); - output.write("data2".getBytes()); + part = "data2".getBytes("ASCII"); + parts.add(part); + output.write(part); output.flush(); output.close(); @@ -72,6 +71,49 @@ public class TestFlushableGZIPOutputStre new ByteArrayInputStream(byteOutStream.toByteArray()); GZIPInputStream inflaterStream = new GZIPInputStream(byteInStream); - IOTools.flow(inflaterStream, sink); + ByteArrayOutputStream sink = new ByteArrayOutputStream(); + try { + IOTools.flow(inflaterStream, sink); + } finally { + sink.close(); + } + + byte[] decompressedBytes = sink.toByteArray(); + int originalLength = 0; + for (byte[] bytes : parts) { + assertArrayEquals(bytes, Arrays.copyOfRange(decompressedBytes, + originalLength, originalLength + bytes.length)); + originalLength += bytes.length; + } + assertEquals(originalLength, decompressedBytes.length); + } + + /** + * Loads file into memory. + */ + private byte[] loadFile(File file) throws IOException { + ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(); + FileInputStream input = new FileInputStream(file); + try { + IOTools.flow(input, byteOutStream); + } finally { + input.close(); + } + return byteOutStream.toByteArray(); + } + + /** + * Writes data to the stream and returns the size of the file. + */ + private void flowBytes(byte[] bytes, OutputStream output) + throws IOException { + // Could use output.write(), but IOTools writes in small portions, and + // that is more natural + ByteArrayInputStream byteInStream = new ByteArrayInputStream(bytes); + try { + IOTools.flow(byteInStream, output); + } finally { + byteInStream.close(); + } } } --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org