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() || complete)
119 {
120 if (content.hasArray())
121 new GzipArrayCB(content,complete,callback).iterate();
122 else
123 new GzipBufferCB(content,complete,callback).iterate();
124 }
125 }
126
127 protected void commit(ByteBuffer content, boolean complete, Callback callback)
128 {
129
130 Response response=getHttpChannel().getResponse();
131 int sc = response.getStatus();
132 if (sc>0 && (sc<200 || sc==204 || sc==205 || sc>=300))
133 {
134 LOG.debug("{} exclude by status {}",this,sc);
135 noCompression();
136 super.write(content,complete,callback);
137 return;
138 }
139
140
141 String ct = getHttpChannel().getResponse().getContentType();
142 if (ct!=null)
143 {
144 ct=MimeTypes.getContentTypeWithoutCharset(ct);
145 if (_factory.isExcludedMimeType(StringUtil.asciiToLowerCase(ct)))
146 {
147 LOG.debug("{} exclude by mimeType {}",this,ct);
148 noCompression();
149 super.write(content,complete,callback);
150 return;
151 }
152 }
153
154
155 String ce=getHttpChannel().getResponse().getHeader("Content-Encoding");
156 if (ce != null)
157 {
158 LOG.debug("{} exclude by content-encoding {}",this,ce);
159 noCompression();
160 super.write(content,complete,callback);
161 return;
162 }
163
164
165 if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.COMMITTING))
166 {
167
168 HttpFields fields = response.getHttpFields();
169 fields.add(_factory.getVaryField());
170
171 long content_length = response.getContentLength();
172 if (content_length<0 && complete)
173 content_length=content.remaining();
174
175 _deflater = _factory.getDeflater(getHttpChannel().getRequest(),content_length);
176
177 if (_deflater==null)
178 {
179 LOG.debug("{} exclude no deflater",this);
180 _state.set(GZState.NOT_COMPRESSING);
181 super.write(content,complete,callback);
182 return;
183 }
184
185 fields.put(CONTENT_ENCODING_GZIP);
186 _crc.reset();
187 _buffer=getHttpChannel().getByteBufferPool().acquire(_factory.getBufferSize(),false);
188 BufferUtil.fill(_buffer,GZIP_HEADER,0,GZIP_HEADER.length);
189
190
191 response.setContentLength(-1);
192 String etag=fields.get(HttpHeader.ETAG);
193 if (etag!=null)
194 fields.put(HttpHeader.ETAG,etag.substring(0,etag.length()-1)+"--gzip\"");
195
196 LOG.debug("{} compressing {}",this,_deflater);
197 _state.set(GZState.COMPRESSING);
198
199 gzip(content,complete,callback);
200 }
201
202 }
203
204 public void noCompression()
205 {
206 while (true)
207 {
208 switch (_state.get())
209 {
210 case NOT_COMPRESSING:
211 return;
212
213 case MIGHT_COMPRESS:
214 if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.NOT_COMPRESSING))
215 return;
216 break;
217
218 default:
219 throw new IllegalStateException(_state.get().toString());
220 }
221 }
222 }
223
224 public void noCompressionIfPossible()
225 {
226 while (true)
227 {
228 switch (_state.get())
229 {
230 case COMPRESSING:
231 case NOT_COMPRESSING:
232 return;
233
234 case MIGHT_COMPRESS:
235 if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.NOT_COMPRESSING))
236 return;
237 break;
238
239 default:
240 throw new IllegalStateException(_state.get().toString());
241 }
242 }
243 }
244
245
246 public void mightCompress(GzipFactory factory)
247 {
248 while (true)
249 {
250 switch (_state.get())
251 {
252 case NOT_COMPRESSING:
253 _factory=factory;
254 if (_state.compareAndSet(GZState.NOT_COMPRESSING,GZState.MIGHT_COMPRESS))
255 {
256 LOG.debug("{} might compress",this);
257 return;
258 }
259 _factory=null;
260 break;
261
262 default:
263 throw new IllegalStateException(_state.get().toString());
264 }
265 }
266 }
267
268 private class GzipArrayCB extends IteratingNestedCallback
269 {
270 private final boolean _complete;
271 public GzipArrayCB(ByteBuffer content, boolean complete, Callback callback)
272 {
273 super(callback);
274 _complete=complete;
275
276 byte[] array=content.array();
277 int off=content.arrayOffset()+content.position();
278 int len=content.remaining();
279 _crc.update(array,off,len);
280 _deflater.setInput(array,off,len);
281 if (complete)
282 _deflater.finish();
283 content.position(content.limit());
284 }
285
286 @Override
287 protected Action process() throws Exception
288 {
289 if (_deflater.needsInput())
290 {
291 if (_deflater.finished())
292 {
293 _factory.recycle(_deflater);
294 _deflater=null;
295 getHttpChannel().getByteBufferPool().release(_buffer);
296 _buffer=null;
297 return Action.SUCCEEDED;
298 }
299
300 if (!_complete)
301 return Action.SUCCEEDED;
302 }
303
304 BufferUtil.compact(_buffer);
305 int off=_buffer.arrayOffset()+_buffer.limit();
306 int len=_buffer.capacity()-_buffer.limit()- (_complete?8:0);
307 int produced=_deflater.deflate(_buffer.array(),off,len,Deflater.NO_FLUSH);
308 _buffer.limit(_buffer.limit()+produced);
309 boolean complete=_deflater.finished();
310 if (complete)
311 addTrailer();
312
313 superWrite(_buffer,complete,this);
314 return Action.SCHEDULED;
315 }
316 }
317
318 private class GzipBufferCB extends IteratingNestedCallback
319 {
320 private final ByteBuffer _input;
321 private final ByteBuffer _content;
322 private final boolean _complete;
323 public GzipBufferCB(ByteBuffer content, boolean complete, Callback callback)
324 {
325 super(callback);
326 _input=getHttpChannel().getByteBufferPool().acquire(Math.min(_factory.getBufferSize(),content.remaining()),false);
327 _content=content;
328 _complete=complete;
329 }
330
331 @Override
332 protected Action process() throws Exception
333 {
334 if (_deflater.needsInput())
335 {
336 if (BufferUtil.isEmpty(_content))
337 {
338 if (_deflater.finished())
339 {
340 _factory.recycle(_deflater);
341 _deflater=null;
342 getHttpChannel().getByteBufferPool().release(_buffer);
343 _buffer=null;
344 return Action.SUCCEEDED;
345 }
346
347 if (!_complete)
348 return Action.SUCCEEDED;
349 }
350 else
351 {
352 BufferUtil.clearToFill(_input);
353 BufferUtil.put(_content,_input);
354 BufferUtil.flipToFlush(_input,0);
355
356 byte[] array=_input.array();
357 int off=_input.arrayOffset()+_input.position();
358 int len=_input.remaining();
359
360 _crc.update(array,off,len);
361 _deflater.setInput(array,off,len);
362 if (_complete && BufferUtil.isEmpty(_content))
363 _deflater.finish();
364 }
365 }
366
367 BufferUtil.compact(_buffer);
368 int off=_buffer.arrayOffset()+_buffer.limit();
369 int len=_buffer.capacity()-_buffer.limit() - (_complete?8:0);
370 int produced=_deflater.deflate(_buffer.array(),off,len,Deflater.NO_FLUSH);
371
372 _buffer.limit(_buffer.limit()+produced);
373 boolean complete=_deflater.finished();
374
375 if (complete)
376 addTrailer();
377
378 superWrite(_buffer,complete,this);
379 return Action.SCHEDULED;
380 }
381
382 }
383 }