View Javadoc

1   /*
2    * Copyright 2009 Red Hat, Inc.
3    *
4    * Red Hat licenses this file to you under the Apache License, version 2.0
5    * (the "License"); you may not use this file except in compliance with the
6    * License.  You may obtain a copy of the License at:
7    *
8    *    http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package org.jboss.netty.handler.codec.http;
17  
18  import java.util.Queue;
19  
20  import org.jboss.netty.buffer.ChannelBuffer;
21  import org.jboss.netty.buffer.ChannelBuffers;
22  import org.jboss.netty.channel.ChannelHandlerContext;
23  import org.jboss.netty.channel.Channels;
24  import org.jboss.netty.channel.MessageEvent;
25  import org.jboss.netty.channel.SimpleChannelHandler;
26  import org.jboss.netty.handler.codec.embedder.EncoderEmbedder;
27  import org.jboss.netty.util.internal.LinkedTransferQueue;
28  
29  /**
30   * Encodes the content of the outbound {@link HttpResponse} and {@link HttpChunk}.
31   * The original content is replaced with the new content encoded by the
32   * {@link EncoderEmbedder}, which is created by {@link #newContentEncoder(String)}.
33   * Once encoding is finished, the value of the <tt>'Content-Encoding'</tt> header
34   * is set to the target content encoding, as returned by {@link #getTargetContentEncoding(String)}.
35   * Also, the <tt>'Content-Length'</tt> header is updated to the length of the
36   * encoded content.  If there is no supported encoding in the
37   * corresponding {@link HttpRequest}'s {@code "Accept-Encoding"} header,
38   * {@link #newContentEncoder(String)} should return {@code null} so that no
39   * encoding occurs (i.e. pass-through).
40   * <p>
41   * Please note that this is an abstract class.  You have to extend this class
42   * and implement {@link #newContentEncoder(String)} and {@link #getTargetContentEncoding(String)}
43   * properly to make this class functional.  For example, refer to the source
44   * code of {@link HttpContentCompressor}.
45   * <p>
46   * This handler must be placed after {@link HttpMessageEncoder} in the pipeline
47   * so that this handler can intercept HTTP responses before {@link HttpMessageEncoder}
48   * converts them into {@link ChannelBuffer}s.
49   *
50   * @author <a href="http://www.jboss.org/netty/">The Netty Project</a>
51   * @author <a href="http://gleamynode.net/">Trustin Lee</a>
52   * @version $Rev: 2368 $, $Date: 2010-10-18 17:19:03 +0900 (Mon, 18 Oct 2010) $
53   */
54  public abstract class HttpContentEncoder extends SimpleChannelHandler {
55  
56      private final Queue<String> acceptEncodingQueue = new LinkedTransferQueue<String>();
57      private volatile EncoderEmbedder<ChannelBuffer> encoder;
58  
59      /**
60       * Creates a new instance.
61       */
62      protected HttpContentEncoder() {
63          super();
64      }
65  
66      @Override
67      public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
68              throws Exception {
69          Object msg = e.getMessage();
70          if (!(msg instanceof HttpMessage)) {
71              ctx.sendUpstream(e);
72              return;
73          }
74  
75          HttpMessage m = (HttpMessage) msg;
76          String acceptedEncoding = m.getHeader(HttpHeaders.Names.ACCEPT_ENCODING);
77          if (acceptedEncoding == null) {
78              acceptedEncoding = HttpHeaders.Values.IDENTITY;
79          }
80          boolean offered = acceptEncodingQueue.offer(acceptedEncoding);
81          assert offered;
82  
83          ctx.sendUpstream(e);
84      }
85  
86      @Override
87      public void writeRequested(ChannelHandlerContext ctx, MessageEvent e)
88              throws Exception {
89  
90          Object msg = e.getMessage();
91          if (msg instanceof HttpResponse && ((HttpResponse) msg).getStatus().getCode() == 100) {
92              // 100-continue response must be passed through.
93              ctx.sendDownstream(e);
94          } else  if (msg instanceof HttpMessage) {
95              HttpMessage m = (HttpMessage) msg;
96  
97              encoder = null;
98  
99              // Determine the content encoding.
100             String acceptEncoding = acceptEncodingQueue.poll();
101             if (acceptEncoding == null) {
102                 throw new IllegalStateException("cannot send more responses than requests");
103             }
104 
105             if ((encoder = newContentEncoder(acceptEncoding)) != null) {
106                 // Encode the content and remove or replace the existing headers
107                 // so that the message looks like a decoded message.
108                 m.setHeader(
109                         HttpHeaders.Names.CONTENT_ENCODING,
110                         getTargetContentEncoding(acceptEncoding));
111 
112                 if (!m.isChunked()) {
113                     ChannelBuffer content = m.getContent();
114                     // Encode the content.
115                     content = ChannelBuffers.wrappedBuffer(
116                             encode(content), finishEncode());
117 
118                     // Replace the content.
119                     m.setContent(content);
120                     if (m.containsHeader(HttpHeaders.Names.CONTENT_LENGTH)) {
121                         m.setHeader(
122                                 HttpHeaders.Names.CONTENT_LENGTH,
123                                 Integer.toString(content.readableBytes()));
124                     }
125                 }
126             }
127 
128             // Because HttpMessage is a mutable object, we can simply forward the write request.
129             ctx.sendDownstream(e);
130         } else if (msg instanceof HttpChunk) {
131             HttpChunk c = (HttpChunk) msg;
132             ChannelBuffer content = c.getContent();
133 
134             // Encode the chunk if necessary.
135             if (encoder != null) {
136                 if (!c.isLast()) {
137                     content = encode(content);
138                     if (content.readable()) {
139                         c.setContent(content);
140                         ctx.sendDownstream(e);
141                     }
142                 } else {
143                     ChannelBuffer lastProduct = finishEncode();
144 
145                     // Generate an additional chunk if the decoder produced
146                     // the last product on closure,
147                     if (lastProduct.readable()) {
148                         Channels.write(
149                                 ctx, Channels.succeededFuture(e.getChannel()), new DefaultHttpChunk(lastProduct), e.getRemoteAddress());
150                     }
151 
152                     // Emit the last chunk.
153                     ctx.sendDownstream(e);
154                 }
155             } else {
156                 ctx.sendDownstream(e);
157             }
158         } else {
159             ctx.sendDownstream(e);
160         }
161     }
162 
163     /**
164      * Returns a new {@link EncoderEmbedder} that encodes the HTTP message
165      * content.
166      *
167      * @param acceptEncoding
168      *        the value of the {@code "Accept-Encoding"} header
169      *
170      * @return a new {@link EncoderEmbedder} if there is a supported encoding
171      *         in {@code acceptEncoding}.  {@code null} otherwise.
172      */
173     protected abstract EncoderEmbedder<ChannelBuffer> newContentEncoder(String acceptEncoding) throws Exception;
174 
175     /**
176      * Returns the expected content encoding of the encoded content.
177      *
178      * @param acceptEncoding the value of the {@code "Accept-Encoding"} header
179      * @return the expected content encoding of the new content
180      */
181     protected abstract String getTargetContentEncoding(String acceptEncoding) throws Exception;
182 
183     private ChannelBuffer encode(ChannelBuffer buf) {
184         encoder.offer(buf);
185         return ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()]));
186     }
187 
188     private ChannelBuffer finishEncode() {
189         ChannelBuffer result;
190         if (encoder.finish()) {
191             result = ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()]));
192         } else {
193             result = ChannelBuffers.EMPTY_BUFFER;
194         }
195         encoder = null;
196         return result;
197     }
198 }