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