diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentCompressor.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentCompressor.java new file mode 100644 index 0000000000..bad37da520 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentCompressor.java @@ -0,0 +1,98 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat 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.jboss.netty.handler.codec.http; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.channel.ChannelPipeline; +import org.jboss.netty.channel.ChannelPipelineCoverage; +import org.jboss.netty.handler.codec.compression.ZlibEncoder; +import org.jboss.netty.handler.codec.compression.ZlibWrapper; +import org.jboss.netty.handler.codec.embedder.EncoderEmbedder; + +/** + * Decompresses an {@link HttpMessage} and an {@link HttpChunk} compressed in + * {@code gzip} or {@code deflate} encoding. Insert this handler after + * {@link HttpMessageDecoder} in the {@link ChannelPipeline}: + *
+ * ChannelPipeline p = ...;
+ * ...
+ * p.addLast("decoder", new HttpRequestDecoder());
+ * p.addLast("inflater", new HttpContentDecomperssor());
+ * ...
+ * p.addLast("encoder", new HttpResponseEncoder());
+ * p.addLast("handler", new HttpRequestHandler());
+ * 
+ * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Trustin Lee (tlee@redhat.com) + * @version $Rev$, $Date$ + */ +@ChannelPipelineCoverage("one") +public class HttpContentCompressor extends HttpContentEncoder { + + private final int compressionLevel; + + public HttpContentCompressor() { + this(6); + } + + public HttpContentCompressor(int compressionLevel) { + if (compressionLevel < 0 || compressionLevel > 9) { + throw new IllegalArgumentException( + "compressionLevel: " + compressionLevel + + " (expected: 0-9)"); + } + this.compressionLevel = compressionLevel; + } + + @Override + protected EncoderEmbedder newContentEncoder(String acceptEncoding) throws Exception { + ZlibWrapper wrapper = determineWrapper(acceptEncoding); + if (wrapper == null) { + return null; + } + + return new EncoderEmbedder(new ZlibEncoder(wrapper, compressionLevel)); + } + + @Override + protected String getTargetContentEncoding(String acceptEncoding) throws Exception { + ZlibWrapper wrapper = determineWrapper(acceptEncoding); + if (wrapper == null) { + return null; + } + + switch (wrapper) { + case GZIP: + return "gzip"; + case ZLIB: + return "deflate"; + default: + throw new Error(); + } + } + + private ZlibWrapper determineWrapper(String acceptEncoding) { + // FIXME: Use the Q value. + if (acceptEncoding.indexOf("gzip") >= 0) { + return ZlibWrapper.GZIP; + } + if (acceptEncoding.indexOf("deflate") >= 0) { + return ZlibWrapper.ZLIB; + } + return null; + } +} diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentDecoder.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentDecoder.java index 71b08f273c..eac7265914 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentDecoder.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentDecoder.java @@ -28,15 +28,15 @@ import org.jboss.netty.handler.codec.embedder.DecoderEmbedder; * Decodes the content of the received {@link HttpMessage} and {@link HttpChunk}. * The original content ({@link HttpMessage#getContent()} or {@link HttpChunk#getContent()}) * is replaced with the new content decoded by the {@link DecoderEmbedder}, - * which is created by {@link #newDecoder(String)}. Once decoding is finished, + * which is created by {@link #newContentDecoder(String)}. Once decoding is finished, * the value of the 'Content-Encoding' header is set to 'identity' * and the 'Content-Length' header is updated to the length of the * decoded content. If the content encoding of the original is not supported - * by the decoder, {@link #newDecoder(String)} returns {@code null} and no + * by the decoder, {@link #newContentDecoder(String)} returns {@code null} and no * decoding occurs (i.e. pass-through). *

* Please note that this is an abstract class. You have to extend this class - * and implement {@link #newDecoder(String)} properly to make this class + * and implement {@link #newContentDecoder(String)} properly to make this class * functional. For example, refer to the source code of {@link HttpContentDecompressor}. * * @author The Netty Project (netty-dev@lists.jboss.org) @@ -46,7 +46,6 @@ import org.jboss.netty.handler.codec.embedder.DecoderEmbedder; @ChannelPipelineCoverage("one") public abstract class HttpContentDecoder extends SimpleChannelUpstreamHandler { - private volatile HttpMessage previous; private volatile DecoderEmbedder decoder; /** @@ -63,11 +62,6 @@ public abstract class HttpContentDecoder extends SimpleChannelUpstreamHandler { HttpMessage m = (HttpMessage) msg; decoder = null; - if (m.isChunked()) { - previous = m; - } else { - previous = null; - } // Determine the content encoding. String contentEncoding = m.getHeader(HttpHeaders.Names.CONTENT_ENCODING); @@ -77,29 +71,25 @@ public abstract class HttpContentDecoder extends SimpleChannelUpstreamHandler { contentEncoding = HttpHeaders.Values.IDENTITY; } - if ((decoder = newDecoder(contentEncoding)) != null) { + if ((decoder = newContentDecoder(contentEncoding)) != null) { // Decode the content and remove or replace the existing headers // so that the message looks like a decoded message. m.setHeader( HttpHeaders.Names.CONTENT_ENCODING, - getTargetEncoding(contentEncoding)); + getTargetContentEncoding(contentEncoding)); if (!m.isChunked()) { ChannelBuffer content = m.getContent(); - if (content.readable()) { - // Decode the content - content = ChannelBuffers.wrappedBuffer( - decode(content), finishDecode()); + // Decode the content + content = ChannelBuffers.wrappedBuffer( + decode(content), finishDecode()); - // Replace the content if necessary. - if (content != null) { - m.setContent(content); - if (m.containsHeader(HttpHeaders.Names.CONTENT_LENGTH)) { - m.setHeader( - HttpHeaders.Names.CONTENT_LENGTH, - Integer.toString(content.readableBytes())); - } - } + // Replace the content. + m.setContent(content); + if (m.containsHeader(HttpHeaders.Names.CONTENT_LENGTH)) { + m.setHeader( + HttpHeaders.Names.CONTENT_LENGTH, + Integer.toString(content.readableBytes())); } } } @@ -107,8 +97,6 @@ public abstract class HttpContentDecoder extends SimpleChannelUpstreamHandler { // Because HttpMessage is a mutable object, we can simply forward the received event. ctx.sendUpstream(e); } else if (msg instanceof HttpChunk) { - assert previous != null; - HttpChunk c = (HttpChunk) msg; ChannelBuffer content = c.getContent(); @@ -124,8 +112,6 @@ public abstract class HttpContentDecoder extends SimpleChannelUpstreamHandler { } } else { ChannelBuffer lastProduct = finishDecode(); - previous = null; - decoder = null; // Generate an additional chunk if the decoder produced // the last product on closure, @@ -154,7 +140,7 @@ public abstract class HttpContentDecoder extends SimpleChannelUpstreamHandler { * {@code null} otherwise (alternatively, you can throw an exception * to block unknown encoding). */ - protected abstract DecoderEmbedder newDecoder(String contentEncoding) throws Exception; + protected abstract DecoderEmbedder newContentDecoder(String contentEncoding) throws Exception; /** * Returns the expected content encoding of the decoded content. @@ -164,7 +150,7 @@ public abstract class HttpContentDecoder extends SimpleChannelUpstreamHandler { * @param contentEncoding the content encoding of the original content * @return the expected content encoding of the new content */ - protected String getTargetEncoding(String contentEncoding) throws Exception { + protected String getTargetContentEncoding(String contentEncoding) throws Exception { return HttpHeaders.Values.IDENTITY; } @@ -174,10 +160,13 @@ public abstract class HttpContentDecoder extends SimpleChannelUpstreamHandler { } private ChannelBuffer finishDecode() { + ChannelBuffer result; if (decoder.finish()) { - return ChannelBuffers.wrappedBuffer(decoder.pollAll(new ChannelBuffer[decoder.size()])); + result = ChannelBuffers.wrappedBuffer(decoder.pollAll(new ChannelBuffer[decoder.size()])); } else { - return ChannelBuffers.EMPTY_BUFFER; + result = ChannelBuffers.EMPTY_BUFFER; } + decoder = null; + return result; } } diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentDecompressor.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentDecompressor.java index 8c5b3cc43c..2bbe6f2a1d 100644 --- a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentDecompressor.java +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentDecompressor.java @@ -43,7 +43,7 @@ import org.jboss.netty.handler.codec.embedder.DecoderEmbedder; @ChannelPipelineCoverage("one") public class HttpContentDecompressor extends HttpContentDecoder { @Override - protected DecoderEmbedder newDecoder(String contentEncoding) throws Exception { + protected DecoderEmbedder newContentDecoder(String contentEncoding) throws Exception { if ("gzip".equalsIgnoreCase(contentEncoding) || "x-gzip".equalsIgnoreCase(contentEncoding)) { return new DecoderEmbedder(new ZlibDecoder(ZlibWrapper.GZIP)); } else if ("deflate".equalsIgnoreCase(contentEncoding) || "x-deflate".equalsIgnoreCase(contentEncoding)) { diff --git a/src/main/java/org/jboss/netty/handler/codec/http/HttpContentEncoder.java b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentEncoder.java new file mode 100644 index 0000000000..0a446cda97 --- /dev/null +++ b/src/main/java/org/jboss/netty/handler/codec/http/HttpContentEncoder.java @@ -0,0 +1,179 @@ +/* + * Copyright 2009 Red Hat, Inc. + * + * Red Hat 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.jboss.netty.handler.codec.http; + +import java.util.Queue; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.Channels; +import org.jboss.netty.channel.MessageEvent; +import org.jboss.netty.channel.SimpleChannelHandler; +import org.jboss.netty.handler.codec.embedder.DecoderEmbedder; +import org.jboss.netty.handler.codec.embedder.EncoderEmbedder; +import org.jboss.netty.util.internal.LinkedTransferQueue; + +/** + * + * @author The Netty Project (netty-dev@lists.jboss.org) + * @author Trustin Lee (tlee@redhat.com) + * @version $Rev$, $Date$ + */ +public abstract class HttpContentEncoder extends SimpleChannelHandler { + + private final Queue acceptEncodingQueue = new LinkedTransferQueue(); + private volatile EncoderEmbedder encoder; + + /** + * Creates a new instance. + */ + protected HttpContentEncoder() { + super(); + } + + @Override + public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + Object msg = e.getMessage(); + if (!(msg instanceof HttpMessage)) { + ctx.sendUpstream(e); + return; + } + + HttpMessage m = (HttpMessage) msg; + String acceptedEncoding = m.getHeader(HttpHeaders.Names.ACCEPT_ENCODING); + if (acceptedEncoding == null) { + acceptedEncoding = HttpHeaders.Values.IDENTITY; + } + acceptEncodingQueue.offer(acceptedEncoding); + + ctx.sendUpstream(e); + } + + @Override + public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) + throws Exception { + + Object msg = e.getMessage(); + if (msg instanceof HttpMessage) { + HttpMessage m = (HttpMessage) msg; + + encoder = null; + + // Determine the content encoding. + String acceptEncoding = acceptEncodingQueue.poll(); + if (acceptEncoding == null) { + throw new IllegalStateException("cannot send more responses than requests"); + } + + if ((encoder = newContentEncoder(acceptEncoding)) != null) { + // Encode the content and remove or replace the existing headers + // so that the message looks like a decoded message. + m.setHeader( + HttpHeaders.Names.CONTENT_ENCODING, + getTargetContentEncoding(acceptEncoding)); + + if (!m.isChunked()) { + ChannelBuffer content = m.getContent(); + // Encode the content. + content = ChannelBuffers.wrappedBuffer( + encode(content), finishEncode()); + + // Replace the content. + m.setContent(content); + if (m.containsHeader(HttpHeaders.Names.CONTENT_LENGTH)) { + m.setHeader( + HttpHeaders.Names.CONTENT_LENGTH, + Integer.toString(content.readableBytes())); + } + } + } + + // Because HttpMessage is a mutable object, we can simply forward the write request. + ctx.sendDownstream(e); + } else if (msg instanceof HttpChunk) { + HttpChunk c = (HttpChunk) msg; + ChannelBuffer content = c.getContent(); + + // Encode the chunk if necessary. + if (encoder != null) { + if (!c.isLast()) { + content = encode(content); + if (content.readable()) { + // Note that HttpChunk is immutable unlike HttpMessage. + // XXX API inconsistency? I can live with it though. + Channels.write( + ctx, e.getFuture(), new DefaultHttpChunk(content), e.getRemoteAddress()); + } + } else { + ChannelBuffer lastProduct = finishEncode(); + + // Generate an additional chunk if the decoder produced + // the last product on closure, + if (lastProduct.readable()) { + Channels.write( + ctx, Channels.succeededFuture(e.getChannel()), new DefaultHttpChunk(lastProduct), e.getRemoteAddress()); + } + + // Emit the last chunk. + ctx.sendDownstream(e); + } + } else { + ctx.sendDownstream(e); + } + } else { + ctx.sendDownstream(e); + } + } + + /** + * Returns a new {@link EncoderEmbedder} that encodes the HTTP message + * content. + * + * @param acceptEncoding + * the value of the {@code "Accept-Encoding"} header. + * + * @return a new {@link DecoderEmbedder} if the specified encoding is supported. + * {@code null} otherwise (alternatively, you can throw an exception + * to block unknown encoding). + */ + protected abstract EncoderEmbedder newContentEncoder(String acceptEncoding) throws Exception; + + /** + * Returns the expected content encoding of the encoded content. + * + * @param contentEncoding the content encoding of the original content + * @return the expected content encoding of the new content + */ + protected abstract String getTargetContentEncoding(String acceptEncoding) throws Exception; + + private ChannelBuffer encode(ChannelBuffer buf) { + encoder.offer(buf); + return ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()])); + } + + private ChannelBuffer finishEncode() { + ChannelBuffer result; + if (encoder.finish()) { + result = ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()])); + } else { + result = ChannelBuffers.EMPTY_BUFFER; + } + encoder = null; + return result; + } +}