    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(final 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(final 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            final 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 (final 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 (final 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 (final 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}