    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;
    024import java.security.AccessController;
    025import java.security.PrivilegedAction;
    026import java.util.ArrayList;
    027import java.util.Collections;
    028import java.util.Iterator;
    029import java.util.Locale;
    030import java.util.Set;
    031import java.util.SortedMap;
    032import java.util.TreeMap;
    033
    034import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
    035import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
    036import org.apache.commons.compress.compressors.deflate.DeflateCompressorInputStream;
    037import org.apache.commons.compress.compressors.deflate.DeflateCompressorOutputStream;
    038import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
    039import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
    040import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream;
    041import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream;
    042import org.apache.commons.compress.compressors.lzma.LZMAUtils;
    043import org.apache.commons.compress.compressors.pack200.Pack200CompressorInputStream;
    044import org.apache.commons.compress.compressors.pack200.Pack200CompressorOutputStream;
    045import org.apache.commons.compress.compressors.snappy.FramedSnappyCompressorInputStream;
    046import org.apache.commons.compress.compressors.snappy.SnappyCompressorInputStream;
    047import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
    048import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream;
    049import org.apache.commons.compress.compressors.xz.XZUtils;
    050import org.apache.commons.compress.compressors.z.ZCompressorInputStream;
    051import org.apache.commons.compress.utils.IOUtils;
    052import org.apache.commons.compress.utils.Lists;
    053import org.apache.commons.compress.utils.ServiceLoaderIterator;
    054import org.apache.commons.compress.utils.Sets;
    055
    056/**
    057 * <p>
    058 * Factory to create Compressor[In|Out]putStreams from names. To add other
    059 * implementations you should extend CompressorStreamFactory and override the
    060 * appropriate methods (and call their implementation from super of course).
    061 * </p>
    062 *
    063 * Example (Compressing a file):
    064 *
    065 * <pre>
    066 * final OutputStream out = new FileOutputStream(output);
    067 * CompressorOutputStream cos = new CompressorStreamFactory()
    068 *         .createCompressorOutputStream(CompressorStreamFactory.BZIP2, out);
    069 * IOUtils.copy(new FileInputStream(input), cos);
    070 * cos.close();
    071 * </pre>
    072 *
    073 * Example (Decompressing a file):
    074 *
    075 * <pre>
    076 * final InputStream is = new FileInputStream(input);
    077 * CompressorInputStream in = new CompressorStreamFactory().createCompressorInputStream(CompressorStreamFactory.BZIP2,
    078 *         is);
    079 * IOUtils.copy(in, new FileOutputStream(output));
    080 * in.close();
    081 * </pre>
    082 *
    083 * @Immutable provided that the deprecated method setDecompressConcatenated is
    084 *            not used.
    085 * @ThreadSafe even if the deprecated method setDecompressConcatenated is used
    086 */
    087public class CompressorStreamFactory implements CompressorStreamProvider {
    088
    089    private static final CompressorStreamFactory SINGLETON = new CompressorStreamFactory();
    090
    091    /**
    092     * Constant (value {@value}) used to identify the BZIP2 compression
    093     * algorithm.
    094     *
    095     * @since 1.1
    096     */
    097    public static final String BZIP2 = "bzip2";
    098
    099    /**
    100     * Constant (value {@value}) used to identify the GZIP compression
    101     * algorithm. Not supported as an output stream type.
    102     *
    103     * @since 1.1
    104     */
    105    public static final String GZIP = "gz";
    106
    107    /**
    108     * Constant (value {@value}) used to identify the PACK200 compression
    109     * algorithm.
    110     *
    111     * @since 1.3
    112     */
    113    public static final String PACK200 = "pack200";
    114
    115    /**
    116     * Constant (value {@value}) used to identify the XZ compression method.
    117     *
    118     * @since 1.4
    119     */
    120    public static final String XZ = "xz";
    121
    122    /**
    123     * Constant (value {@value}) used to identify the LZMA compression method.
    124     * Not supported as an output stream type.
    125     *
    126     * @since 1.6
    127     */
    128    public static final String LZMA = "lzma";
    129
    130    /**
    131     * Constant (value {@value}) used to identify the "framed" Snappy
    132     * compression method. Not supported as an output stream type.
    133     *
    134     * @since 1.7
    135     */
    136    public static final String SNAPPY_FRAMED = "snappy-framed";
    137
    138    /**
    139     * Constant (value {@value}) used to identify the "raw" Snappy compression
    140     * method. Not supported as an output stream type.
    141     *
    142     * @since 1.7
    143     */
    144    public static final String SNAPPY_RAW = "snappy-raw";
    145
    146    /**
    147     * Constant (value {@value}) used to identify the traditional Unix compress
    148     * method. Not supported as an output stream type.
    149     *
    150     * @since 1.7
    151     */
    152    public static final String Z = "z";
    153
    154    /**
    155     * Constant (value {@value}) used to identify the Deflate compress method.
    156     *
    157     * @since 1.9
    158     */
    159    public static final String DEFLATE = "deflate";
    160
    161    /**
    162     * Constructs a new sorted map from input stream provider names to provider
    163     * objects.
    164     *
    165     * <p>
    166     * The map returned by this method will have one entry for each provider for
    167     * which support is available in the current Java virtual machine. If two or
    168     * more supported provider have the same name then the resulting map will
    169     * contain just one of them; which one it will contain is not specified.
    170     * </p>
    171     *
    172     * <p>
    173     * The invocation of this method, and the subsequent use of the resulting
    174     * map, may cause time-consuming disk or network I/O operations to occur.
    175     * This method is provided for applications that need to enumerate all of
    176     * the available providers, for example to allow user provider selection.
    177     * </p>
    178     *
    179     * <p>
    180     * This method may return different results at different times if new
    181     * providers are dynamically made available to the current Java virtual
    182     * machine.
    183     * </p>
    184     *
    185     * @return An immutable, map from names to provider objects
    186     * @since 1.13
    187     */
    188    public static SortedMap<String, CompressorStreamProvider> findAvailableCompressorInputStreamProviders() {
    189        return AccessController.doPrivileged(new PrivilegedAction<SortedMap<String, CompressorStreamProvider>>() {
    190            @Override
    191            public SortedMap<String, CompressorStreamProvider> run() {
    192                final TreeMap<String, CompressorStreamProvider> map = new TreeMap<>();
    193                putAll(SINGLETON.getInputStreamCompressorNames(), SINGLETON, map);
    194                for (final CompressorStreamProvider provider : findCompressorStreamProviders()) {
    195                    putAll(provider.getInputStreamCompressorNames(), provider, map);
    196                }
    197                return map;
    198            }
    199        });
    200    }
    201
    202    /**
    203     * Constructs a new sorted map from output stream provider names to provider
    204     * objects.
    205     *
    206     * <p>
    207     * The map returned by this method will have one entry for each provider for
    208     * which support is available in the current Java virtual machine. If two or
    209     * more supported provider have the same name then the resulting map will
    210     * contain just one of them; which one it will contain is not specified.
    211     * </p>
    212     *
    213     * <p>
    214     * The invocation of this method, and the subsequent use of the resulting
    215     * map, may cause time-consuming disk or network I/O operations to occur.
    216     * This method is provided for applications that need to enumerate all of
    217     * the available providers, for example to allow user provider selection.
    218     * </p>
    219     *
    220     * <p>
    221     * This method may return different results at different times if new
    222     * providers are dynamically made available to the current Java virtual
    223     * machine.
    224     * </p>
    225     *
    226     * @return An immutable, map from names to provider objects
    227     * @since 1.13
    228     */
    229    public static SortedMap<String, CompressorStreamProvider> findAvailableCompressorOutputStreamProviders() {
    230        return AccessController.doPrivileged(new PrivilegedAction<SortedMap<String, CompressorStreamProvider>>() {
    231            @Override
    232            public SortedMap<String, CompressorStreamProvider> run() {
    233                final TreeMap<String, CompressorStreamProvider> map = new TreeMap<>();
    234                putAll(SINGLETON.getOutputStreamCompressorNames(), SINGLETON, map);
    235                for (final CompressorStreamProvider provider : findCompressorStreamProviders()) {
    236                    putAll(provider.getOutputStreamCompressorNames(), provider, map);
    237                }
    238                return map;
    239            }
    240
    241        });
    242    }
    243    private static ArrayList<CompressorStreamProvider> findCompressorStreamProviders() {
    244        return Lists.newArrayList(serviceLoaderIterator());
    245    }
    246
    247    public static String getBzip2() {
    248        return BZIP2;
    249    }
    250
    251    public static String getDeflate() {
    252        return DEFLATE;
    253    }
    254
    255    public static String getGzip() {
    256        return GZIP;
    257    }
    258
    259    public static String getLzma() {
    260        return LZMA;
    261    }
    262
    263    public static String getPack200() {
    264        return PACK200;
    265    }
    266
    267    public static CompressorStreamFactory getSingleton() {
    268        return SINGLETON;
    269    }
    270
    271    public static String getSnappyFramed() {
    272        return SNAPPY_FRAMED;
    273    }
    274
    275    public static String getSnappyRaw() {
    276        return SNAPPY_RAW;
    277    }
    278
    279    public static String getXz() {
    280        return XZ;
    281    }
    282
    283    public static String getZ() {
    284        return Z;
    285    }
    286
    287    static void putAll(final Set<String> names, final CompressorStreamProvider provider,
    288            final TreeMap<String, CompressorStreamProvider> map) {
    289        for (final String name : names) {
    290            map.put(toKey(name), provider);
    291        }
    292    }
    293
    294    private static Iterator<CompressorStreamProvider> serviceLoaderIterator() {
    295        return new ServiceLoaderIterator<>(CompressorStreamProvider.class);
    296    }
    297
    298    private static String toKey(final String name) {
    299        return name.toUpperCase(Locale.ROOT);
    300    }
    301
    302    /**
    303     * If true, decompress until the end of the input. If false, stop after the
    304     * first stream and leave the input position to point to the next byte after
    305     * the stream
    306     */
    307    private final Boolean decompressUntilEOF;
    308    // This is Boolean so setDecompressConcatenated can determine whether it has
    309    // been set by the ctor
    310    // once the setDecompressConcatenated method has been removed, it can revert
    311    // to boolean
    312
    313    private SortedMap<String, CompressorStreamProvider> compressorInputStreamProviders;
    314
    315    private SortedMap<String, CompressorStreamProvider> compressorOutputStreamProviders;
    316
    317    /**
    318     * If true, decompress until the end of the input. If false, stop after the
    319     * first stream and leave the input position to point to the next byte after
    320     * the stream
    321     */
    322    private volatile boolean decompressConcatenated = false;
    323
    324    /**
    325     * Create an instance with the decompress Concatenated option set to false.
    326     */
    327    public CompressorStreamFactory() {
    328        this.decompressUntilEOF = null;
    329    }
    330
    331    /**
    332     * Create an instance with the provided decompress Concatenated option.
    333     *
    334     * @param decompressUntilEOF
    335     *            if true, decompress until the end of the input; if false, stop
    336     *            after the first stream and leave the input position to point
    337     *            to the next byte after the stream. This setting applies to the
    338     *            gzip, bzip2 and xz formats only.
    339     * @since 1.10
    340     */
    341    public CompressorStreamFactory(final boolean decompressUntilEOF) {
    342        this.decompressUntilEOF = Boolean.valueOf(decompressUntilEOF);
    343        // Also copy to existing variable so can continue to use that as the
    344        // current value
    345        this.decompressConcatenated = decompressUntilEOF;
    346    }
    347
    348    /**
    349     * Create an compressor input stream from an input stream, autodetecting the
    350     * compressor type from the first few bytes of the stream. The InputStream
    351     * must support marks, like BufferedInputStream.
    352     *
    353     * @param in
    354     *            the input stream
    355     * @return the compressor input stream
    356     * @throws CompressorException
    357     *             if the compressor name is not known
    358     * @throws IllegalArgumentException
    359     *             if the stream is null or does not support mark
    360     * @since 1.1
    361     */
    362    public CompressorInputStream createCompressorInputStream(final InputStream in) throws CompressorException {
    363        if (in == null) {
    364            throw new IllegalArgumentException("Stream must not be null.");
    365        }
    366
    367        if (!in.markSupported()) {
    368            throw new IllegalArgumentException("Mark is not supported.");
    369        }
    370
    371        final byte[] signature = new byte[12];
    372        in.mark(signature.length);
    373        try {
    374            final int signatureLength = IOUtils.readFully(in, signature);
    375            in.reset();
    376
    377            if (BZip2CompressorInputStream.matches(signature, signatureLength)) {
    378                return new BZip2CompressorInputStream(in, decompressConcatenated);
    379            }
    380
    381            if (GzipCompressorInputStream.matches(signature, signatureLength)) {
    382                return new GzipCompressorInputStream(in, decompressConcatenated);
    383            }
    384
    385            if (Pack200CompressorInputStream.matches(signature, signatureLength)) {
    386                return new Pack200CompressorInputStream(in);
    387            }
    388
    389            if (FramedSnappyCompressorInputStream.matches(signature, signatureLength)) {
    390                return new FramedSnappyCompressorInputStream(in);
    391            }
    392
    393            if (ZCompressorInputStream.matches(signature, signatureLength)) {
    394                return new ZCompressorInputStream(in);
    395            }
    396
    397            if (DeflateCompressorInputStream.matches(signature, signatureLength)) {
    398                return new DeflateCompressorInputStream(in);
    399            }
    400
    401            if (XZUtils.matches(signature, signatureLength) && XZUtils.isXZCompressionAvailable()) {
    402                return new XZCompressorInputStream(in, decompressConcatenated);
    403            }
    404
    405            if (LZMAUtils.matches(signature, signatureLength) && LZMAUtils.isLZMACompressionAvailable()) {
    406                return new LZMACompressorInputStream(in);
    407            }
    408
    409        } catch (final IOException e) {
    410            throw new CompressorException("Failed to detect Compressor from InputStream.", e);
    411        }
    412
    413        throw new CompressorException("No Compressor found for the stream signature.");
    414    }
    415
    416    /**
    417     * Creates a compressor input stream from a compressor name and an input
    418     * stream.
    419     *
    420     * @param name
    421     *            of the compressor, i.e. {@value #GZIP}, {@value #BZIP2},
    422     *            {@value #XZ}, {@value #LZMA}, {@value #PACK200},
    423     *            {@value #SNAPPY_RAW}, {@value #SNAPPY_FRAMED}, {@value #Z} or
    424     *            {@value #DEFLATE}
    425     * @param in
    426     *            the input stream
    427     * @return compressor input stream
    428     * @throws CompressorException
    429     *             if the compressor name is not known
    430     * @throws IllegalArgumentException
    431     *             if the name or input stream is null
    432     */
    433    public CompressorInputStream createCompressorInputStream(final String name, final InputStream in)
    434            throws CompressorException {
    435        return createCompressorInputStream(name, in, decompressConcatenated);
    436    }
    437
    438    @Override
    439    public CompressorInputStream createCompressorInputStream(final String name, final InputStream in,
    440            final boolean actualDecompressConcatenated) throws CompressorException {
    441        if (name == null || in == null) {
    442            throw new IllegalArgumentException("Compressor name and stream must not be null.");
    443        }
    444
    445        try {
    446
    447            if (GZIP.equalsIgnoreCase(name)) {
    448                return new GzipCompressorInputStream(in, actualDecompressConcatenated);
    449            }
    450
    451            if (BZIP2.equalsIgnoreCase(name)) {
    452                return new BZip2CompressorInputStream(in, actualDecompressConcatenated);
    453            }
    454
    455            if (XZ.equalsIgnoreCase(name)) {
    456                return new XZCompressorInputStream(in, actualDecompressConcatenated);
    457            }
    458
    459            if (LZMA.equalsIgnoreCase(name)) {
    460                return new LZMACompressorInputStream(in);
    461            }
    462
    463            if (PACK200.equalsIgnoreCase(name)) {
    464                return new Pack200CompressorInputStream(in);
    465            }
    466
    467            if (SNAPPY_RAW.equalsIgnoreCase(name)) {
    468                return new SnappyCompressorInputStream(in);
    469            }
    470
    471            if (SNAPPY_FRAMED.equalsIgnoreCase(name)) {
    472                return new FramedSnappyCompressorInputStream(in);
    473            }
    474
    475            if (Z.equalsIgnoreCase(name)) {
    476                return new ZCompressorInputStream(in);
    477            }
    478
    479            if (DEFLATE.equalsIgnoreCase(name)) {
    480                return new DeflateCompressorInputStream(in);
    481            }
    482
    483        } catch (final IOException e) {
    484            throw new CompressorException("Could not create CompressorInputStream.", e);
    485        }
    486        final CompressorStreamProvider compressorStreamProvider = getCompressorInputStreamProviders().get(toKey(name));
    487        if (compressorStreamProvider != null) {
    488            return compressorStreamProvider.createCompressorInputStream(name, in, actualDecompressConcatenated);
    489        }
    490
    491        throw new CompressorException("Compressor: " + name + " not found.");
    492    }
    493
    494    /**
    495     * Creates an compressor output stream from an compressor name and an output
    496     * stream.
    497     *
    498     * @param name
    499     *            the compressor name, i.e. {@value #GZIP}, {@value #BZIP2},
    500     *            {@value #XZ}, {@value #PACK200} or {@value #DEFLATE}
    501     * @param out
    502     *            the output stream
    503     * @return the compressor output stream
    504     * @throws CompressorException
    505     *             if the archiver name is not known
    506     * @throws IllegalArgumentException
    507     *             if the archiver name or stream is null
    508     */
    509    @Override
    510    public CompressorOutputStream createCompressorOutputStream(final String name, final OutputStream out)
    511            throws CompressorException {
    512        if (name == null || out == null) {
    513            throw new IllegalArgumentException("Compressor name and stream must not be null.");
    514        }
    515
    516        try {
    517
    518            if (GZIP.equalsIgnoreCase(name)) {
    519                return new GzipCompressorOutputStream(out);
    520            }
    521
    522            if (BZIP2.equalsIgnoreCase(name)) {
    523                return new BZip2CompressorOutputStream(out);
    524            }
    525
    526            if (XZ.equalsIgnoreCase(name)) {
    527                return new XZCompressorOutputStream(out);
    528            }
    529
    530            if (PACK200.equalsIgnoreCase(name)) {
    531                return new Pack200CompressorOutputStream(out);
    532            }
    533
    534            if (LZMA.equalsIgnoreCase(name)) {
    535                return new LZMACompressorOutputStream(out);
    536            }
    537
    538            if (DEFLATE.equalsIgnoreCase(name)) {
    539                return new DeflateCompressorOutputStream(out);
    540            }
    541
    542        } catch (final IOException e) {
    543            throw new CompressorException("Could not create CompressorOutputStream", e);
    544        }
    545        final CompressorStreamProvider compressorStreamProvider = getCompressorOutputStreamProviders().get(toKey(name));
    546        if (compressorStreamProvider != null) {
    547            return compressorStreamProvider.createCompressorOutputStream(name, out);
    548        }
    549        throw new CompressorException("Compressor: " + name + " not found.");
    550    }
    551
    552    public SortedMap<String, CompressorStreamProvider> getCompressorInputStreamProviders() {
    553        if (compressorInputStreamProviders == null) {
    554            compressorInputStreamProviders = Collections
    555                    .unmodifiableSortedMap(findAvailableCompressorInputStreamProviders());
    556        }
    557        return compressorInputStreamProviders;
    558    }
    559
    560    public SortedMap<String, CompressorStreamProvider> getCompressorOutputStreamProviders() {
    561        if (compressorOutputStreamProviders == null) {
    562            compressorOutputStreamProviders = Collections
    563                    .unmodifiableSortedMap(findAvailableCompressorOutputStreamProviders());
    564        }
    565        return compressorOutputStreamProviders;
    566    }
    567
    568    // For Unit tests
    569    boolean getDecompressConcatenated() {
    570        return decompressConcatenated;
    571    }
    572
    573    public Boolean getDecompressUntilEOF() {
    574        return decompressUntilEOF;
    575    }
    576
    577    @Override
    578    public Set<String> getInputStreamCompressorNames() {
    579        return Sets.newHashSet(GZIP, BZIP2, XZ, LZMA, PACK200, SNAPPY_RAW, SNAPPY_FRAMED, Z, DEFLATE);
    580    }
    581
    582    @Override
    583    public Set<String> getOutputStreamCompressorNames() {
    584        return Sets.newHashSet(GZIP, BZIP2, XZ, LZMA, PACK200, DEFLATE);
    585    }
    586
    587    /**
    588     * Whether to decompress the full input or only the first stream in formats
    589     * supporting multiple concatenated input streams.
    590     *
    591     * <p>
    592     * This setting applies to the gzip, bzip2 and xz formats only.
    593     * </p>
    594     *
    595     * @param decompressConcatenated
    596     *            if true, decompress until the end of the input; if false, stop
    597     *            after the first stream and leave the input position to point
    598     *            to the next byte after the stream
    599     * @since 1.5
    600     * @deprecated 1.10 use the {@link #CompressorStreamFactory(boolean)}
    601     *             constructor instead
    602     * @throws IllegalStateException
    603     *             if the constructor {@link #CompressorStreamFactory(boolean)}
    604     *             was used to create the factory
    605     */
    606    @Deprecated
    607    public void setDecompressConcatenated(final boolean decompressConcatenated) {
    608        if (this.decompressUntilEOF != null) {
    609            throw new IllegalStateException("Cannot override the setting defined by the constructor");
    610        }
    611        this.decompressConcatenated = decompressConcatenated;
    612    }
    613
    614}