View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
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         // Are we excluding because of status?
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         // Are we excluding because of mime-type?
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         // Has the Content-Encoding header already been set?
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         // Are we the thread that commits?
170         if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.COMMITTING))
171         {
172             // We are varying the response due to accept encoding header.
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             // Adjust headers
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 }