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 }