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 org.jboss.netty.buffer.ChannelBuffer;
19 import org.jboss.netty.buffer.ChannelBuffers;
20 import org.jboss.netty.channel.ChannelHandlerContext;
21 import org.jboss.netty.channel.ChannelStateEvent;
22 import org.jboss.netty.channel.Channels;
23 import org.jboss.netty.channel.LifeCycleAwareChannelHandler;
24 import org.jboss.netty.channel.MessageEvent;
25 import org.jboss.netty.channel.SimpleChannelHandler;
26 import org.jboss.netty.handler.codec.embedder.EncoderEmbedder;
27
28 import java.util.Queue;
29 import java.util.concurrent.ConcurrentLinkedQueue;
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 private volatile boolean offerred;
58
59 /**
60 * Creates a new instance.
61 */
62 protected HttpContentEncoder() {
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.headers().get(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.headers().get(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.headers().set(
116 HttpHeaders.Names.CONTENT_ENCODING,
117 getTargetContentEncoding(acceptEncoding));
118
119 if (m.isChunked()) {
120 m.headers().remove(HttpHeaders.Names.CONTENT_LENGTH);
121 } else {
122 ChannelBuffer content = m.getContent();
123 // Encode the content.
124 content = ChannelBuffers.wrappedBuffer(
125 encode(content), finishEncode());
126
127 // Replace the content.
128 m.setContent(content);
129 if (m.headers().contains(HttpHeaders.Names.CONTENT_LENGTH)) {
130 m.headers().set(
131 HttpHeaders.Names.CONTENT_LENGTH,
132 Integer.toString(content.readableBytes()));
133 }
134 }
135 }
136
137 // Because HttpMessage is a mutable object, we can simply forward the write request.
138 ctx.sendDownstream(e);
139 }
140 } else if (msg instanceof HttpChunk) {
141 HttpChunk c = (HttpChunk) msg;
142 ChannelBuffer content = c.getContent();
143
144 // Encode the chunk if necessary.
145 if (encoder != null) {
146 if (!c.isLast()) {
147 content = encode(content);
148 if (content.readable()) {
149 c.setContent(content);
150 ctx.sendDownstream(e);
151 }
152 } else {
153 ChannelBuffer lastProduct = finishEncode();
154
155 // Generate an additional chunk if the decoder produced
156 // the last product on closure,
157 if (lastProduct.readable()) {
158 Channels.write(
159 ctx, Channels.succeededFuture(e.getChannel()),
160 new DefaultHttpChunk(lastProduct), e.getRemoteAddress());
161 }
162
163 // Emit the last chunk.
164 ctx.sendDownstream(e);
165 }
166 } else {
167 ctx.sendDownstream(e);
168 }
169 } else {
170 ctx.sendDownstream(e);
171 }
172 }
173
174 @Override
175 public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
176 // Clean-up the previous encoder if not cleaned up correctly.
177 finishEncode();
178
179 super.channelClosed(ctx, e);
180 }
181
182 /**
183 * Returns a new {@link EncoderEmbedder} that encodes the HTTP message
184 * content.
185 *
186 * @param acceptEncoding
187 * the value of the {@code "Accept-Encoding"} header
188 *
189 * @return a new {@link EncoderEmbedder} if there is a supported encoding
190 * in {@code acceptEncoding}. {@code null} otherwise.
191 */
192 protected abstract EncoderEmbedder<ChannelBuffer> newContentEncoder(
193 HttpMessage msg, String acceptEncoding) throws Exception;
194
195 /**
196 * Returns the expected content encoding of the encoded content.
197 *
198 * @param acceptEncoding the value of the {@code "Accept-Encoding"} header
199 * @return the expected content encoding of the new content
200 */
201 protected abstract String getTargetContentEncoding(String acceptEncoding) throws Exception;
202
203 private ChannelBuffer encode(ChannelBuffer buf) {
204 offerred = true;
205 encoder.offer(buf);
206 return ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()]));
207 }
208
209 private ChannelBuffer finishEncode() {
210 if (encoder == null) {
211 offerred = false;
212 return ChannelBuffers.EMPTY_BUFFER;
213 }
214
215 ChannelBuffer result;
216 if (!offerred) {
217 // No data was offerred to the encoder since the encoder was created.
218 // We should offer at least an empty buffer so that the encoder knows its is encoding empty content.
219 offerred = false;
220 encoder.offer(ChannelBuffers.EMPTY_BUFFER);
221 }
222 if (encoder.finish()) {
223 result = ChannelBuffers.wrappedBuffer(encoder.pollAll(new ChannelBuffer[encoder.size()]));
224 } else {
225 result = ChannelBuffers.EMPTY_BUFFER;
226 }
227 encoder = null;
228 return result;
229 }
230
231 public void beforeAdd(ChannelHandlerContext ctx) throws Exception {
232 // NOOP
233 }
234
235 public void afterAdd(ChannelHandlerContext ctx) throws Exception {
236 // NOOP
237 }
238
239 public void beforeRemove(ChannelHandlerContext ctx) throws Exception {
240 // NOOP
241 }
242
243 public void afterRemove(ChannelHandlerContext ctx) throws Exception {
244 finishEncode();
245 }
246 }