1 package org.opentrafficsim.base.compressedfiles;
2
3 import java.io.BufferedInputStream;
4 import java.io.Closeable;
5 import java.io.File;
6 import java.io.FileInputStream;
7 import java.io.IOException;
8 import java.io.InputStream;
9 import java.util.zip.GZIPInputStream;
10 import java.util.zip.ZipFile;
11
12 /**
13 * Reader for compressed files.
14 * <p>
15 * Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
16 * BSD-style license. See <a href="http://opentrafficsim.org/node/13">OpenTrafficSim License</a>.
17 * <p>
18 * @version $Revision$, $LastChangedDate$, by $Author$, initial version Oct 25, 2018 <br>
19 * @author <a href="http://www.tbm.tudelft.nl/averbraeck">Alexander Verbraeck</a>
20 * @author <a href="http://www.tudelft.nl/pknoppers">Peter Knoppers</a>
21 * @author <a href="http://www.transport.citg.tudelft.nl">Wouter Schakel</a>
22 */
23 public final class Reader
24 {
25 /**
26 * Class with only static methods should not be instantiated.
27 */
28 private Reader()
29 {
30 // Do not instantiate.
31 }
32
33 /**
34 * Construct a InputStream for a compressed data file.
35 * @param fileName String; the name of the file
36 * @param compressionType CompressionType; the expected type of the data compression in the file
37 * @return InputStream that can yield the expanded content of the file.
38 * @throws IOException when the file could not be read
39 */
40 public static InputStream createInputStream(final String fileName, final CompressionType compressionType) throws IOException
41 {
42 CompressionType useCompressionType =
43 CompressionType.AUTODETECT.equals(compressionType) ? autoDetectCompressionType(fileName) : compressionType;
44 switch (useCompressionType)
45 {
46 case AUTODETECT:
47 throw new IOException("Cannot happen");
48
49 case GZIP:
50 return new GZIPInputStream(new FileInputStream(fileName));
51
52 case NONE:
53 return new FileInputStream(fileName);
54
55 case ZIP:
56 {
57 ZipFile zipFile = new ZipFile(fileName);
58 return new ZipInputStream(zipFile, zipFile.getInputStream(zipFile.entries().nextElement()));
59 }
60
61 default:
62 // Cannot happen
63 throw new IOException("Don't know how to create input stream for compression type " + compressionType);
64
65 }
66 }
67
68 /**
69 * Construct a InputStream for a compressed data file. The type of compression is auto-detected.
70 * @param fileName String; the name of the file
71 * @return InputStream that can yield the expanded content of the file.
72 * @throws IOException when the file can not be opened or read
73 */
74 public static InputStream createInputStream(final String fileName) throws IOException
75 {
76 return createInputStream(fileName, CompressionType.AUTODETECT);
77 }
78
79 /**
80 * Determine the type of compression used in a file.
81 * <p>
82 * Derived from <a href="http://stackoverflow.com/questions/4818468/how-to-check-if-inputstream-is-gzipped">
83 * http://stackoverflow.com/questions/4818468/how-to-check-if-inputstream-is-gzipped</a>. <br>
84 * Gzip inflate an inputStream (if it is indeed gzip compressed), otherwise return an InputStream that yields the same data
85 * as the <cite>input</cite> argument.
86 * @param fileName String; the name of the file to check
87 * @return InputStream yielding the inflated data
88 * @throws IOException when errors occur reading the signature bytes
89 */
90 public static CompressionType autoDetectCompressionType(final String fileName) throws IOException
91 {
92 final int signatureSize = 10;
93 BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(new File(fileName)));
94 byte[] signature = new byte[signatureSize];
95 bufferedInputStream.read(signature); // read the signature
96 bufferedInputStream.close();
97 // for (int i = 0; i < signatureSize; i++)
98 // {
99 // System.err.println("byte " + i + " is " + String.format("%02x", signature[i]));
100 // }
101 if (isGZipCompressed(signature))
102 {
103 return CompressionType.GZIP;
104 }
105 else if (isZipCompressed(signature))
106 {
107 return CompressionType.ZIP;
108 }
109 return CompressionType.NONE;
110 }
111
112 /**
113 * Determine if bytes match the GZip compression signature. Derived from
114 * <a href="http://stackoverflow.com/questions/4818468/how-to-check-if-inputstream-is-gzipped">
115 * http://stackoverflow.com/questions/4818468/how-to-check-if-inputstream-is-gzipped</a>. <br>
116 * Determines if a byte array is compressed. The java.util.zip GZip implementation does not expose the GZip header so it is
117 * difficult to determine if a string is compressed.
118 * @param bytes byte[]; at least 2 bytes from the start of the stream to determine compression type
119 * @return boolean; true if the data appears to be GZip compressed; false otherwise
120 * @throws java.io.IOException if the byte array couldn't be read
121 */
122 public static boolean isGZipCompressed(final byte[] bytes) throws IOException
123 {
124 return (bytes[0] == (byte) (GZIPInputStream.GZIP_MAGIC)) && (bytes[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8));
125 }
126
127 /**
128 * Determine if bytes match a ZIP archive signature. Derived from <a href=
129 * "https://en.wikipedia.org/wiki/List_of_file_signatures">https://en.wikipedia.org/wiki/List_of_file_signatures</a>.
130 * @param bytes byte[]; at least 4 bytes from the start of the stream to determine compression type.
131 * @return boolean; true if bytes indicates the start of a ZIP archive; false otherwise
132 */
133 private static boolean isZipCompressed(final byte[] bytes)
134 {
135 if (bytes[0] != 0x50 || bytes[1] != 0x4b)
136 {
137 return false;
138 }
139 return 0x03 == bytes[2] && 0x04 == bytes[3] || 0x05 == bytes[2] && 0x06 == bytes[3]
140 || 0x07 == bytes[2] && 0x08 == bytes[3];
141 }
142
143 /**
144 * Container for a ZipFile that implements Readable and closes the contained ZipFile on close.
145 */
146 static class ZipInputStream extends InputStream implements Closeable
147 {
148 /** The ZipFile that needs to be closed when the input stream is closed. */
149 private final ZipFile zipFile;
150
151 /** The input stream. */
152 private final InputStream inputStream;
153
154 /**
155 * Construct a new ZipInputStream.
156 * @param zipFile ZipFile; the opened ZIP file
157 * @param inputStream InputStream; input stream of (the first) entry in the ZIP file
158 */
159 ZipInputStream(final ZipFile zipFile, final InputStream inputStream)
160 {
161 this.inputStream = inputStream;
162 this.zipFile = zipFile;
163 }
164
165 /**
166 * Close down the reader and release all resources.
167 * @throws IOException when closing the reader fails
168 */
169 @Override
170 public void close() throws IOException
171 {
172 super.close();
173 this.zipFile.close();
174 }
175
176 /** {@inheritDoc} */
177 @Override
178 public int read() throws IOException
179 {
180 return this.inputStream.read();
181 }
182
183 /** {@inheritDoc} */
184 @Override
185 public String toString()
186 {
187 return "ZipInputStream [zipFile=" + this.zipFile + ", inputStream=" + this.inputStream + "]";
188 }
189
190 }
191
192 }