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 io.netty.handler.codec.http;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.ChannelHandlerContext;
20  import io.netty.channel.FileRegion;
21  import io.netty.handler.codec.MessageToMessageEncoder;
22  import io.netty.util.CharsetUtil;
23  import io.netty.util.internal.PlatformDependent;
24  import io.netty.util.internal.StringUtil;
25  
26  import java.util.List;
27  
28  import static io.netty.buffer.Unpooled.*;
29  import static io.netty.handler.codec.http.HttpConstants.*;
30  
31  /**
32   * Encodes an {@link HttpMessage} or an {@link HttpContent} into
33   * a {@link ByteBuf}.
34   *
35   * <h3>Extensibility</h3>
36   *
37   * Please note that this encoder is designed to be extended to implement
38   * a protocol derived from HTTP, such as
39   * <a href="http://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
40   * <a href="http://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>.
41   * To implement the encoder of such a derived protocol, extend this class and
42   * implement all abstract methods properly.
43   */
44  public abstract class HttpObjectEncoder<H extends HttpMessage> extends MessageToMessageEncoder<Object> {
45      private static final byte[] CRLF = { CR, LF };
46      private static final byte[] ZERO_CRLF = { '0', CR, LF };
47      private static final byte[] ZERO_CRLF_CRLF = { '0', CR, LF, CR, LF };
48      private static final ByteBuf CRLF_BUF = unreleasableBuffer(directBuffer(CRLF.length).writeBytes(CRLF));
49      private static final ByteBuf ZERO_CRLF_CRLF_BUF = unreleasableBuffer(directBuffer(ZERO_CRLF_CRLF.length)
50              .writeBytes(ZERO_CRLF_CRLF));
51  
52      private static final int ST_INIT = 0;
53      private static final int ST_CONTENT_NON_CHUNK = 1;
54      private static final int ST_CONTENT_CHUNK = 2;
55  
56      @SuppressWarnings("RedundantFieldInitialization")
57      private int state = ST_INIT;
58  
59      @Override
60      protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
61          ByteBuf buf = null;
62          if (msg instanceof HttpMessage) {
63              if (state != ST_INIT) {
64                  throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg));
65              }
66  
67              @SuppressWarnings({ "unchecked", "CastConflictsWithInstanceof" })
68              H m = (H) msg;
69  
70              buf = ctx.alloc().buffer();
71              // Encode the message.
72              encodeInitialLine(buf, m);
73              encodeHeaders(m.headers(), buf);
74              buf.writeBytes(CRLF);
75              state = HttpHeaderUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK;
76          }
77  
78          // Bypass the encoder in case of an empty buffer, so that the following idiom works:
79          //
80          //     ch.write(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
81          //
82          // See https://github.com/netty/netty/issues/2983 for more information.
83  
84          if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) {
85              out.add(EMPTY_BUFFER);
86              return;
87          }
88  
89          if (msg instanceof HttpContent || msg instanceof ByteBuf || msg instanceof FileRegion) {
90  
91              if (state == ST_INIT) {
92                  throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg));
93              }
94  
95              final long contentLength = contentLength(msg);
96              if (state == ST_CONTENT_NON_CHUNK) {
97                  if (contentLength > 0) {
98                      if (buf != null && buf.writableBytes() >= contentLength && msg instanceof HttpContent) {
99                          // merge into other buffer for performance reasons
100                         buf.writeBytes(((HttpContent) msg).content());
101                         out.add(buf);
102                     } else {
103                         if (buf != null) {
104                             out.add(buf);
105                         }
106                         out.add(encodeAndRetain(msg));
107                     }
108                 } else {
109                     if (buf != null) {
110                         out.add(buf);
111                     } else {
112                         // Need to produce some output otherwise an
113                         // IllegalStateException will be thrown
114                         out.add(EMPTY_BUFFER);
115                     }
116                 }
117 
118                 if (msg instanceof LastHttpContent) {
119                     state = ST_INIT;
120                 }
121             } else if (state == ST_CONTENT_CHUNK) {
122                 if (buf != null) {
123                     out.add(buf);
124                 }
125                 encodeChunkedContent(ctx, msg, contentLength, out);
126             } else {
127                 throw new Error();
128             }
129         } else {
130             if (buf != null) {
131                 out.add(buf);
132             }
133         }
134     }
135 
136     /**
137      * Encode the {@link HttpHeaders} into a {@link ByteBuf}.
138      */
139     protected void encodeHeaders(HttpHeaders headers, ByteBuf buf) throws Exception {
140         headers.forEachEntry(new HttpHeadersEncoder(buf));
141     }
142 
143     private void encodeChunkedContent(ChannelHandlerContext ctx, Object msg, long contentLength, List<Object> out) {
144         if (contentLength > 0) {
145             byte[] length = Long.toHexString(contentLength).getBytes(CharsetUtil.US_ASCII);
146             ByteBuf buf = ctx.alloc().buffer(length.length + 2);
147             buf.writeBytes(length);
148             buf.writeBytes(CRLF);
149             out.add(buf);
150             out.add(encodeAndRetain(msg));
151             out.add(CRLF_BUF.duplicate());
152         }
153 
154         if (msg instanceof LastHttpContent) {
155             HttpHeaders headers = ((LastHttpContent) msg).trailingHeaders();
156             if (headers.isEmpty()) {
157                 out.add(ZERO_CRLF_CRLF_BUF.duplicate());
158             } else {
159                 ByteBuf buf = ctx.alloc().buffer();
160                 buf.writeBytes(ZERO_CRLF);
161                 try {
162                     encodeHeaders(headers, buf);
163                 } catch (Exception ex) {
164                     buf.release();
165                     PlatformDependent.throwException(ex);
166                 }
167                 buf.writeBytes(CRLF);
168                 out.add(buf);
169             }
170 
171             state = ST_INIT;
172         } else {
173             if (contentLength == 0) {
174                 // Need to produce some output otherwise an
175                 // IllegalstateException will be thrown
176                 out.add(EMPTY_BUFFER);
177             }
178         }
179     }
180 
181     @Override
182     public boolean acceptOutboundMessage(Object msg) throws Exception {
183         return msg instanceof HttpObject || msg instanceof ByteBuf || msg instanceof FileRegion;
184     }
185 
186     private static Object encodeAndRetain(Object msg) {
187         if (msg instanceof ByteBuf) {
188             return ((ByteBuf) msg).retain();
189         }
190         if (msg instanceof HttpContent) {
191             return ((HttpContent) msg).content().retain();
192         }
193         if (msg instanceof FileRegion) {
194             return ((FileRegion) msg).retain();
195         }
196         throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg));
197     }
198 
199     private static long contentLength(Object msg) {
200         if (msg instanceof HttpContent) {
201             return ((HttpContent) msg).content().readableBytes();
202         }
203         if (msg instanceof ByteBuf) {
204             return ((ByteBuf) msg).readableBytes();
205         }
206         if (msg instanceof FileRegion) {
207             return ((FileRegion) msg).count();
208         }
209         throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg));
210     }
211 
212     protected abstract void encodeInitialLine(ByteBuf buf, H message) throws Exception;
213 }