View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 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.server.handler.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.GzipHttpContent;
28  import org.eclipse.jetty.http.HttpField;
29  import org.eclipse.jetty.http.HttpFields;
30  import org.eclipse.jetty.http.HttpHeader;
31  import org.eclipse.jetty.http.MimeTypes;
32  import org.eclipse.jetty.http.PreEncodedHttpField;
33  import org.eclipse.jetty.server.HttpChannel;
34  import org.eclipse.jetty.server.HttpOutput;
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 GzipHttpOutputInterceptor implements HttpOutput.Interceptor
43  {
44      public static Logger LOG = Log.getLogger(GzipHttpOutputInterceptor.class);
45      private final static byte[] GZIP_HEADER = new byte[] { (byte)0x1f, (byte)0x8b, Deflater.DEFLATED, 0, 0, 0, 0, 0, 0, 0 };
46  
47      public final static HttpField VARY_ACCEPT_ENCODING_USER_AGENT=new PreEncodedHttpField(HttpHeader.VARY,HttpHeader.ACCEPT_ENCODING+", "+HttpHeader.USER_AGENT);
48      public final static HttpField VARY_ACCEPT_ENCODING=new PreEncodedHttpField(HttpHeader.VARY,HttpHeader.ACCEPT_ENCODING.asString());
49      
50      private enum GZState {  MIGHT_COMPRESS, NOT_COMPRESSING, COMMITTING, COMPRESSING, FINISHED};
51      private final AtomicReference<GZState> _state = new AtomicReference<>(GZState.MIGHT_COMPRESS);
52      private final CRC32 _crc = new CRC32();
53  
54      private final GzipFactory _factory;
55      private final HttpOutput.Interceptor _interceptor;
56      private final HttpChannel _channel;
57      private final HttpField _vary;
58      private final int _bufferSize;
59      
60      private Deflater _deflater;
61      private ByteBuffer _buffer;
62  
63      public GzipHttpOutputInterceptor(GzipFactory factory, HttpChannel channel, HttpOutput.Interceptor next)
64      {
65          this(factory,VARY_ACCEPT_ENCODING_USER_AGENT,channel.getHttpConfiguration().getOutputBufferSize(),channel,next);
66      }
67      
68      public GzipHttpOutputInterceptor(GzipFactory factory, HttpField vary, HttpChannel channel, HttpOutput.Interceptor next)
69      {
70          this(factory,vary,channel.getHttpConfiguration().getOutputBufferSize(),channel,next);
71      }
72      
73      public GzipHttpOutputInterceptor(GzipFactory factory, HttpField vary, int bufferSize, HttpChannel channel, HttpOutput.Interceptor next)
74      {
75          _factory=factory;
76          _channel=channel;
77          _interceptor=next;
78          _vary=vary;
79          _bufferSize=bufferSize;
80      }
81  
82      public HttpOutput.Interceptor getNextInterceptor()
83      {
84          return _interceptor;
85      }
86      
87      @Override
88      public boolean isOptimizedForDirectBuffers()
89      {
90          return false; // No point as deflator is in user space.
91      }
92      
93      
94      @Override
95      public void write(ByteBuffer content, boolean complete, Callback callback)
96      {
97          switch (_state.get())
98          {
99              case MIGHT_COMPRESS:
100                 commit(content,complete,callback);
101                 break;
102                 
103             case NOT_COMPRESSING:
104                 _interceptor.write(content, complete, callback);
105                 return;
106                 
107             case COMMITTING:
108                 callback.failed(new WritePendingException());
109                 break;
110 
111             case COMPRESSING:
112                 gzip(content,complete,callback);
113                 break;
114 
115             default:
116                 callback.failed(new IllegalStateException("state="+_state.get()));
117                 break;
118         }
119     }
120 
121     private void addTrailer()
122     {
123         int i=_buffer.limit();
124         _buffer.limit(i+8);
125         
126         int v=(int)_crc.getValue();
127         _buffer.put(i++,(byte)(v & 0xFF));
128         _buffer.put(i++,(byte)((v>>>8) & 0xFF));
129         _buffer.put(i++,(byte)((v>>>16) & 0xFF));
130         _buffer.put(i++,(byte)((v>>>24) & 0xFF));
131         
132         v=_deflater.getTotalIn();
133         _buffer.put(i++,(byte)(v & 0xFF));
134         _buffer.put(i++,(byte)((v>>>8) & 0xFF));
135         _buffer.put(i++,(byte)((v>>>16) & 0xFF));
136         _buffer.put(i++,(byte)((v>>>24) & 0xFF));
137     }
138     
139     
140     private void gzip(ByteBuffer content, boolean complete, final Callback callback)
141     {
142         if (content.hasRemaining() || complete)
143             new GzipBufferCB(content,complete,callback).iterate();
144         else
145             callback.succeeded();
146     }
147 
148     protected void commit(ByteBuffer content, boolean complete, Callback callback)
149     {
150         // Are we excluding because of status?
151         int sc = _channel.getResponse().getStatus();
152         if (sc>0 && (sc<200 || sc==204 || sc==205 || sc>=300))
153         {
154             LOG.debug("{} exclude by status {}",this,sc);
155             noCompression();
156             _interceptor.write(content, complete, callback);
157             return;
158         }
159         
160         // Are we excluding because of mime-type?
161         String ct = _channel.getResponse().getContentType();
162         if (ct!=null)
163         {
164             ct=MimeTypes.getContentTypeWithoutCharset(ct);
165             if (!_factory.isMimeTypeGzipable(StringUtil.asciiToLowerCase(ct)))
166             {
167                 LOG.debug("{} exclude by mimeType {}",this,ct);
168                 noCompression();
169                 _interceptor.write(content, complete, callback);
170                 return;
171             }
172         }
173         
174         // Has the Content-Encoding header already been set?
175         String ce=_channel.getResponse().getHeader("Content-Encoding");
176         if (ce != null)
177         {
178             LOG.debug("{} exclude by content-encoding {}",this,ce);
179             noCompression();
180             _interceptor.write(content, complete, callback);
181             return;
182         }
183         
184         // Are we the thread that commits?
185         if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.COMMITTING))
186         {
187             // We are varying the response due to accept encoding header.
188             HttpFields fields = _channel.getResponse().getHttpFields();
189             fields.add(_vary);
190 
191             long content_length = _channel.getResponse().getContentLength();
192             if (content_length<0 && complete)
193                 content_length=content.remaining();
194             
195             _deflater = _factory.getDeflater(_channel.getRequest(),content_length);
196             
197             if (_deflater==null)
198             {
199                 LOG.debug("{} exclude no deflater",this);
200                 _state.set(GZState.NOT_COMPRESSING);
201                 _interceptor.write(content, complete, callback);
202                 return;
203             }
204 
205             fields.put(GzipHttpContent.CONTENT_ENCODING_GZIP);
206             _crc.reset();
207             _buffer=_channel.getByteBufferPool().acquire(_bufferSize,false);
208             BufferUtil.fill(_buffer,GZIP_HEADER,0,GZIP_HEADER.length);
209 
210             // Adjust headers
211             _channel.getResponse().setContentLength(-1);
212             String etag=fields.get(HttpHeader.ETAG);
213             if (etag!=null)
214             {
215                 int end = etag.length()-1;
216                 etag=(etag.charAt(end)=='"')?etag.substring(0,end)+GzipHttpContent.ETAG_GZIP+'"':etag+GzipHttpContent.ETAG_GZIP;
217                 fields.put(HttpHeader.ETAG,etag);
218             }
219             
220             LOG.debug("{} compressing {}",this,_deflater);
221             _state.set(GZState.COMPRESSING);
222             
223             gzip(content,complete,callback);
224         }
225         else
226             callback.failed(new WritePendingException());
227     }
228 
229     public void noCompression()
230     {
231         while (true)
232         {
233             switch (_state.get())
234             {
235                 case NOT_COMPRESSING:
236                     return;
237 
238                 case MIGHT_COMPRESS:
239                     if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.NOT_COMPRESSING))
240                         return;
241                     break;
242 
243                 default:
244                     throw new IllegalStateException(_state.get().toString());
245             }
246         }
247     }
248 
249     public void noCompressionIfPossible()
250     {
251         while (true)
252         {
253             switch (_state.get())
254             {
255                 case COMPRESSING:
256                 case NOT_COMPRESSING:
257                     return;
258 
259                 case MIGHT_COMPRESS:
260                     if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.NOT_COMPRESSING))
261                         return;
262                     break;
263 
264                 default:
265                     throw new IllegalStateException(_state.get().toString());
266             }
267         }
268     }
269     
270     public boolean mightCompress()
271     {
272         return _state.get()==GZState.MIGHT_COMPRESS;
273     }
274     
275     private class GzipBufferCB extends IteratingNestedCallback
276     {        
277         private ByteBuffer _copy;
278         private final ByteBuffer _content;
279         private final boolean _last;
280         public GzipBufferCB(ByteBuffer content, boolean complete, Callback callback)
281         {
282             super(callback);
283             _content=content;
284             _last=complete;
285         }
286 
287         @Override
288         protected Action process() throws Exception
289         {
290             if (_deflater==null)
291                 return Action.SUCCEEDED;
292                 
293             if (_deflater.needsInput())
294             {                
295                 if (BufferUtil.isEmpty(_content))
296                 {                    
297                     if (_deflater.finished())
298                     {
299                         _factory.recycle(_deflater);
300                         _deflater=null;
301                         _channel.getByteBufferPool().release(_buffer);
302                         _buffer=null;
303                         if (_copy!=null)
304                         {
305                             _channel.getByteBufferPool().release(_copy);
306                             _copy=null;
307                         }
308                         return Action.SUCCEEDED;
309                     }
310                     
311                     if (!_last)
312                     {
313                         return Action.SUCCEEDED;
314                     }
315                     
316                     _deflater.finish();
317                 }
318                 else if (_content.hasArray())
319                 {
320                     byte[] array=_content.array();
321                     int off=_content.arrayOffset()+_content.position();
322                     int len=_content.remaining();
323                     BufferUtil.clear(_content);
324                     
325                     _crc.update(array,off,len);
326                     _deflater.setInput(array,off,len);                
327                     if (_last)
328                         _deflater.finish();
329                 }
330                 else
331                 {
332                     if (_copy==null)
333                         _copy=_channel.getByteBufferPool().acquire(_bufferSize,false);
334                     BufferUtil.clearToFill(_copy);
335                     int took=BufferUtil.put(_content,_copy);
336                     BufferUtil.flipToFlush(_copy,0);
337                     if (took==0)
338                         throw new IllegalStateException();
339                    
340                     byte[] array=_copy.array();
341                     int off=_copy.arrayOffset()+_copy.position();
342                     int len=_copy.remaining();
343 
344                     _crc.update(array,off,len);
345                     _deflater.setInput(array,off,len);                
346                     if (_last && BufferUtil.isEmpty(_content))
347                         _deflater.finish();
348                 }
349             }
350 
351             BufferUtil.compact(_buffer);
352             int off=_buffer.arrayOffset()+_buffer.limit();
353             int len=_buffer.capacity()-_buffer.limit() - (_last?8:0);
354             if (len>0)
355             {
356                 int produced=_deflater.deflate(_buffer.array(),off,len,Deflater.NO_FLUSH);
357                 _buffer.limit(_buffer.limit()+produced);
358             }
359             boolean finished=_deflater.finished();
360             
361             if (finished)
362                 addTrailer();
363                 
364             _interceptor.write(_buffer,finished,this);
365             return Action.SCHEDULED;
366         }
367     }
368 }