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 }