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}