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    *   https://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.compression;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.ChannelFuture;
20  import io.netty.channel.ChannelFutureListener;
21  import io.netty.channel.ChannelHandlerContext;
22  import io.netty.channel.ChannelPromise;
23  import io.netty.util.concurrent.EventExecutor;
24  import io.netty.util.concurrent.PromiseNotifier;
25  import io.netty.util.internal.ObjectUtil;
26  import io.netty.util.internal.PlatformDependent;
27  import io.netty.util.internal.SuppressJava6Requirement;
28  
29  import java.util.concurrent.TimeUnit;
30  import java.util.zip.CRC32;
31  import java.util.zip.Deflater;
32  
33  /**
34   * Compresses a {@link ByteBuf} using the deflate algorithm.
35   */
36  public class JdkZlibEncoder extends ZlibEncoder {
37  
38      private final ZlibWrapper wrapper;
39      private final Deflater deflater;
40      private volatile boolean finished;
41      private volatile ChannelHandlerContext ctx;
42  
43      /*
44       * GZIP support
45       */
46      private final CRC32 crc = new CRC32();
47      private static final byte[] gzipHeader = {0x1f, (byte) 0x8b, Deflater.DEFLATED, 0, 0, 0, 0, 0, 0, 0};
48      private boolean writeHeader = true;
49      private static final int THREAD_POOL_DELAY_SECONDS = 10;
50  
51      /**
52       * Creates a new zlib encoder with the default compression level ({@code 6})
53       * and the default wrapper ({@link ZlibWrapper#ZLIB}).
54       *
55       * @throws CompressionException if failed to initialize zlib
56       */
57      public JdkZlibEncoder() {
58          this(6);
59      }
60  
61      /**
62       * Creates a new zlib encoder with the specified {@code compressionLevel}
63       * and the default wrapper ({@link ZlibWrapper#ZLIB}).
64       *
65       * @param compressionLevel
66       *        {@code 1} yields the fastest compression and {@code 9} yields the
67       *        best compression.  {@code 0} means no compression.  The default
68       *        compression level is {@code 6}.
69       *
70       * @throws CompressionException if failed to initialize zlib
71       */
72      public JdkZlibEncoder(int compressionLevel) {
73          this(ZlibWrapper.ZLIB, compressionLevel);
74      }
75  
76      /**
77       * Creates a new zlib encoder with the default compression level ({@code 6})
78       * and the specified wrapper.
79       *
80       * @throws CompressionException if failed to initialize zlib
81       */
82      public JdkZlibEncoder(ZlibWrapper wrapper) {
83          this(wrapper, 6);
84      }
85  
86      /**
87       * Creates a new zlib encoder with the specified {@code compressionLevel}
88       * and the specified wrapper.
89       *
90       * @param compressionLevel
91       *        {@code 1} yields the fastest compression and {@code 9} yields the
92       *        best compression.  {@code 0} means no compression.  The default
93       *        compression level is {@code 6}.
94       *
95       * @throws CompressionException if failed to initialize zlib
96       */
97      public JdkZlibEncoder(ZlibWrapper wrapper, int compressionLevel) {
98          ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
99          ObjectUtil.checkNotNull(wrapper, "wrapper");
100 
101         if (wrapper == ZlibWrapper.ZLIB_OR_NONE) {
102             throw new IllegalArgumentException(
103                     "wrapper '" + ZlibWrapper.ZLIB_OR_NONE + "' is not " +
104                     "allowed for compression.");
105         }
106 
107         this.wrapper = wrapper;
108         deflater = new Deflater(compressionLevel, wrapper != ZlibWrapper.ZLIB);
109     }
110 
111     /**
112      * Creates a new zlib encoder with the default compression level ({@code 6})
113      * and the specified preset dictionary.  The wrapper is always
114      * {@link ZlibWrapper#ZLIB} because it is the only format that supports
115      * the preset dictionary.
116      *
117      * @param dictionary  the preset dictionary
118      *
119      * @throws CompressionException if failed to initialize zlib
120      */
121     public JdkZlibEncoder(byte[] dictionary) {
122         this(6, dictionary);
123     }
124 
125     /**
126      * Creates a new zlib encoder with the specified {@code compressionLevel}
127      * and the specified preset dictionary.  The wrapper is always
128      * {@link ZlibWrapper#ZLIB} because it is the only format that supports
129      * the preset dictionary.
130      *
131      * @param compressionLevel
132      *        {@code 1} yields the fastest compression and {@code 9} yields the
133      *        best compression.  {@code 0} means no compression.  The default
134      *        compression level is {@code 6}.
135      * @param dictionary  the preset dictionary
136      *
137      * @throws CompressionException if failed to initialize zlib
138      */
139     public JdkZlibEncoder(int compressionLevel, byte[] dictionary) {
140         ObjectUtil.checkInRange(compressionLevel, 0, 9, "compressionLevel");
141         ObjectUtil.checkNotNull(dictionary, "dictionary");
142 
143         wrapper = ZlibWrapper.ZLIB;
144         deflater = new Deflater(compressionLevel);
145         deflater.setDictionary(dictionary);
146     }
147 
148     @Override
149     public ChannelFuture close() {
150         return close(ctx().newPromise());
151     }
152 
153     @Override
154     public ChannelFuture close(final ChannelPromise promise) {
155         ChannelHandlerContext ctx = ctx();
156         EventExecutor executor = ctx.executor();
157         if (executor.inEventLoop()) {
158             return finishEncode(ctx, promise);
159         } else {
160             final ChannelPromise p = ctx.newPromise();
161             executor.execute(new Runnable() {
162                 @Override
163                 public void run() {
164                     ChannelFuture f = finishEncode(ctx(), p);
165                     PromiseNotifier.cascade(f, promise);
166                 }
167             });
168             return p;
169         }
170     }
171 
172     private ChannelHandlerContext ctx() {
173         ChannelHandlerContext ctx = this.ctx;
174         if (ctx == null) {
175             throw new IllegalStateException("not added to a pipeline");
176         }
177         return ctx;
178     }
179 
180     @Override
181     public boolean isClosed() {
182         return finished;
183     }
184 
185     @Override
186     protected void encode(ChannelHandlerContext ctx, ByteBuf uncompressed, ByteBuf out) throws Exception {
187         if (finished) {
188             out.writeBytes(uncompressed);
189             return;
190         }
191 
192         int len = uncompressed.readableBytes();
193         if (len == 0) {
194             return;
195         }
196 
197         int offset;
198         byte[] inAry;
199         ByteBuf heapBuf = null;
200         try {
201             if (uncompressed.hasArray()) {
202                 // if it is backed by an array we not need to do a copy at all
203                 inAry = uncompressed.array();
204                 offset = uncompressed.arrayOffset() + uncompressed.readerIndex();
205                 // skip all bytes as we will consume all of them
206                 uncompressed.skipBytes(len);
207             } else {
208                 heapBuf = ctx.alloc().heapBuffer(len, len);
209                 uncompressed.readBytes(heapBuf, len);
210                 inAry = heapBuf.array();
211                 offset = heapBuf.arrayOffset() + heapBuf.readerIndex();
212             }
213 
214             if (writeHeader) {
215                 writeHeader = false;
216                 if (wrapper == ZlibWrapper.GZIP) {
217                     out.writeBytes(gzipHeader);
218                 }
219             }
220 
221             if (wrapper == ZlibWrapper.GZIP) {
222                 crc.update(inAry, offset, len);
223             }
224 
225             deflater.setInput(inAry, offset, len);
226             for (;;) {
227                 deflate(out);
228                 if (deflater.needsInput()) {
229                     // Consumed everything
230                     break;
231                 } else {
232                     if (!out.isWritable()) {
233                         // We did not consume everything but the buffer is not writable anymore. Increase the capacity
234                         // to make more room.
235                         out.ensureWritable(out.writerIndex());
236                     }
237                 }
238             }
239         } finally {
240             if (heapBuf != null) {
241                 heapBuf.release();
242             }
243         }
244     }
245 
246     @Override
247     protected final ByteBuf allocateBuffer(ChannelHandlerContext ctx, ByteBuf msg,
248                                            boolean preferDirect) throws Exception {
249         int sizeEstimate = (int) Math.ceil(msg.readableBytes() * 1.001) + 12;
250         if (writeHeader) {
251             switch (wrapper) {
252                 case GZIP:
253                     sizeEstimate += gzipHeader.length;
254                     break;
255                 case ZLIB:
256                     sizeEstimate += 2; // first two magic bytes
257                     break;
258                 default:
259                     // no op
260             }
261         }
262         return ctx.alloc().heapBuffer(sizeEstimate);
263     }
264 
265     @Override
266     public void close(final ChannelHandlerContext ctx, final ChannelPromise promise) throws Exception {
267         ChannelFuture f = finishEncode(ctx, ctx.newPromise());
268         f.addListener(new ChannelFutureListener() {
269             @Override
270             public void operationComplete(ChannelFuture f) throws Exception {
271                 ctx.close(promise);
272             }
273         });
274 
275         if (!f.isDone()) {
276             // Ensure the channel is closed even if the write operation completes in time.
277             ctx.executor().schedule(new Runnable() {
278                 @Override
279                 public void run() {
280                     ctx.close(promise);
281                 }
282             }, THREAD_POOL_DELAY_SECONDS, TimeUnit.SECONDS);
283         }
284     }
285 
286     private ChannelFuture finishEncode(final ChannelHandlerContext ctx, ChannelPromise promise) {
287         if (finished) {
288             promise.setSuccess();
289             return promise;
290         }
291 
292         finished = true;
293         ByteBuf footer = ctx.alloc().heapBuffer();
294         if (writeHeader && wrapper == ZlibWrapper.GZIP) {
295             // Write the GZIP header first if not written yet. (i.e. user wrote nothing.)
296             writeHeader = false;
297             footer.writeBytes(gzipHeader);
298         }
299 
300         deflater.finish();
301 
302         while (!deflater.finished()) {
303             deflate(footer);
304             if (!footer.isWritable()) {
305                 // no more space so write it to the channel and continue
306                 ctx.write(footer);
307                 footer = ctx.alloc().heapBuffer();
308             }
309         }
310         if (wrapper == ZlibWrapper.GZIP) {
311             int crcValue = (int) crc.getValue();
312             int uncBytes = deflater.getTotalIn();
313             footer.writeByte(crcValue);
314             footer.writeByte(crcValue >>> 8);
315             footer.writeByte(crcValue >>> 16);
316             footer.writeByte(crcValue >>> 24);
317             footer.writeByte(uncBytes);
318             footer.writeByte(uncBytes >>> 8);
319             footer.writeByte(uncBytes >>> 16);
320             footer.writeByte(uncBytes >>> 24);
321         }
322         deflater.end();
323         return ctx.writeAndFlush(footer, promise);
324     }
325 
326     @SuppressJava6Requirement(reason = "Usage guarded by java version check")
327     private void deflate(ByteBuf out) {
328         if (PlatformDependent.javaVersion() < 7) {
329             deflateJdk6(out);
330         }
331         int numBytes;
332         do {
333             int writerIndex = out.writerIndex();
334             numBytes = deflater.deflate(
335                     out.array(), out.arrayOffset() + writerIndex, out.writableBytes(), Deflater.SYNC_FLUSH);
336             out.writerIndex(writerIndex + numBytes);
337         } while (numBytes > 0);
338     }
339 
340     private void deflateJdk6(ByteBuf out) {
341         int numBytes;
342         do {
343             int writerIndex = out.writerIndex();
344             numBytes = deflater.deflate(
345                     out.array(), out.arrayOffset() + writerIndex, out.writableBytes());
346             out.writerIndex(writerIndex + numBytes);
347         } while (numBytes > 0);
348     }
349 
350     @Override
351     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
352         this.ctx = ctx;
353     }
354 }