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 java.util.Queue;
19  import java.util.concurrent.ConcurrentLinkedQueue;
20  
21  import org.jboss.netty.buffer.ChannelBuffer;
22  import org.jboss.netty.buffer.ChannelBuffers;
23  import org.jboss.netty.channel.ChannelHandlerContext;
24  import org.jboss.netty.channel.ChannelStateEvent;
25  import org.jboss.netty.channel.Channels;
26  import org.jboss.netty.channel.LifeCycleAwareChannelHandler;
27  import org.jboss.netty.channel.MessageEvent;
28  import org.jboss.netty.channel.SimpleChannelHandler;
29  import org.jboss.netty.handler.codec.embedder.EncoderEmbedder;
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  
58      /**
59       * Creates a new instance.
60       */
61      protected HttpContentEncoder() {
62      }
63  
64      @Override
65      public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
66              throws Exception {
67          Object msg = e.getMessage();
68          if (!(msg instanceof HttpMessage)) {
69              ctx.sendUpstream(e);
70              return;
71          }
72  
73          HttpMessage m = (HttpMessage) msg;
74          String acceptedEncoding = m.getHeader(HttpHeaders.Names.ACCEPT_ENCODING);
75          if (acceptedEncoding == null) {
76              acceptedEncoding = HttpHeaders.Values.IDENTITY;
77          }
78          boolean offered = acceptEncodingQueue.offer(acceptedEncoding);
79          assert offered;
80  
81          ctx.sendUpstream(e);
82      }
83  
84      @Override
85      public void writeRequested(ChannelHandlerContext ctx, MessageEvent e)
86              throws Exception {
87  
88          Object msg = e.getMessage();
89          if (msg instanceof HttpResponse && ((HttpResponse) msg).getStatus().getCode() == 100) {
90              // 100-continue response must be passed through.
91              ctx.sendDownstream(e);
92          } else  if (msg instanceof HttpMessage) {
93              HttpMessage m = (HttpMessage) msg;
94  
95              // Clean-up the previous encoder if not cleaned up correctly.
96              finishEncode();
97  
98              String acceptEncoding = acceptEncodingQueue.poll();
99              if (acceptEncoding == null) {
100                 throw new IllegalStateException("cannot send more responses than requests");
101             }
102 
103             String contentEncoding = m.getHeader(HttpHeaders.Names.CONTENT_ENCODING);
104             if (contentEncoding != null &&
105                 !HttpHeaders.Values.IDENTITY.equalsIgnoreCase(contentEncoding)) {
106                 // Content-Encoding is set already and it is not 'identity'.
107                 ctx.sendDownstream(e);
108             } else {
109                 // Determine the content encoding.
110                 boolean hasContent = m.isChunked() || m.getContent().readable();
111                 if (hasContent && (encoder = newContentEncoder(m, acceptEncoding)) != null) {
112                     // Encode the content and remove or replace the existing headers
113                     // so that the message looks like a decoded message.
114                     m.setHeader(
115                             HttpHeaders.Names.CONTENT_ENCODING,
116                             getTargetContentEncoding(acceptEncoding));
117 
118                     if (!m.isChunked()) {
119                         ChannelBuffer content = m.getContent();
120                         // Encode the content.
121                         content = ChannelBuffers.wrappedBuffer(
122                                 encode(content), finishEncode());
123 
124                         // Replace the content.
125                         m.setContent(content);
126                         if (m.containsHeader(HttpHeaders.Names.CONTENT_LENGTH)) {
127                             m.setHeader(
128                                     HttpHeaders.Names.CONTENT_LENGTH,
129                                     Integer.toString(content.readableBytes()));
130                         }
131                     }
132                 }
133 
134                 // Because HttpMessage is a mutable object, we can simply forward the write request.
135                 ctx.sendDownstream(e);
136             }
137         } else if (msg instanceof HttpChunk) {
138             HttpChunk c = (HttpChunk) msg;
139             ChannelBuffer content = c.getContent();
140 
141             // Encode the chunk if necessary.
142             if (encoder != null) {
143                 if (!c.isLast()) {
144                     content = encode(content);
145                     if (content.readable()) {
146                         c.setContent(content);
147                         ctx.sendDownstream(e);
148                     }
149                 } else {
150                     ChannelBuffer lastProduct = finishEncode();
151 
152                     // Generate an additional chunk if the decoder produced
153                     // the last product on closure,
154                     if (lastProduct.readable()) {
155                         Channels.write(
156                                 ctx, Channels.succeededFuture(e.getChannel()),
157                                 new DefaultHttpChunk(lastProduct), e.getRemoteAddress());
158                     }
159 
160                     // Emit the last chunk.
161                     ctx.sendDownstream(e);
162                 }
163             } else {
164                 ctx.sendDownstream(e);
165             }
166         } else {
167             ctx.sendDownstream(e);
168         }
169     }
170 
171     @Override
172     public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
173         // Clean-up the previous encoder if not cleaned up correctly.
174         finishEncode();
175 
176         super.channelClosed(ctx, e);
177     }
178 
179     /**
180      * Returns a new {@link EncoderEmbedder} that encodes the HTTP message
181      * content.
182      *
183      * @param acceptEncoding
184      *        the value of the {@code "Accept-Encoding"} header
185      *
186      * @return a new {@link EncoderEmbedder} if there is a supported encoding
187      *         in {@code acceptEncoding}.  {@code null} otherwise.
188      */
189     protected abstract EncoderEmbedder<ChannelBuffer> newContentEncoder(
190             HttpMessage msg, String acceptEncoding) throws Exception;
191 
192     /**
193      * Returns the expected content encoding of the encoded content.
194      *
195      * @param acceptEncoding the value of the {@code "Accept-Encoding"} header
196      * @return the expected content encoding of the new content
197      */
198     protected abstract String getTargetContentEncoding(String acceptEncoding) throws Exception;
199 
200     private ChannelBuffer encode(ChannelBuffer buf) {
201         encoder.offer(buf);
202         return ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()]));
203     }
204 
205     private ChannelBuffer finishEncode() {
206         if (encoder == null) {
207             return ChannelBuffers.EMPTY_BUFFER;
208         }
209 
210         ChannelBuffer result;
211         if (encoder.finish()) {
212             result = ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()]));
213         } else {
214             result = ChannelBuffers.EMPTY_BUFFER;
215         }
216         encoder = null;
217         return result;
218     }
219 
220     public void beforeAdd(ChannelHandlerContext ctx) throws Exception {
221         // NOOP
222     }
223 
224     public void afterAdd(ChannelHandlerContext ctx) throws Exception {
225         // NOOP
226     }
227 
228     public void beforeRemove(ChannelHandlerContext ctx) throws Exception {
229         // NOOP
230     }
231 
232     public void afterRemove(ChannelHandlerContext ctx) throws Exception {
233         finishEncode();
234     }
235 }