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