001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.commons.compress.compressors;
020
021import java.io.IOException;
022import java.io.InputStream;
023import java.io.OutputStream;
024
025import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
026import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
027import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream;
028import org.apache.commons.compress.compressors.deflate.DeflateCompressorOutputStream;
029import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
030import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
031import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
032import org.apache.commons.compress.compressors.lzma.LZMAUtils;
033import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
034import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream;
035import org.apache.commons.compress.compressors.xz.XZUtils;
036import org.apache.commons.compress.compressors.pack200.Pack200CompressorInputStream;
037import org.apache.commons.compress.compressors.pack200.Pack200CompressorOutputStream;
038import org.apache.commons.compress.compressors.snappy.FramedSnappyCompressorInputStream;
039import org.apache.commons.compress.compressors.snappy.SnappyCompressorInputStream;
040import org.apache.commons.compress.compressors.z.ZCompressorInputStream;
041import org.apache.commons.compress.utils.IOUtils;
042
043/**
044 * <p>Factory to create Compressor[In|Out]putStreams from names. To add other
045 * implementations you should extend CompressorStreamFactory and override the
046 * appropriate methods (and call their implementation from super of course).</p>
047 * 
048 * Example (Compressing a file):
049 * 
050 * <pre>
051 * final OutputStream out = new FileOutputStream(output); 
052 * CompressorOutputStream cos = 
053 *      new CompressorStreamFactory().createCompressorOutputStream(CompressorStreamFactory.BZIP2, out);
054 * IOUtils.copy(new FileInputStream(input), cos);
055 * cos.close();
056 * </pre>
057 * 
058 * Example (Decompressing a file):
059 * <pre>
060 * final InputStream is = new FileInputStream(input); 
061 * CompressorInputStream in = 
062 *      new CompressorStreamFactory().createCompressorInputStream(CompressorStreamFactory.BZIP2, is);
063 * IOUtils.copy(in, new FileOutputStream(output));
064 * in.close();
065 * </pre>
066 * @Immutable provided that the deprecated method setDecompressConcatenated is not used.
067 * @ThreadSafe even if the deprecated method setDecompressConcatenated is used
068 */
069public class CompressorStreamFactory {
070
071    /**
072     * Constant (value {@value}) used to identify the BZIP2 compression algorithm.
073     * @since 1.1
074     */
075    public static final String BZIP2 = "bzip2";
076
077    /**
078     * Constant (value {@value}) used to identify the GZIP compression algorithm.
079     * Not supported as an output stream type.
080     * @since 1.1
081     */
082    public static final String GZIP = "gz";
083    
084    /**
085     * Constant (value {@value}) used to identify the PACK200 compression algorithm.
086     * @since 1.3
087     */
088    public static final String PACK200 = "pack200";
089
090    /**
091     * Constant (value {@value}) used to identify the XZ compression method.
092     * @since 1.4
093     */
094    public static final String XZ = "xz";
095
096    /**
097     * Constant (value {@value}) used to identify the LZMA compression method.
098     * Not supported as an output stream type.
099     * @since 1.6
100     */
101    public static final String LZMA = "lzma";
102
103    /**
104     * Constant (value {@value}) used to identify the "framed" Snappy compression method.
105     * Not supported as an output stream type.
106     * @since 1.7
107     */
108    public static final String SNAPPY_FRAMED = "snappy-framed";
109
110    /**
111     * Constant (value {@value}) used to identify the "raw" Snappy compression method.
112     * Not supported as an output stream type.
113     * @since 1.7
114     */
115    public static final String SNAPPY_RAW = "snappy-raw";
116
117    /**
118     * Constant (value {@value}) used to identify the traditional Unix compress method.
119     * Not supported as an output stream type.
120     * @since 1.7
121     */
122    public static final String Z = "z";
123
124    /**
125     * Constant (value {@value}) used to identify the Deflate compress method.
126     * @since 1.9
127     */
128    public static final String DEFLATE = "deflate";
129
130    /**
131     * If true, decompress until the end of the input.
132     * If false, stop after the first stream and leave the 
133     * input position to point to the next byte after the stream
134     */
135    private final Boolean decompressUntilEOF;
136    // This is Boolean so setDecompressConcatenated can determine whether it has been set by the ctor
137    // once the setDecompressConcatenated method has been removed, it can revert to boolean
138
139    /**
140     * If true, decompress until the end of the input.
141     * If false, stop after the first stream and leave the 
142     * input position to point to the next byte after the stream
143     */
144    private volatile boolean decompressConcatenated = false;
145
146    /**
147     * Create an instance with the decompress Concatenated option set to false.
148     */
149    public CompressorStreamFactory() {
150        this.decompressUntilEOF = null;  
151    }
152
153    /**
154     * Create an instance with the provided decompress Concatenated option.
155     * @param       decompressUntilEOF
156     *                          if true, decompress until the end of the
157     *                          input; if false, stop after the first
158     *                          stream and leave the input position to point
159     *                          to the next byte after the stream.
160     *           This setting applies to the gzip, bzip2 and xz formats only.
161     * @since 1.10
162     */
163    public CompressorStreamFactory(boolean decompressUntilEOF) {
164        this.decompressUntilEOF = Boolean.valueOf(decompressUntilEOF);
165        // Also copy to existing variable so can continue to use that as the current value
166        this.decompressConcatenated = decompressUntilEOF;
167    }
168
169    /**
170     * Whether to decompress the full input or only the first stream
171     * in formats supporting multiple concatenated input streams.
172     *
173     * <p>This setting applies to the gzip, bzip2 and xz formats only.</p>
174     *
175     * @param       decompressConcatenated
176     *                          if true, decompress until the end of the
177     *                          input; if false, stop after the first
178     *                          stream and leave the input position to point
179     *                          to the next byte after the stream
180     * @since 1.5
181     * @deprecated 1.10 use the {@link #CompressorStreamFactory(boolean)} constructor instead
182     * @throws IllegalStateException if the constructor {@link #CompressorStreamFactory(boolean)} 
183     * was used to create the factory
184     */
185    @Deprecated
186    public void setDecompressConcatenated(boolean decompressConcatenated) {
187        if (this.decompressUntilEOF != null) {
188            throw new IllegalStateException("Cannot override the setting defined by the constructor");
189        }
190        this.decompressConcatenated = decompressConcatenated;
191    }
192
193    /**
194     * Create an compressor input stream from an input stream, autodetecting
195     * the compressor type from the first few bytes of the stream. The InputStream
196     * must support marks, like BufferedInputStream.
197     * 
198     * @param in the input stream
199     * @return the compressor input stream
200     * @throws CompressorException if the compressor name is not known
201     * @throws IllegalArgumentException if the stream is null or does not support mark
202     * @since 1.1
203     */
204    public CompressorInputStream createCompressorInputStream(final InputStream in)
205            throws CompressorException {
206        if (in == null) {
207            throw new IllegalArgumentException("Stream must not be null.");
208        }
209
210        if (!in.markSupported()) {
211            throw new IllegalArgumentException("Mark is not supported.");
212        }
213
214        final byte[] signature = new byte[12];
215        in.mark(signature.length);
216        try {
217            int signatureLength = IOUtils.readFully(in, signature);
218            in.reset();
219
220            if (BZip2CompressorInputStream.matches(signature, signatureLength)) {
221                return new BZip2CompressorInputStream(in, decompressConcatenated);
222            }
223
224            if (GzipCompressorInputStream.matches(signature, signatureLength)) {
225                return new GzipCompressorInputStream(in, decompressConcatenated);
226            }
227
228            if (Pack200CompressorInputStream.matches(signature, signatureLength)) {
229                return new Pack200CompressorInputStream(in);
230            }
231
232            if (FramedSnappyCompressorInputStream.matches(signature, signatureLength)) {
233                return new FramedSnappyCompressorInputStream(in);
234            }
235
236            if (ZCompressorInputStream.matches(signature, signatureLength)) {
237                return new ZCompressorInputStream(in);
238            }
239
240            if (DeflateCompressorInputStream.matches(signature, signatureLength)) {
241                return new DeflateCompressorInputStream(in);
242            }
243
244            if (XZUtils.matches(signature, signatureLength) &&
245                XZUtils.isXZCompressionAvailable()) {
246                return new XZCompressorInputStream(in, decompressConcatenated);
247            }
248
249            if (LZMAUtils.matches(signature, signatureLength) &&
250                LZMAUtils.isLZMACompressionAvailable()) {
251                return new LZMACompressorInputStream(in);
252            }
253
254        } catch (IOException e) {
255            throw new CompressorException("Failed to detect Compressor from InputStream.", e);
256        }
257
258        throw new CompressorException("No Compressor found for the stream signature.");
259    }
260
261    /**
262     * Create a compressor input stream from a compressor name and an input stream.
263     * 
264     * @param name of the compressor,
265     * i.e. {@value #GZIP}, {@value #BZIP2}, {@value #XZ}, {@value #LZMA},
266     * {@value #PACK200}, {@value #SNAPPY_RAW}, {@value #SNAPPY_FRAMED}, 
267     * {@value #Z} or {@value #DEFLATE} 
268     * @param in the input stream
269     * @return compressor input stream
270     * @throws CompressorException if the compressor name is not known
271     * @throws IllegalArgumentException if the name or input stream is null
272     */
273    public CompressorInputStream createCompressorInputStream(final String name,
274            final InputStream in) throws CompressorException {
275        if (name == null || in == null) {
276            throw new IllegalArgumentException(
277                    "Compressor name and stream must not be null.");
278        }
279
280        try {
281
282            if (GZIP.equalsIgnoreCase(name)) {
283                return new GzipCompressorInputStream(in, decompressConcatenated);
284            }
285
286            if (BZIP2.equalsIgnoreCase(name)) {
287                return new BZip2CompressorInputStream(in, decompressConcatenated);
288            }
289
290            if (XZ.equalsIgnoreCase(name)) {
291                return new XZCompressorInputStream(in, decompressConcatenated);
292            }
293
294            if (LZMA.equalsIgnoreCase(name)) {
295                return new LZMACompressorInputStream(in);
296            }
297
298            if (PACK200.equalsIgnoreCase(name)) {
299                return new Pack200CompressorInputStream(in);
300            }
301
302            if (SNAPPY_RAW.equalsIgnoreCase(name)) {
303                return new SnappyCompressorInputStream(in);
304            }
305
306            if (SNAPPY_FRAMED.equalsIgnoreCase(name)) {
307                return new FramedSnappyCompressorInputStream(in);
308            }
309
310            if (Z.equalsIgnoreCase(name)) {
311                return new ZCompressorInputStream(in);
312            }
313
314            if (DEFLATE.equalsIgnoreCase(name)) {
315                return new DeflateCompressorInputStream(in);
316            }
317
318        } catch (IOException e) {
319            throw new CompressorException(
320                    "Could not create CompressorInputStream.", e);
321        }
322        throw new CompressorException("Compressor: " + name + " not found.");
323    }
324
325    /**
326     * Create an compressor output stream from an compressor name and an output stream.
327     * 
328     * @param name the compressor name,
329     * i.e. {@value #GZIP}, {@value #BZIP2}, {@value #XZ},
330     * {@value #PACK200} or {@value #DEFLATE} 
331     * @param out the output stream
332     * @return the compressor output stream
333     * @throws CompressorException if the archiver name is not known
334     * @throws IllegalArgumentException if the archiver name or stream is null
335     */
336    public CompressorOutputStream createCompressorOutputStream(
337            final String name, final OutputStream out)
338            throws CompressorException {
339        if (name == null || out == null) {
340            throw new IllegalArgumentException(
341                    "Compressor name and stream must not be null.");
342        }
343
344        try {
345
346            if (GZIP.equalsIgnoreCase(name)) {
347                return new GzipCompressorOutputStream(out);
348            }
349
350            if (BZIP2.equalsIgnoreCase(name)) {
351                return new BZip2CompressorOutputStream(out);
352            }
353
354            if (XZ.equalsIgnoreCase(name)) {
355                return new XZCompressorOutputStream(out);
356            }
357
358            if (PACK200.equalsIgnoreCase(name)) {
359                return new Pack200CompressorOutputStream(out);
360            }
361
362            if (DEFLATE.equalsIgnoreCase(name)) {
363                return new DeflateCompressorOutputStream(out);
364            }
365
366        } catch (IOException e) {
367            throw new CompressorException(
368                    "Could not create CompressorOutputStream", e);
369        }
370        throw new CompressorException("Compressor: " + name + " not found.");
371    }
372
373    // For Unit tests
374    boolean getDecompressConcatenated() {
375        return decompressConcatenated;
376    }
377}