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.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         // Are we excluding because of status?
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         // Are we excluding because of mime-type?
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         // Are we the thread that commits?
179         if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.COMMITTING))
180         {
181             // We are varying the response due to accept encoding header.
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             // Adjust headers
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 }