--- java/org/apache/coyote/http11/InternalAprOutputBuffer.java (revision 988002) +++ java/org/apache/coyote/http11/InternalAprOutputBuffer.java (working copy) @@ -31,6 +31,7 @@ import org.apache.coyote.ActionCode; import org.apache.coyote.OutputBuffer; import org.apache.coyote.Response; +import org.apache.coyote.http11.filters.GzipOutputFilter; /** * Output buffer. @@ -94,6 +95,12 @@ protected static StringManager sm = StringManager.getManager(Constants.Package); + /** + * Logger. + */ + private static final org.apache.juli.logging.Log log + = org.apache.juli.logging.LogFactory.getLog( + InternalAprOutputBuffer.class); // ----------------------------------------------------- Instance Variables @@ -280,6 +287,19 @@ } + // go through the filters and if there is gzip filter + // invoke it to flush + for (int i = 0; i <= lastActiveFilter; i++) { + if (activeFilters[i] instanceof GzipOutputFilter) { + if (log.isDebugEnabled()) { + log.debug("Flushing the gzip filter at position " + i + + " of the filter chain..."); + } + ((GzipOutputFilter) activeFilters[i]).flush(); + break; + } + } + // Flush the current buffer flushBuffer(); --- java/org/apache/coyote/http11/InternalNioOutputBuffer.java (revision 988002) +++ java/org/apache/coyote/http11/InternalNioOutputBuffer.java (working copy) @@ -25,6 +25,7 @@ import org.apache.coyote.ActionCode; import org.apache.coyote.OutputBuffer; import org.apache.coyote.Response; +import org.apache.coyote.http11.filters.GzipOutputFilter; import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.util.buf.CharChunk; import org.apache.tomcat.util.buf.MessageBytes; @@ -34,7 +35,6 @@ import org.apache.tomcat.util.net.NioEndpoint; import org.apache.tomcat.util.net.NioSelectorPool; import org.apache.tomcat.util.res.StringManager; -import java.io.EOFException; import org.apache.tomcat.util.MutableInteger; /** @@ -103,6 +103,12 @@ protected static StringManager sm = StringManager.getManager(Constants.Package); + /** + * Logger. + */ + private static final org.apache.juli.logging.Log log + = org.apache.juli.logging.LogFactory.getLog( + InternalNioOutputBuffer.class); // ----------------------------------------------------- Instance Variables @@ -296,6 +302,19 @@ } + // go through the filters and if there is gzip filter + // invoke it to flush + for (int i = 0; i <= lastActiveFilter; i++) { + if (activeFilters[i] instanceof GzipOutputFilter) { + if (log.isDebugEnabled()) { + log.debug("Flushing the gzip filter at position " + i + + " of the filter chain..."); + } + ((GzipOutputFilter) activeFilters[i]).flush(); + break; + } + } + // Flush the current buffer flushBuffer(); --- java/org/apache/coyote/http11/InternalOutputBuffer.java (revision 988002) +++ java/org/apache/coyote/http11/InternalOutputBuffer.java (working copy) @@ -32,6 +32,7 @@ import org.apache.coyote.ActionCode; import org.apache.coyote.OutputBuffer; import org.apache.coyote.Response; +import org.apache.coyote.http11.filters.GzipOutputFilter; /** * Output buffer. @@ -90,6 +91,11 @@ protected static StringManager sm = StringManager.getManager(Constants.Package); + /** + * Logger. + */ + private static final org.apache.juli.logging.Log log + = org.apache.juli.logging.LogFactory.getLog(InternalOutputBuffer.class); // ----------------------------------------------------- Instance Variables @@ -294,6 +300,19 @@ } + // go through the filters and if there is gzip filter + // invoke it to flush + for (int i = 0; i <= lastActiveFilter; i++) { + if (activeFilters[i] instanceof GzipOutputFilter) { + if (log.isDebugEnabled()) { + log.debug("Flushing the gzip filter at position " + i + + " of the filter chain..."); + } + ((GzipOutputFilter) activeFilters[i]).flush(); + break; + } + } + // Flush the current buffer if (useSocketBuffer) { socketBuffer.flushBuffer(); --- java/org/apache/coyote/http11/filters/FlushableGZIPOutputStream.java (revision 0) +++ java/org/apache/coyote/http11/filters/FlushableGZIPOutputStream.java (revision 0) @@ -0,0 +1,108 @@ +/* + * 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.coyote.http11.filters; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.Deflater; +import java.util.zip.GZIPOutputStream; + +/** + * Extension of {@link GZIPOutputStream} to workaround for a couple of long + * standing JDK bugs + * (Bug + * 4255743 and + * Bug + * 4813885) so the GZIP'd output can be flushed. + */ +public class FlushableGZIPOutputStream extends GZIPOutputStream { + public FlushableGZIPOutputStream(OutputStream os) throws IOException { + super(os); + } + + private static final byte[] EMPTYBYTEARRAY = new byte[0]; + private boolean hasData = false; + + /** + * Here we make sure we have received data, so that the header has been for + * sure written to the output stream already. + */ + @Override + public synchronized void write(byte[] bytes, int i, int i1) + throws IOException { + super.write(bytes, i, i1); + hasData = true; + } + + @Override + public synchronized void write(int i) throws IOException { + super.write(i); + hasData = true; + } + + @Override + public synchronized void write(byte[] bytes) throws IOException { + super.write(bytes); + hasData = true; + } + + @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(); + + def.setLevel(Deflater.DEFAULT_COMPRESSION); + deflate(); + + out.flush(); + } + + hasData = false; // no more data to flush + } + + /* + * Keep on calling deflate until it runs dry. The default implementation + * only does it once and can therefore hold onto data when they need to be + * flushed out. + */ + @Override + protected void deflate() throws IOException { + int len; + do { + len = def.deflate(buf, 0, buf.length); + if (len > 0) { + out.write(buf, 0, len); + } + } while (len != 0); + } + +} + native --- java/org/apache/coyote/http11/filters/GzipOutputFilter.java (revision 988002) +++ java/org/apache/coyote/http11/filters/GzipOutputFilter.java (working copy) @@ -42,6 +42,13 @@ protected static final ByteChunk ENCODING = new ByteChunk(); + /** + * Logger. + */ + protected static org.apache.juli.logging.Log log = + org.apache.juli.logging.LogFactory.getLog(GzipOutputFilter.class); + + // ----------------------------------------------------- Static Initializer @@ -82,7 +89,7 @@ public int doWrite(ByteChunk chunk, Response res) throws IOException { if (compressionStream == null) { - compressionStream = new GZIPOutputStream(fakeOutputStream); + compressionStream = new FlushableGZIPOutputStream(fakeOutputStream); } compressionStream.write(chunk.getBytes(), chunk.getStart(), chunk.getLength()); @@ -92,6 +99,23 @@ // --------------------------------------------------- OutputFilter Methods + /** + * Added to allow flushing to happen for the gzip'ed outputstream + */ + public void flush() { + if (compressionStream != null) { + try { + if (log.isDebugEnabled()) { + log.debug("Flushing the compression stream!"); + } + compressionStream.flush(); + } catch (IOException e) { + if (log.isDebugEnabled()) { + log.debug("Ignored exception while flushing gzip filter", e); + } + } + } + } /** * Some filters need additional parameters from the response. All the @@ -117,7 +141,7 @@ public long end() throws IOException { if (compressionStream == null) { - compressionStream = new GZIPOutputStream(fakeOutputStream); + compressionStream = new FlushableGZIPOutputStream(fakeOutputStream); } compressionStream.finish(); compressionStream.close();