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() || 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         // Are we excluding because of status?
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         // Are we excluding because of mime-type?
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         // Has the Content-Encoding header already been set?
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         // Are we the thread that commits?
165         if (_state.compareAndSet(GZState.MIGHT_COMPRESS,GZState.COMMITTING))
166         {
167             // We are varying the response due to accept encoding header.
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             // Adjust headers
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         // TODO else ?
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 }