View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http;
17  
18  import io.netty.channel.ChannelHandlerContext;
19  import io.netty.channel.embedded.EmbeddedChannel;
20  import io.netty.handler.codec.compression.ZlibCodecFactory;
21  import io.netty.handler.codec.compression.ZlibWrapper;
22  
23  /**
24   * Compresses an {@link HttpMessage} and an {@link HttpContent} in {@code gzip} or
25   * {@code deflate} encoding while respecting the {@code "Accept-Encoding"} header.
26   * If there is no matching encoding, no compression is done.  For more
27   * information on how this handler modifies the message, please refer to
28   * {@link HttpContentEncoder}.
29   */
30  public class HttpContentCompressor extends HttpContentEncoder {
31  
32      private final int compressionLevel;
33      private final int windowBits;
34      private final int memLevel;
35      private final int contentSizeThreshold;
36      private ChannelHandlerContext ctx;
37  
38      /**
39       * Creates a new handler with the default compression level (<tt>6</tt>),
40       * default window size (<tt>15</tt>) and default memory level (<tt>8</tt>).
41       */
42      public HttpContentCompressor() {
43          this(6);
44      }
45  
46      /**
47       * Creates a new handler with the specified compression level, default
48       * window size (<tt>15</tt>) and default memory level (<tt>8</tt>).
49       *
50       * @param compressionLevel
51       *        {@code 1} yields the fastest compression and {@code 9} yields the
52       *        best compression.  {@code 0} means no compression.  The default
53       *        compression level is {@code 6}.
54       */
55      public HttpContentCompressor(int compressionLevel) {
56          this(compressionLevel, 15, 8, 0);
57      }
58  
59      /**
60       * Creates a new handler with the specified compression level, window size,
61       * and memory level..
62       *
63       * @param compressionLevel
64       *        {@code 1} yields the fastest compression and {@code 9} yields the
65       *        best compression.  {@code 0} means no compression.  The default
66       *        compression level is {@code 6}.
67       * @param windowBits
68       *        The base two logarithm of the size of the history buffer.  The
69       *        value should be in the range {@code 9} to {@code 15} inclusive.
70       *        Larger values result in better compression at the expense of
71       *        memory usage.  The default value is {@code 15}.
72       * @param memLevel
73       *        How much memory should be allocated for the internal compression
74       *        state.  {@code 1} uses minimum memory and {@code 9} uses maximum
75       *        memory.  Larger values result in better and faster compression
76       *        at the expense of memory usage.  The default value is {@code 8}
77       */
78      public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel) {
79          this(compressionLevel, windowBits, memLevel, 0);
80      }
81  
82      /**
83       * Creates a new handler with the specified compression level, window size,
84       * and memory level..
85       *
86       * @param compressionLevel
87       *        {@code 1} yields the fastest compression and {@code 9} yields the
88       *        best compression.  {@code 0} means no compression.  The default
89       *        compression level is {@code 6}.
90       * @param windowBits
91       *        The base two logarithm of the size of the history buffer.  The
92       *        value should be in the range {@code 9} to {@code 15} inclusive.
93       *        Larger values result in better compression at the expense of
94       *        memory usage.  The default value is {@code 15}.
95       * @param memLevel
96       *        How much memory should be allocated for the internal compression
97       *        state.  {@code 1} uses minimum memory and {@code 9} uses maximum
98       *        memory.  Larger values result in better and faster compression
99       *        at the expense of memory usage.  The default value is {@code 8}
100      * @param contentSizeThreshold
101      *        The response body is compressed when the size of the response
102      *        body exceeds the threshold. The value should be a non negative
103      *        number. {@code 0} will enable compression for all responses.
104      */
105     public HttpContentCompressor(int compressionLevel, int windowBits, int memLevel, int contentSizeThreshold) {
106         if (compressionLevel < 0 || compressionLevel > 9) {
107             throw new IllegalArgumentException(
108                     "compressionLevel: " + compressionLevel +
109                     " (expected: 0-9)");
110         }
111         if (windowBits < 9 || windowBits > 15) {
112             throw new IllegalArgumentException(
113                     "windowBits: " + windowBits + " (expected: 9-15)");
114         }
115         if (memLevel < 1 || memLevel > 9) {
116             throw new IllegalArgumentException(
117                     "memLevel: " + memLevel + " (expected: 1-9)");
118         }
119         if (contentSizeThreshold < 0) {
120             throw new IllegalArgumentException(
121                     "contentSizeThreshold: " + contentSizeThreshold + " (expected: non negative number)");
122         }
123         this.compressionLevel = compressionLevel;
124         this.windowBits = windowBits;
125         this.memLevel = memLevel;
126         this.contentSizeThreshold = contentSizeThreshold;
127     }
128 
129     @Override
130     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
131         this.ctx = ctx;
132     }
133 
134     @Override
135     protected Result beginEncode(HttpResponse headers, String acceptEncoding) throws Exception {
136         if (this.contentSizeThreshold > 0) {
137             if (headers instanceof HttpContent &&
138                     ((HttpContent) headers).content().readableBytes() < contentSizeThreshold) {
139                 return null;
140             }
141         }
142 
143         String contentEncoding = headers.headers().get(HttpHeaderNames.CONTENT_ENCODING);
144         if (contentEncoding != null) {
145             // Content-Encoding was set, either as something specific or as the IDENTITY encoding
146             // Therefore, we should NOT encode here
147             return null;
148         }
149 
150         ZlibWrapper wrapper = determineWrapper(acceptEncoding);
151         if (wrapper == null) {
152             return null;
153         }
154 
155         String targetContentEncoding;
156         switch (wrapper) {
157         case GZIP:
158             targetContentEncoding = "gzip";
159             break;
160         case ZLIB:
161             targetContentEncoding = "deflate";
162             break;
163         default:
164             throw new Error();
165         }
166 
167         return new Result(
168                 targetContentEncoding,
169                 new EmbeddedChannel(ctx.channel().id(), ctx.channel().metadata().hasDisconnect(),
170                         ctx.channel().config(), ZlibCodecFactory.newZlibEncoder(
171                         wrapper, compressionLevel, windowBits, memLevel)));
172     }
173 
174     @SuppressWarnings("FloatingPointEquality")
175     protected ZlibWrapper determineWrapper(String acceptEncoding) {
176         float starQ = -1.0f;
177         float gzipQ = -1.0f;
178         float deflateQ = -1.0f;
179         for (String encoding : acceptEncoding.split(",")) {
180             float q = 1.0f;
181             int equalsPos = encoding.indexOf('=');
182             if (equalsPos != -1) {
183                 try {
184                     q = Float.parseFloat(encoding.substring(equalsPos + 1));
185                 } catch (NumberFormatException e) {
186                     // Ignore encoding
187                     q = 0.0f;
188                 }
189             }
190             if (encoding.contains("*")) {
191                 starQ = q;
192             } else if (encoding.contains("gzip") && q > gzipQ) {
193                 gzipQ = q;
194             } else if (encoding.contains("deflate") && q > deflateQ) {
195                 deflateQ = q;
196             }
197         }
198         if (gzipQ > 0.0f || deflateQ > 0.0f) {
199             if (gzipQ >= deflateQ) {
200                 return ZlibWrapper.GZIP;
201             } else {
202                 return ZlibWrapper.ZLIB;
203             }
204         }
205         if (starQ > 0.0f) {
206             if (gzipQ == -1.0f) {
207                 return ZlibWrapper.GZIP;
208             }
209             if (deflateQ == -1.0f) {
210                 return ZlibWrapper.ZLIB;
211             }
212         }
213         return null;
214     }
215 }