View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the 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 org.jboss.netty.buffer.ChannelBuffer;
19  import org.jboss.netty.buffer.ChannelBuffers;
20  import org.jboss.netty.channel.ChannelHandlerContext;
21  import org.jboss.netty.channel.ChannelStateEvent;
22  import org.jboss.netty.channel.Channels;
23  import org.jboss.netty.channel.LifeCycleAwareChannelHandler;
24  import org.jboss.netty.channel.MessageEvent;
25  import org.jboss.netty.channel.SimpleChannelHandler;
26  import org.jboss.netty.handler.codec.embedder.EncoderEmbedder;
27  
28  import java.util.Queue;
29  import java.util.concurrent.ConcurrentLinkedQueue;
30  
31  /**
32   * Encodes the content of the outbound {@link HttpResponse} and {@link HttpChunk}.
33   * The original content is replaced with the new content encoded by the
34   * {@link EncoderEmbedder}, which is created by {@link #newContentEncoder(HttpMessage, String)}.
35   * Once encoding is finished, the value of the <tt>'Content-Encoding'</tt> header
36   * is set to the target content encoding, as returned by {@link #getTargetContentEncoding(String)}.
37   * Also, the <tt>'Content-Length'</tt> header is updated to the length of the
38   * encoded content.  If there is no supported encoding in the
39   * corresponding {@link HttpRequest}'s {@code "Accept-Encoding"} header,
40   * {@link #newContentEncoder(HttpMessage, String)} should return {@code null} so that no
41   * encoding occurs (i.e. pass-through).
42   * <p>
43   * Please note that this is an abstract class.  You have to extend this class
44   * and implement {@link #newContentEncoder(HttpMessage, String)} and {@link #getTargetContentEncoding(String)}
45   * properly to make this class functional.  For example, refer to the source
46   * code of {@link HttpContentCompressor}.
47   * <p>
48   * This handler must be placed after {@link HttpMessageEncoder} in the pipeline
49   * so that this handler can intercept HTTP responses before {@link HttpMessageEncoder}
50   * converts them into {@link ChannelBuffer}s.
51   */
52  public abstract class HttpContentEncoder extends SimpleChannelHandler
53                                           implements LifeCycleAwareChannelHandler {
54  
55      private final Queue<String> acceptEncodingQueue = new ConcurrentLinkedQueue<String>();
56      private volatile EncoderEmbedder<ChannelBuffer> encoder;
57      private volatile boolean offerred;
58  
59      /**
60       * Creates a new instance.
61       */
62      protected HttpContentEncoder() {
63      }
64  
65      @Override
66      public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
67              throws Exception {
68          Object msg = e.getMessage();
69          if (!(msg instanceof HttpMessage)) {
70              ctx.sendUpstream(e);
71              return;
72          }
73  
74          HttpMessage m = (HttpMessage) msg;
75          String acceptedEncoding = m.headers().get(HttpHeaders.Names.ACCEPT_ENCODING);
76          if (acceptedEncoding == null) {
77              acceptedEncoding = HttpHeaders.Values.IDENTITY;
78          }
79          boolean offered = acceptEncodingQueue.offer(acceptedEncoding);
80          assert offered;
81  
82          ctx.sendUpstream(e);
83      }
84  
85      @Override
86      public void writeRequested(ChannelHandlerContext ctx, MessageEvent e)
87              throws Exception {
88  
89          Object msg = e.getMessage();
90          if (msg instanceof HttpResponse && ((HttpResponse) msg).getStatus().getCode() == 100) {
91              // 100-continue response must be passed through.
92              ctx.sendDownstream(e);
93          } else  if (msg instanceof HttpMessage) {
94              HttpMessage m = (HttpMessage) msg;
95  
96              // Clean-up the previous encoder if not cleaned up correctly.
97              finishEncode();
98  
99              String acceptEncoding = acceptEncodingQueue.poll();
100             if (acceptEncoding == null) {
101                 throw new IllegalStateException("cannot send more responses than requests");
102             }
103 
104             String contentEncoding = m.headers().get(HttpHeaders.Names.CONTENT_ENCODING);
105             if (contentEncoding != null &&
106                 !HttpHeaders.Values.IDENTITY.equalsIgnoreCase(contentEncoding)) {
107                 // Content-Encoding is set already and it is not 'identity'.
108                 ctx.sendDownstream(e);
109             } else {
110                 // Determine the content encoding.
111                 boolean hasContent = m.isChunked() || m.getContent().readable();
112                 if (hasContent && (encoder = newContentEncoder(m, acceptEncoding)) != null) {
113                     // Encode the content and remove or replace the existing headers
114                     // so that the message looks like a decoded message.
115                     m.headers().set(
116                             HttpHeaders.Names.CONTENT_ENCODING,
117                             getTargetContentEncoding(acceptEncoding));
118 
119                     if (m.isChunked()) {
120                         m.headers().remove(HttpHeaders.Names.CONTENT_LENGTH);
121                     } else {
122                         ChannelBuffer content = m.getContent();
123                         // Encode the content.
124                         content = ChannelBuffers.wrappedBuffer(
125                                 encode(content), finishEncode());
126 
127                         // Replace the content.
128                         m.setContent(content);
129                         if (m.headers().contains(HttpHeaders.Names.CONTENT_LENGTH)) {
130                             m.headers().set(
131                                     HttpHeaders.Names.CONTENT_LENGTH,
132                                     Integer.toString(content.readableBytes()));
133                         }
134                     }
135                 }
136 
137                 // Because HttpMessage is a mutable object, we can simply forward the write request.
138                 ctx.sendDownstream(e);
139             }
140         } else if (msg instanceof HttpChunk) {
141             HttpChunk c = (HttpChunk) msg;
142             ChannelBuffer content = c.getContent();
143 
144             // Encode the chunk if necessary.
145             if (encoder != null) {
146                 if (!c.isLast()) {
147                     content = encode(content);
148                     if (content.readable()) {
149                         c.setContent(content);
150                         ctx.sendDownstream(e);
151                     }
152                 } else {
153                     ChannelBuffer lastProduct = finishEncode();
154 
155                     // Generate an additional chunk if the decoder produced
156                     // the last product on closure,
157                     if (lastProduct.readable()) {
158                         Channels.write(
159                                 ctx, Channels.succeededFuture(e.getChannel()),
160                                 new DefaultHttpChunk(lastProduct), e.getRemoteAddress());
161                     }
162 
163                     // Emit the last chunk.
164                     ctx.sendDownstream(e);
165                 }
166             } else {
167                 ctx.sendDownstream(e);
168             }
169         } else {
170             ctx.sendDownstream(e);
171         }
172     }
173 
174     @Override
175     public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
176         // Clean-up the previous encoder if not cleaned up correctly.
177         finishEncode();
178 
179         super.channelClosed(ctx, e);
180     }
181 
182     /**
183      * Returns a new {@link EncoderEmbedder} that encodes the HTTP message
184      * content.
185      *
186      * @param acceptEncoding
187      *        the value of the {@code "Accept-Encoding"} header
188      *
189      * @return a new {@link EncoderEmbedder} if there is a supported encoding
190      *         in {@code acceptEncoding}.  {@code null} otherwise.
191      */
192     protected abstract EncoderEmbedder<ChannelBuffer> newContentEncoder(
193             HttpMessage msg, String acceptEncoding) throws Exception;
194 
195     /**
196      * Returns the expected content encoding of the encoded content.
197      *
198      * @param acceptEncoding the value of the {@code "Accept-Encoding"} header
199      * @return the expected content encoding of the new content
200      */
201     protected abstract String getTargetContentEncoding(String acceptEncoding) throws Exception;
202 
203     private ChannelBuffer encode(ChannelBuffer buf) {
204         offerred = true;
205         encoder.offer(buf);
206         return ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()]));
207     }
208 
209     private ChannelBuffer finishEncode() {
210         if (encoder == null) {
211             offerred = false;
212             return ChannelBuffers.EMPTY_BUFFER;
213         }
214 
215         ChannelBuffer result;
216         if (!offerred) {
217             // No data was offerred to the encoder since the encoder was created.
218             // We should offer at least an empty buffer so that the encoder knows its is encoding empty content.
219             offerred = false;
220             encoder.offer(ChannelBuffers.EMPTY_BUFFER);
221         }
222         if (encoder.finish()) {
223             result = ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()]));
224         } else {
225             result = ChannelBuffers.EMPTY_BUFFER;
226         }
227         encoder = null;
228         return result;
229     }
230 
231     public void beforeAdd(ChannelHandlerContext ctx) throws Exception {
232         // NOOP
233     }
234 
235     public void afterAdd(ChannelHandlerContext ctx) throws Exception {
236         // NOOP
237     }
238 
239     public void beforeRemove(ChannelHandlerContext ctx) throws Exception {
240         // NOOP
241     }
242 
243     public void afterRemove(ChannelHandlerContext ctx) throws Exception {
244         finishEncode();
245     }
246 }