package org.opentrafficsim.base.compressedfiles; import java.io.BufferedInputStream; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.zip.GZIPInputStream; import java.util.zip.ZipFile; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; /** * Reader for compressed files. *

* Copyright (c) 2013-2020 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License. *

* @version $Revision$, $LastChangedDate$, by $Author$, initial version Oct 25, 2018
* @author Alexander Verbraeck * @author Peter Knoppers * @author Wouter Schakel */ public final class Reader { /** * Class with only static methods should not be instantiated. */ private Reader() { // Do not instantiate. } /** * Construct a InputStream for a compressed data file. * @param fileName String; the name of the file * @param compressionType CompressionType; the expected type of the data compression in the file * @return InputStream that can yield the expanded content of the file. * @throws IOException when the file could not be read */ public static InputStream createInputStream(final String fileName, final CompressionType compressionType) throws IOException { CompressionType useCompressionType = CompressionType.AUTODETECT.equals(compressionType) ? autoDetectCompressionType(fileName) : compressionType; switch (useCompressionType) { case AUTODETECT: throw new IOException("Cannot happen"); case BZIP2: // BUG create with "true" as second argument: see https://issues.apache.org/jira/browse/COMPRESS-224 return new BZip2CompressorInputStream(new FileInputStream(fileName), true); case GZIP: return new GZIPInputStream(new FileInputStream(fileName)); case NONE: return new FileInputStream(fileName); case ZIP: { ZipFile zipFile = new ZipFile(fileName); return new ZipInputStream(zipFile, zipFile.getInputStream(zipFile.entries().nextElement())); } default: // Cannot happen throw new IOException("Don't know how to create input stream for compression type " + compressionType); } } /** * Construct a InputStream for a compressed data file. The type of compression is auto-detected. * @param fileName String; the name of the file * @return InputStream that can yield the expanded content of the file. * @throws IOException when the file can not be opened or read */ public static InputStream createInputStream(final String fileName) throws IOException { return createInputStream(fileName, CompressionType.AUTODETECT); } /** * Determine the type of compression used in a file. *

* Derived from * http://stackoverflow.com/questions/4818468/how-to-check-if-inputstream-is-gzipped.
* Gzip inflate an inputStream (if it is indeed gzip compressed), otherwise return an InputStream that yields the same data * as the input argument. * @param fileName String; the name of the file to check * @return InputStream yielding the inflated data * @throws IOException when errors occur reading the signature bytes */ public static CompressionType autoDetectCompressionType(final String fileName) throws IOException { final int signatureSize = 10; BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(new File(fileName))); byte[] signature = new byte[signatureSize]; bufferedInputStream.read(signature); // read the signature bufferedInputStream.close(); // for (int i = 0; i < signatureSize; i++) // { // System.err.println("byte " + i + " is " + String.format("%02x", signature[i])); // } if (isGZipCompressed(signature)) { return CompressionType.GZIP; } else if (isBZipCompressed(signature)) { return CompressionType.BZIP2; } else if (isZipCompressed(signature)) { return CompressionType.ZIP; } return CompressionType.NONE; } /** * Determine if bytes match the GZip compression signature. Derived from * * http://stackoverflow.com/questions/4818468/how-to-check-if-inputstream-is-gzipped.
* Determines if a byte array is compressed. The java.util.zip GZip implementation does not expose the GZip header so it is * difficult to determine if a string is compressed. * @param bytes byte[]; at least 2 bytes from the start of the stream to determine compression type * @return boolean; true if the data appears to be GZip compressed; false otherwise * @throws java.io.IOException if the byte array couldn't be read */ public static boolean isGZipCompressed(final byte[] bytes) throws IOException { return (bytes[0] == (byte) (GZIPInputStream.GZIP_MAGIC)) && (bytes[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8)); } /** * Determine if bytes match the BZip2 compression signature. * @param bytes byte[]; at least 10 bytes from the start of the stream to determine compression type. * @return boolean; true if bytes indicates the start of a BZip compressed stream */ private static boolean isBZipCompressed(final byte[] bytes) { return bytes[0] == 'B' && bytes[1] == 'Z' && (bytes[2] == 'h' || bytes[2] == '0') && Character.isDigit(bytes[3]) && bytes[4] == 0x31 && bytes[5] == 0x41 && bytes[6] == 0x59 && bytes[7] == 0x26 && bytes[8] == 0x53 && bytes[9] == 0x59; } /** * Determine if bytes match a ZIP archive signature. Derived from https://en.wikipedia.org/wiki/List_of_file_signatures. * @param bytes byte[]; at least 4 bytes from the start of the stream to determine compression type. * @return boolean; true if bytes indicates the start of a ZIP archive; false otherwise */ private static boolean isZipCompressed(final byte[] bytes) { if (bytes[0] != 0x50 || bytes[1] != 0x4b) { return false; } return 0x03 == bytes[2] && 0x04 == bytes[3] || 0x05 == bytes[2] && 0x06 == bytes[3] || 0x07 == bytes[2] && 0x08 == bytes[3]; } /** * Container for a ZipFile that implements Readable and closes the contained ZipFile on close. */ static class ZipInputStream extends InputStream implements Closeable { /** The ZipFile that needs to be closed when the input stream is closed. */ private final ZipFile zipFile; /** The input stream. */ private final InputStream inputStream; /** * Construct a new ZipInputStream. * @param zipFile ZipFile; the opened ZIP file * @param inputStream InputStream; input stream of (the first) entry in the ZIP file */ ZipInputStream(final ZipFile zipFile, final InputStream inputStream) { this.inputStream = inputStream; this.zipFile = zipFile; } /** * Close down the reader and release all resources. * @throws IOException when closing the reader fails */ @Override public void close() throws IOException { super.close(); this.zipFile.close(); } /** {@inheritDoc} */ @Override public int read() throws IOException { return this.inputStream.read(); } /** {@inheritDoc} */ @Override public String toString() { return "ZipInputStream [zipFile=" + this.zipFile + ", inputStream=" + this.inputStream + "]"; } } }