View Javadoc
1   /*
2    * Copyright 2014 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.compression;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.channel.ChannelHandlerContext;
20  import io.netty.handler.codec.MessageToByteEncoder;
21  
22  import java.util.zip.Adler32;
23  import java.util.zip.Checksum;
24  
25  import static io.netty.handler.codec.compression.FastLz.*;
26  
27  /**
28   * Compresses a {@link ByteBuf} using the FastLZ algorithm.
29   *
30   * See <a href="https://github.com/netty/netty/issues/2750">FastLZ format</a>.
31   */
32  public class FastLzFrameEncoder extends MessageToByteEncoder<ByteBuf> {
33      /**
34       * Compression level.
35       */
36      private final int level;
37  
38      /**
39       * Underlying checksum calculator in use.
40       */
41      private final Checksum checksum;
42  
43      /**
44       * Creates a FastLZ encoder without checksum calculator and with auto detection of compression level.
45       */
46      public FastLzFrameEncoder() {
47          this(LEVEL_AUTO, null);
48      }
49  
50      /**
51       * Creates a FastLZ encoder with specified compression level and without checksum calculator.
52       *
53       * @param level supports only these values:
54       *        0 - Encoder will choose level automatically depending on the length of the input buffer.
55       *        1 - Level 1 is the fastest compression and generally useful for short data.
56       *        2 - Level 2 is slightly slower but it gives better compression ratio.
57       */
58      public FastLzFrameEncoder(int level) {
59          this(level, null);
60      }
61  
62      /**
63       * Creates a FastLZ encoder with auto detection of compression
64       * level and calculation of checksums as specified.
65       *
66       * @param validateChecksums
67       *        If true, the checksum of each block will be calculated and this value
68       *        will be added to the header of block.
69       *        By default {@link FastLzFrameEncoder} uses {@link java.util.zip.Adler32}
70       *        for checksum calculation.
71       */
72      public FastLzFrameEncoder(boolean validateChecksums) {
73          this(LEVEL_AUTO, validateChecksums ? new Adler32() : null);
74      }
75  
76      /**
77       * Creates a FastLZ encoder with specified compression level and checksum calculator.
78       *
79       * @param level supports only these values:
80       *        0 - Encoder will choose level automatically depending on the length of the input buffer.
81       *        1 - Level 1 is the fastest compression and generally useful for short data.
82       *        2 - Level 2 is slightly slower but it gives better compression ratio.
83       * @param checksum
84       *        the {@link Checksum} instance to use to check data for integrity.
85       *        You may set {@code null} if you don't want to validate checksum of each block.
86       */
87      public FastLzFrameEncoder(int level, Checksum checksum) {
88          super(false);
89          if (level != LEVEL_AUTO && level != LEVEL_1 && level != LEVEL_2) {
90              throw new IllegalArgumentException(String.format(
91                      "level: %d (expected: %d or %d or %d)", level, LEVEL_AUTO, LEVEL_1, LEVEL_2));
92          }
93          this.level = level;
94          this.checksum = checksum;
95      }
96  
97      @Override
98      protected void encode(ChannelHandlerContext ctx, ByteBuf in, ByteBuf out) throws Exception {
99          final Checksum checksum = this.checksum;
100 
101         for (;;) {
102             if (!in.isReadable()) {
103                 return;
104             }
105             final int idx = in.readerIndex();
106             final int length = Math.min(in.readableBytes(), MAX_CHUNK_LENGTH);
107 
108             final int outputIdx = out.writerIndex();
109             out.setMedium(outputIdx, MAGIC_NUMBER);
110             int outputOffset = outputIdx + CHECKSUM_OFFSET + (checksum != null ? 4 : 0);
111 
112             final byte blockType;
113             final int chunkLength;
114             if (length < MIN_LENGTH_TO_COMPRESSION) {
115                 blockType = BLOCK_TYPE_NON_COMPRESSED;
116 
117                 out.ensureWritable(outputOffset + 2 + length);
118                 final byte[] output = out.array();
119                 final int outputPtr = out.arrayOffset() + outputOffset + 2;
120 
121                 if (checksum != null) {
122                     final byte[] input;
123                     final int inputPtr;
124                     if (in.hasArray()) {
125                         input = in.array();
126                         inputPtr = in.arrayOffset() + idx;
127                     } else {
128                         input = new byte[length];
129                         in.getBytes(idx, input);
130                         inputPtr = 0;
131                     }
132 
133                     checksum.reset();
134                     checksum.update(input, inputPtr, length);
135                     out.setInt(outputIdx + CHECKSUM_OFFSET, (int) checksum.getValue());
136 
137                     System.arraycopy(input, inputPtr, output, outputPtr, length);
138                 } else {
139                     in.getBytes(idx, output, outputPtr, length);
140                 }
141                 chunkLength = length;
142             } else {
143                 // try to compress
144                 final byte[] input;
145                 final int inputPtr;
146                 if (in.hasArray()) {
147                     input = in.array();
148                     inputPtr = in.arrayOffset() + idx;
149                 } else {
150                     input = new byte[length];
151                     in.getBytes(idx, input);
152                     inputPtr = 0;
153                 }
154 
155                 if (checksum != null) {
156                     checksum.reset();
157                     checksum.update(input, inputPtr, length);
158                     out.setInt(outputIdx + CHECKSUM_OFFSET, (int) checksum.getValue());
159                 }
160 
161                 final int maxOutputLength = calculateOutputBufferLength(length);
162                 out.ensureWritable(outputOffset + 4 + maxOutputLength);
163                 final byte[] output = out.array();
164                 final int outputPtr = out.arrayOffset() + outputOffset + 4;
165                 final int compressedLength = compress(input, inputPtr, length, output, outputPtr, level);
166                 if (compressedLength < length) {
167                     blockType = BLOCK_TYPE_COMPRESSED;
168                     chunkLength = compressedLength;
169 
170                     out.setShort(outputOffset, chunkLength);
171                     outputOffset += 2;
172                 } else {
173                     blockType = BLOCK_TYPE_NON_COMPRESSED;
174                     System.arraycopy(input, inputPtr, output, outputPtr - 2, length);
175                     chunkLength = length;
176                 }
177             }
178             out.setShort(outputOffset, length);
179 
180             out.setByte(outputIdx + OPTIONS_OFFSET,
181                     blockType | (checksum != null ? BLOCK_WITH_CHECKSUM : BLOCK_WITHOUT_CHECKSUM));
182             out.writerIndex(outputOffset + 2 + chunkLength);
183             in.skipBytes(length);
184         }
185     }
186 }