1 /*
2 * Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package javax.imageio.stream;
27
28 import java.io.File;
29 import java.io.InputStream;
30 import java.io.IOException;
31 import java.io.RandomAccessFile;
32 import java.nio.file.Files;
33 import com.sun.imageio.stream.StreamCloser;
34 import com.sun.imageio.stream.StreamFinalizer;
35 import sun.java2d.Disposer;
36 import sun.java2d.DisposerRecord;
37
38 /**
39 * An implementation of {@code ImageInputStream} that gets its
40 * input from a regular {@code InputStream}. A file is used to
41 * cache previously read data.
42 *
43 */
44 public class FileCacheImageInputStream extends ImageInputStreamImpl {
45
46 private InputStream stream;
47
48 private File cacheFile;
49
50 private RandomAccessFile cache;
51
52 private static final int BUFFER_LENGTH = 1024;
53
54 private byte[] buf = new byte[BUFFER_LENGTH];
55
56 private long length = 0L;
57
58 private boolean foundEOF = false;
59
60 /** The referent to be registered with the Disposer. */
61 private final Object disposerReferent;
62
63 /** The DisposerRecord that closes the underlying cache. */
64 private final DisposerRecord disposerRecord;
65
66 /** The CloseAction that closes the stream in
67 * the StreamCloser's shutdown hook */
68 private final StreamCloser.CloseAction closeAction;
69
70 /**
71 * Constructs a {@code FileCacheImageInputStream} that will read
72 * from a given {@code InputStream}.
73 *
74 * <p> A temporary file is used as a cache. If
75 * {@code cacheDir} is non-{@code null} and is a
76 * directory, the file will be created there. If it is
77 * {@code null}, the system-dependent default temporary-file
78 * directory will be used (see the documentation for
79 * {@code File.createTempFile} for details).
80 *
81 * @param stream an {@code InputStream} to read from.
82 * @param cacheDir a {@code File} indicating where the
83 * cache file should be created, or {@code null} to use the
84 * system directory.
85 *
86 * @exception IllegalArgumentException if {@code stream} is
87 * {@code null}.
88 * @exception IllegalArgumentException if {@code cacheDir} is
89 * non-{@code null} but is not a directory.
90 * @throws IOException if a cache file cannot be created.
91 */
92 public FileCacheImageInputStream(InputStream stream, File cacheDir)
93 throws IOException {
94 if (stream == null) {
95 throw new IllegalArgumentException("stream == null!");
96 }
97 if ((cacheDir != null) && !(cacheDir.isDirectory())) {
98 throw new IllegalArgumentException("Not a directory!");
99 }
100 this.stream = stream;
101 if (cacheDir == null)
102 this.cacheFile = Files.createTempFile("imageio", ".tmp").toFile();
103 else
104 this.cacheFile = Files.createTempFile(cacheDir.toPath(), "imageio", ".tmp")
105 .toFile();
106 this.cache = new RandomAccessFile(cacheFile, "rw");
107
108 this.closeAction = StreamCloser.createCloseAction(this);
109 StreamCloser.addToQueue(closeAction);
110
111 disposerRecord = new StreamDisposerRecord(cacheFile, cache);
112 if (getClass() == FileCacheImageInputStream.class) {
113 disposerReferent = new Object();
114 Disposer.addRecord(disposerReferent, disposerRecord);
115 } else {
116 disposerReferent = new StreamFinalizer(this);
117 }
118 }
119
120 /**
121 * Ensures that at least {@code pos} bytes are cached,
122 * or the end of the source is reached. The return value
123 * is equal to the smaller of {@code pos} and the
124 * length of the source file.
125 *
126 * @throws IOException if an I/O error occurs while reading from the
127 * source file
128 */
129 private long readUntil(long pos) throws IOException {
130 // We've already got enough data cached
131 if (pos < length) {
132 return pos;
133 }
134 // pos >= length but length isn't getting any bigger, so return it
135 if (foundEOF) {
136 return length;
137 }
138
139 long len = pos - length;
140 cache.seek(length);
141 while (len > 0) {
142 // Copy a buffer's worth of data from the source to the cache
143 // BUFFER_LENGTH will always fit into an int so this is safe
144 int nbytes =
145 stream.read(buf, 0, (int)Math.min(len, (long)BUFFER_LENGTH));
146 if (nbytes == -1) {
147 foundEOF = true;
148 return length;
149 }
150
151 cache.write(buf, 0, nbytes);
152 len -= nbytes;
153 length += nbytes;
154 }
155
156 return pos;
157 }
158
159 public int read() throws IOException {
160 checkClosed();
161 bitOffset = 0;
162 long next = streamPos + 1;
163 long pos = readUntil(next);
164 if (pos >= next) {
165 cache.seek(streamPos++);
166 return cache.read();
167 } else {
168 return -1;
169 }
170 }
171
172 public int read(byte[] b, int off, int len) throws IOException {
173 checkClosed();
174
175 if (b == null) {
176 throw new NullPointerException("b == null!");
177 }
178 // Fix 4430357 - if off + len < 0, overflow occurred
179 if (off < 0 || len < 0 || off + len > b.length || off + len < 0) {
180 throw new IndexOutOfBoundsException
181 ("off < 0 || len < 0 || off+len > b.length || off+len < 0!");
182 }
183
184 bitOffset = 0;
185
186 if (len == 0) {
187 return 0;
188 }
189
190 long pos = readUntil(streamPos + len);
191
192 // len will always fit into an int so this is safe
193 len = (int)Math.min((long)len, pos - streamPos);
194 if (len > 0) {
195 cache.seek(streamPos);
196 cache.readFully(b, off, len);
197 streamPos += len;
198 return len;
199 } else {
200 return -1;
201 }
202 }
203
204 /**
205 * Returns {@code true} since this
206 * {@code ImageInputStream} caches data in order to allow
207 * seeking backwards.
208 *
209 * @return {@code true}.
210 *
211 * @see #isCachedMemory
212 * @see #isCachedFile
213 */
214 public boolean isCached() {
215 return true;
216 }
217
218 /**
219 * Returns {@code true} since this
220 * {@code ImageInputStream} maintains a file cache.
221 *
222 * @return {@code true}.
223 *
224 * @see #isCached
225 * @see #isCachedMemory
226 */
227 public boolean isCachedFile() {
228 return true;
229 }
230
231 /**
232 * Returns {@code false} since this
233 * {@code ImageInputStream} does not maintain a main memory
234 * cache.
235 *
236 * @return {@code false}.
237 *
238 * @see #isCached
239 * @see #isCachedFile
240 */
241 public boolean isCachedMemory() {
242 return false;
243 }
244
245 /**
246 * Closes this {@code FileCacheImageInputStream}, closing
247 * and removing the cache file. The source {@code InputStream}
248 * is not closed.
249 *
250 * @throws IOException if an error occurs.
251 */
252 public void close() throws IOException {
253 super.close();
254 disposerRecord.dispose(); // this will close/delete the cache file
255 stream = null;
256 cache = null;
257 cacheFile = null;
258 StreamCloser.removeFromQueue(closeAction);
259 }
260
261 /**
262 * {@inheritDoc}
263 *
264 * @deprecated The {@code finalize} method has been deprecated.
265 * Subclasses that override {@code finalize} in order to perform cleanup
266 * should be modified to use alternative cleanup mechanisms and
267 * to remove the overriding {@code finalize} method.
268 * When overriding the {@code finalize} method, its implementation must explicitly
269 * ensure that {@code super.finalize()} is invoked as described in {@link Object#finalize}.
270 * See the specification for {@link Object#finalize()} for further
271 * information about migration options.
272 */
273 @Deprecated(since="9")
274 protected void finalize() throws Throwable {
275 // Empty finalizer: for performance reasons we instead use the
276 // Disposer mechanism for ensuring that the underlying
277 // RandomAccessFile is closed/deleted prior to garbage collection
278 }
279
280 private static class StreamDisposerRecord implements DisposerRecord {
281 private File cacheFile;
282 private RandomAccessFile cache;
283
284 public StreamDisposerRecord(File cacheFile, RandomAccessFile cache) {
285 this.cacheFile = cacheFile;
286 this.cache = cache;
287 }
288
289 public synchronized void dispose() {
290 if (cache != null) {
291 try {
292 cache.close();
293 } catch (IOException e) {
294 } finally {
295 cache = null;
296 }
297 }
298 if (cacheFile != null) {
299 cacheFile.delete();
300 cacheFile = null;
301 }
302 // Note: Explicit removal of the stream from the StreamCloser
303 // queue is not mandatory in this case, as it will be removed
304 // automatically by GC shortly after this method is called.
305 }
306 }
307 }
308