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      */
FileCacheImageInputStream(InputStream stream, File cacheDir)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      */
readUntil(long pos)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 
read()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 
read(byte[] b, int off, int len)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      */
isCached()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      */
isCachedFile()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      */
isCachedMemory()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      */
close()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")
finalize()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 
StreamDisposerRecord(File cacheFile, RandomAccessFile cache)284         public StreamDisposerRecord(File cacheFile, RandomAccessFile cache) {
285             this.cacheFile = cacheFile;
286             this.cache = cache;
287         }
288 
dispose()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