1// Copyright 2015 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package com.google.archivepatcher.shared;
16
17import java.io.IOException;
18import java.io.InputStream;
19import java.io.OutputStream;
20import java.util.zip.Deflater;
21import java.util.zip.DeflaterOutputStream;
22
23/**
24 * Implementation of {@link Compressor} based on Java's built-in {@link Deflater}. Uses default
25 * compression, the default strategy, and no-wrap by default along with a 32k read buffer and a 32k
26 * write buffer. Buffers are allocated on-demand and discarded after use.
27 */
28public class DeflateCompressor implements Compressor {
29
30  /**
31   * The compression level to use. Defaults to {@link Deflater#DEFAULT_COMPRESSION}.
32   */
33  private int compressionLevel = Deflater.DEFAULT_COMPRESSION;
34
35  /**
36   * The compression strategy to use. Defaults to {@link Deflater#DEFAULT_STRATEGY}.
37   */
38  private int strategy = Deflater.DEFAULT_STRATEGY;
39
40  /**
41   * Whether or not to suppress wrapping the deflate output with the
42   * standard zlib header and checksum fields. Defaults to true.
43   */
44  private boolean nowrap = true;
45
46  /**
47   * The size of the buffer used for reading data in during
48   * {@link #compress(InputStream, OutputStream)}.
49   */
50  private int inputBufferSize = 32768;
51
52  /**
53   * The size of the buffer used for writing data out during
54   * {@link #compress(InputStream, OutputStream)}.
55   */
56  private int outputBufferSize = 32768;
57
58  /**
59   * Cached {@link Deflater} to be used.
60   */
61  private Deflater deflater = null;
62
63  /**
64   * Whether or not to cache {@link Deflater} instances, which is a major performance tradeoff.
65   */
66  private boolean caching = false;
67
68  /**
69   * Returns whether or not to suppress wrapping the deflate output with the standard zlib header
70   * and checksum fields.
71   * @return the value
72   * @see Deflater#Deflater(int, boolean)
73   */
74  public boolean isNowrap() {
75    return nowrap;
76  }
77
78  /**
79   * Sets whether or not to suppress wrapping the deflate output with the standard zlib header and
80   * checksum fields. Defaults to false.
81   * @param nowrap see {@link Deflater#Deflater(int, boolean)}
82   */
83  public void setNowrap(boolean nowrap) {
84    if (nowrap != this.nowrap) {
85      release(); // Cannot re-use the deflater any more.
86      this.nowrap = nowrap;
87    }
88  }
89
90  /**
91   * Returns the compression level that will be used, in the range 0-9.
92   * @return the level
93   */
94  public int getCompressionLevel() {
95    return compressionLevel;
96  }
97
98  /**
99   * Sets the compression level to be used. Defaults to {@link Deflater#BEST_COMPRESSION}.
100   * @param compressionLevel the level, in the range 0-9
101   */
102  public void setCompressionLevel(int compressionLevel) {
103    if (compressionLevel < 0 || compressionLevel > 9) {
104      throw new IllegalArgumentException(
105          "compressionLevel must be in the range [0,9]: " + compressionLevel);
106    }
107    if (deflater != null && compressionLevel != this.compressionLevel) {
108      deflater.reset();
109      deflater.setLevel(compressionLevel);
110    }
111    this.compressionLevel = compressionLevel;
112  }
113
114  /**
115   * Returns the strategy that will be used, from {@link Deflater}.
116   * @return the strategy
117   */
118  public int getStrategy() {
119    return strategy;
120  }
121
122  /**
123   * Sets the strategy that will be used. Valid values can be found in {@link Deflater}. Defaults to
124   * {@link Deflater#DEFAULT_STRATEGY}
125   * @param strategy the strategy to be used
126   */
127  public void setStrategy(int strategy) {
128    if (deflater != null && strategy != this.strategy) {
129      deflater.reset();
130      deflater.setStrategy(strategy);
131    }
132    this.strategy = strategy;
133  }
134
135  /**
136   * Returns the size of the buffer used for reading from the input stream in
137   * {@link #compress(InputStream, OutputStream)}.
138   * @return the size (default is 32768)
139   */
140  public int getInputBufferSize() {
141    return inputBufferSize;
142  }
143
144  /**
145   * Sets the size of the buffer used for reading from the input stream in
146   * {@link #compress(InputStream, OutputStream)}.
147   * @param inputBufferSize the size to set (default is 32768)
148   */
149  public void setInputBufferSize(int inputBufferSize) {
150    this.inputBufferSize = inputBufferSize;
151  }
152
153  /**
154   * Returns the size of the buffer used for writing to the output stream in
155   * {@link #compress(InputStream, OutputStream)}.
156   * @return the size (default is 32768)
157   */
158  public int getOutputBufferSize() {
159    return outputBufferSize;
160  }
161
162  /**
163   * Sets the size of the buffer used for writing to the output stream in
164   * {@link #compress(InputStream, OutputStream)}.
165   * NB: {@link Deflater} uses an <em>internal</em> buffer and this method adjusts the size of that
166   * buffer. This buffer is important for performance, <em>even if the {@link OutputStream} is
167   * is already buffered</em>.
168   * @param outputBufferSize the size to set (default is 32768)
169   */
170  public void setOutputBufferSize(int outputBufferSize) {
171    this.outputBufferSize = outputBufferSize;
172  }
173
174  /**
175   * Returns if caching is enabled.
176   * @return true if enabled, otherwise false
177   * @see #setCaching(boolean)
178   */
179  public boolean isCaching() {
180    return caching;
181  }
182
183  /**
184   * Sets whether or not to cache the {@link Deflater} instance. Defaults to false. If set to true,
185   * the {@link Deflater} is kept until this object is finalized or until {@link #release()} is
186   * called. Instances of {@link Deflater} can be surprisingly expensive, so caching is advised in
187   * situations where many resources need to be deflated.
188   * @param caching whether to enable caching
189   */
190  public void setCaching(boolean caching) {
191    this.caching = caching;
192  }
193
194  /**
195   * Returns the {@link Deflater} to be used, creating a new one if necessary and caching it for
196   * future use.
197   * @return the deflater
198   */
199  protected Deflater createOrResetDeflater() {
200    Deflater result = deflater;
201    if (result == null) {
202      result = new Deflater(compressionLevel, nowrap);
203      result.setStrategy(strategy);
204      if (caching) {
205        deflater = result;
206      }
207    } else {
208      result.reset();
209    }
210    return result;
211  }
212
213  /**
214   * Immediately releases any cached {@link Deflater} instance.
215   */
216  public void release() {
217    if (deflater != null) {
218      deflater.end();
219      deflater = null;
220    }
221  }
222
223  @Override
224  public void compress(InputStream uncompressedIn, OutputStream compressedOut) throws IOException {
225    byte[] buffer = new byte[inputBufferSize];
226    DeflaterOutputStream deflaterOut =
227        new DeflaterOutputStream(compressedOut, createOrResetDeflater(), outputBufferSize);
228    int numRead = 0;
229    while ((numRead = uncompressedIn.read(buffer)) >= 0) {
230      deflaterOut.write(buffer, 0, numRead);
231    }
232    deflaterOut.finish();
233    deflaterOut.flush();
234  }
235}
236