1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.jetty.servlets.gzip;
20
21 import java.nio.ByteBuffer;
22 import java.nio.channels.WritePendingException;
23 import java.util.concurrent.atomic.AtomicReference;
24 import java.util.zip.CRC32;
25 import java.util.zip.Deflater;
26
27 import org.eclipse.jetty.http.HttpFields;
28 import org.eclipse.jetty.http.HttpGenerator;
29 import org.eclipse.jetty.http.HttpHeader;
30 import org.eclipse.jetty.http.MimeTypes;
31 import org.eclipse.jetty.server.HttpChannel;
32 import org.eclipse.jetty.server.HttpOutput;
33 import org.eclipse.jetty.server.Response;
34 import org.eclipse.jetty.util.BufferUtil;
35 import org.eclipse.jetty.util.Callback;
36 import org.eclipse.jetty.util.IteratingNestedCallback;
37 import org.eclipse.jetty.util.StringUtil;
38 import org.eclipse.jetty.util.log.Log;
39 import org.eclipse.jetty.util.log.Logger;
40
41 public class GzipHttpOutput extends HttpOutput
42 {
43 public static Logger LOG = Log.getLogger(GzipHttpOutput.class);
44 private final static HttpGenerator.CachedHttpField CONTENT_ENCODING_GZIP=new HttpGenerator.CachedHttpField(HttpHeader.CONTENT_ENCODING,"gzip");
45 private final static byte[] GZIP_HEADER = new byte[] { (byte)0x1f, (byte)0x8b, Deflater.DEFLATED, 0, 0, 0, 0, 0, 0, 0 };
46
47 private enum GZState { NOT_COMPRESSING, MIGHT_COMPRESS, COMMITTING, COMPRESSING, FINISHED};
48 private final AtomicReference<GZState> _state = new AtomicReference<>(GZState.NOT_COMPRESSING);
49 private final CRC32 _crc = new CRC32();
50
51 private Deflater _deflater;
52 private GzipFactory _factory;
53 private ByteBuffer _buffer;
54
55 public GzipHttpOutput(HttpChannel<?> channel)
56 {
57 super(channel);
58 }
59
60 @Override
61 public void reset()
62 {
63 _state.set(GZState.NOT_COMPRESSING);
64 super.reset();
65 }
66
67 @Override
68 protected void write(ByteBuffer content, boolean complete, Callback callback)
69 {
70 switch (_state.get())
71 {
72 case NOT_COMPRESSING:
73 super.write(content,complete,callback);
74 return;
75
76 case MIGHT_COMPRESS:
77 commit(content,complete,callback);
78 break;
79
80 case COMMITTING:
81 throw new WritePendingException();
82
83 case COMPRESSING:
84 gzip(content,complete,callback);
85 break;
86
87 case FINISHED:
88 throw new IllegalStateException();
89 }
90 }
91
92 private void superWrite(ByteBuffer content, boolean complete, Callback callback)
93 {
94 super.write(content,complete,callback);
95 }
96
97 private void addTrailer()
98 {
99 int i=_buffer.limit();
100 _buffer.limit(i+8);
101
102 int v=(int)_crc.getValue();
103 _buffer.put(i++,(byte)(v & 0xFF));
104 _buffer.put(i++,(byte)((v>>>8) & 0xFF));
105 _buffer.put(i++,(byte)((v>>>16) & 0xFF));
106 _buffer.put(i++,(byte)((v>>>24) & 0xFF));
107
108 v=_deflater.getTotalIn();
109 _buffer.put(i++,(byte)(v & 0xFF));
110 _buffer.put(i++,(byte)((v>>>8) & 0xFF));
111 _buffer.put(i++,(byte)((v>>>16) & 0xFF));
112 _buffer.put(i++,(byte)((v>>>24) & 0xFF));
113 }
114
115
116 private void gzip(ByteBuffer content, boolean complete, final Callback callback)
117 {
118 if (content.hasRemaining())
119 {
120 if (content.hasArray())
121 new GzipArrayCB(content,complete,callback).iterate();
122 else
123 new GzipBufferCB(content,complete,callback).iterate();
124 }
125 else if (complete)
126 {
127 _deflater.finish();
128
129 int produced=_deflater.deflate(_buffer.array(),_buffer.arrayOffset()+_buffer.limit(),_buffer.capacity()-_buffer.limit(),Deflater.NO_FLUSH);
130 _buffer.limit(_buffer.limit()+produced);
131 addTrailer();
132 superWrite(_buffer,complete,new Callback()
133 {
134 @Override
135 public void succeeded()
136 {
137 getHttpChannel().getByteBufferPool().release(_buffer);
138 _buffer=null;
139 callback.succeeded();
140 }
141
142 @Override
143 public void failed(Throwable x)
144 {
145 callback.failed(x);
146 }
147 });
148 }
149 }
150
151 protected void commit(ByteBuffer content, boolean complete, Callback callback)
152 {
153
154 Response response=getHttpChannel().getResponse();
155 int sc = response.getStatus();
156 if (sc>0 && (sc<200 || sc==204 || sc==205 || sc>=300))
157 {
158 LOG.debug("{} exclude by status {}",this,sc);
159 noCompression();
160 super.write(content,complete,callback);
161 return;
162 }
163
164
165 String ct = getHttpChannel().getResponse().getContentType();
166 if (ct!=null)
167 {
168 ct=MimeTypes.getContentTypeWithoutCharset(ct);
169 if (_factory.isExcludedMimeType(StringUtil.asciiToLowerCase(ct)))
170 {
171 LOG.debug("{} exclude by mimeType {}",this,ct);
172 noCompression();
173 super.write(content,complete,callback);
174 return;
175 }
176 }
177
178
179 if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.COMMITTING))
180 {
181
182 HttpFields fields = response.getHttpFields();
183 fields.add(_factory.getVaryField());
184
185 long content_length = response.getContentLength();
186 if (content_length<0 && complete)
187 content_length=content.remaining();
188
189 _deflater = _factory.getDeflater(getHttpChannel().getRequest(),content_length);
190
191 if (_deflater==null)
192 {
193 LOG.debug("{} exclude no deflater",this);
194 _state.set(GZState.NOT_COMPRESSING);
195 super.write(content,complete,callback);
196 return;
197 }
198
199 fields.put(CONTENT_ENCODING_GZIP);
200 _crc.reset();
201 _buffer=getHttpChannel().getByteBufferPool().acquire(_factory.getBufferSize(),false);
202 BufferUtil.fill(_buffer,GZIP_HEADER,0,GZIP_HEADER.length);
203
204
205 response.setContentLength(-1);
206 String etag=fields.get(HttpHeader.ETAG);
207 if (etag!=null)
208 fields.put(HttpHeader.ETAG,etag.substring(0,etag.length()-1)+"--gzip\"");
209
210 LOG.debug("{} compressing {}",this,_deflater);
211 _state.set(GZState.COMPRESSING);
212
213 gzip(content,complete,callback);
214 }
215 }
216
217 public void noCompression()
218 {
219 while (true)
220 {
221 switch (_state.get())
222 {
223 case NOT_COMPRESSING:
224 return;
225
226 case MIGHT_COMPRESS:
227 if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.NOT_COMPRESSING))
228 return;
229 break;
230
231 default:
232 throw new IllegalStateException(_state.get().toString());
233 }
234 }
235 }
236
237 public void noCompressionIfPossible()
238 {
239 while (true)
240 {
241 switch (_state.get())
242 {
243 case COMPRESSING:
244 case NOT_COMPRESSING:
245 return;
246
247 case MIGHT_COMPRESS:
248 if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.NOT_COMPRESSING))
249 return;
250 break;
251
252 default:
253 throw new IllegalStateException(_state.get().toString());
254 }
255 }
256 }
257
258
259 public void mightCompress(GzipFactory factory)
260 {
261 while (true)
262 {
263 switch (_state.get())
264 {
265 case NOT_COMPRESSING:
266 _factory=factory;
267 if (_state.compareAndSet(GZState.NOT_COMPRESSING,GZState.MIGHT_COMPRESS))
268 {
269 LOG.debug("{} might compress",this);
270 return;
271 }
272 _factory=null;
273 break;
274
275 default:
276 throw new IllegalStateException(_state.get().toString());
277 }
278 }
279 }
280
281 private class GzipArrayCB extends IteratingNestedCallback
282 {
283 private final boolean _complete;
284 public GzipArrayCB(ByteBuffer content, boolean complete, Callback callback)
285 {
286 super(callback);
287 _complete=complete;
288
289 byte[] array=content.array();
290 int off=content.arrayOffset()+content.position();
291 int len=content.remaining();
292 _crc.update(array,off,len);
293 _deflater.setInput(array,off,len);
294 if (complete)
295 _deflater.finish();
296 content.position(content.limit());
297 }
298
299 @Override
300 protected Action process() throws Exception
301 {
302 if (_deflater.needsInput())
303 {
304 if (_deflater.finished())
305 {
306 _factory.recycle(_deflater);
307 _deflater=null;
308 getHttpChannel().getByteBufferPool().release(_buffer);
309 _buffer=null;
310 return Action.SUCCEEDED;
311 }
312
313 if (!_complete)
314 return Action.SUCCEEDED;
315 }
316
317 BufferUtil.compact(_buffer);
318 int off=_buffer.arrayOffset()+_buffer.limit();
319 int len=_buffer.capacity()-_buffer.limit()- (_complete?8:0);
320 int produced=_deflater.deflate(_buffer.array(),off,len,Deflater.NO_FLUSH);
321 _buffer.limit(_buffer.limit()+produced);
322 boolean complete=_deflater.finished();
323 if (complete)
324 addTrailer();
325
326 superWrite(_buffer,complete,this);
327 return Action.SCHEDULED;
328 }
329 }
330
331 private class GzipBufferCB extends IteratingNestedCallback
332 {
333 private final ByteBuffer _input;
334 private final ByteBuffer _content;
335 private final boolean _complete;
336 public GzipBufferCB(ByteBuffer content, boolean complete, Callback callback)
337 {
338 super(callback);
339 _input=getHttpChannel().getByteBufferPool().acquire(Math.min(_factory.getBufferSize(),content.remaining()),false);
340 _content=content;
341 _complete=complete;
342 }
343
344 @Override
345 protected Action process() throws Exception
346 {
347 if (_deflater.needsInput())
348 {
349 if (BufferUtil.isEmpty(_content))
350 {
351 if (_deflater.finished())
352 {
353 _factory.recycle(_deflater);
354 _deflater=null;
355 getHttpChannel().getByteBufferPool().release(_buffer);
356 _buffer=null;
357 return Action.SUCCEEDED;
358 }
359
360 if (!_complete)
361 return Action.SUCCEEDED;
362 }
363 else
364 {
365 BufferUtil.clearToFill(_input);
366 BufferUtil.put(_content,_input);
367 BufferUtil.flipToFlush(_input,0);
368
369 byte[] array=_input.array();
370 int off=_input.arrayOffset()+_input.position();
371 int len=_input.remaining();
372
373 _crc.update(array,off,len);
374 _deflater.setInput(array,off,len);
375 if (_complete && BufferUtil.isEmpty(_content))
376 _deflater.finish();
377 }
378 }
379
380 BufferUtil.compact(_buffer);
381 int off=_buffer.arrayOffset()+_buffer.limit();
382 int len=_buffer.capacity()-_buffer.limit() - (_complete?8:0);
383 int produced=_deflater.deflate(_buffer.array(),off,len,Deflater.NO_FLUSH);
384
385 _buffer.limit(_buffer.limit()+produced);
386 boolean complete=_deflater.finished();
387
388 if (complete)
389 addTrailer();
390
391 superWrite(_buffer,complete,this);
392 return Action.SCHEDULED;
393 }
394
395 }
396 }