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          super();
    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.getHeader(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.getHeader(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.setHeader(
    116                             HttpHeaders.Names.CONTENT_ENCODING,
    117                             getTargetContentEncoding(acceptEncoding));
    118
    119                     if (!m.isChunked()) {
    120                         ChannelBuffer content = m.getContent();
    121                         // Encode the content.
    122                         content = ChannelBuffers.wrappedBuffer(
    123                                 encode(content), finishEncode());
    124
    125                         // Replace the content.
    126                         m.setContent(content);
    127                         if (m.containsHeader(HttpHeaders.Names.CONTENT_LENGTH)) {
    128                             m.setHeader(
    129                                     HttpHeaders.Names.CONTENT_LENGTH,
    130                                     Integer.toString(content.readableBytes()));
    131                         }
    132                     }
    133                 }
    134
    135                 // Because HttpMessage is a mutable object, we can simply forward the write request.
    136                 ctx.sendDownstream(e);
    137             }
    138         } else if (msg instanceof HttpChunk) {
    139             HttpChunk c = (HttpChunk) msg;
    140             ChannelBuffer content = c.getContent();
    141
    142             // Encode the chunk if necessary.
    143             if (encoder != null) {
    144                 if (!c.isLast()) {
    145                     content = encode(content);
    146                     if (content.readable()) {
    147                         c.setContent(content);
    148                         ctx.sendDownstream(e);
    149                     }
    150                 } else {
    151                     ChannelBuffer lastProduct = finishEncode();
    152
    153                     // Generate an additional chunk if the decoder produced
    154                     // the last product on closure,
    155                     if (lastProduct.readable()) {
    156                         Channels.write(
    157                                 ctx, Channels.succeededFuture(e.getChannel()),
    158                                 new DefaultHttpChunk(lastProduct), e.getRemoteAddress());
    159                     }
    160
    161                     // Emit the last chunk.
    162                     ctx.sendDownstream(e);
    163                 }
    164             } else {
    165                 ctx.sendDownstream(e);
    166             }
    167         } else {
    168             ctx.sendDownstream(e);
    169         }
    170     }
    171
    172     @Override
    173     public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
    174         // Clean-up the previous encoder if not cleaned up correctly.
    175         finishEncode();
    176
    177         super.channelClosed(ctx, e);
    178     }
    179
    180     /**
    181      * Returns a new {@link EncoderEmbedder} that encodes the HTTP message
    182      * content.
    183      *
    184      * @param acceptEncoding
    185      *        the value of the {@code "Accept-Encoding"} header
    186      *
    187      * @return a new {@link EncoderEmbedder} if there is a supported encoding
    188      *         in {@code acceptEncoding}.  {@code null} otherwise.
    189      */
    190     protected abstract EncoderEmbedder<ChannelBuffer> newContentEncoder(
    191             HttpMessage msg, String acceptEncoding) throws Exception;
    192
    193     /**
    194      * Returns the expected content encoding of the encoded content.
    195      *
    196      * @param acceptEncoding the value of the {@code "Accept-Encoding"} header
    197      * @return the expected content encoding of the new content
    198      */
    199     protected abstract String getTargetContentEncoding(String acceptEncoding) throws Exception;
    200
    201     private ChannelBuffer encode(ChannelBuffer buf) {
    202         encoder.offer(buf);
    203         return ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()]));
    204     }
    205
    206     private ChannelBuffer finishEncode() {
    207         if (encoder == null) {
    208             return ChannelBuffers.EMPTY_BUFFER;
    209         }
    210
    211         ChannelBuffer result;
    212         if (encoder.finish()) {
    213             result = ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()]));
    214         } else {
    215             result = ChannelBuffers.EMPTY_BUFFER;
    216         }
    217         encoder = null;
    218         return result;
    219     }
    220
    221     public void beforeAdd(ChannelHandlerContext ctx) throws Exception {
    222         // NOOP
    223     }
    224
    225     public void afterAdd(ChannelHandlerContext ctx) throws Exception {
    226         // NOOP
    227     }
    228
    229     public void beforeRemove(ChannelHandlerContext ctx) throws Exception {
    230         // NOOP
    231     }
    232
    233     public void afterRemove(ChannelHandlerContext ctx) throws Exception {
    234         finishEncode();
    235     }
    236 }