1 /*
2  * Copyright (c) 2000, 2012, 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</code> that gets its
40  * input from a regular <code>InputStream</code>.  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</code> that will read
72      * from a given <code>InputStream</code>.
73      *
74      * <p> A temporary file is used as a cache.  If
75      * <code>cacheDir</code>is non-<code>null</code> and is a
76      * directory, the file will be created there.  If it is
77      * <code>null</code>, the system-dependent default temporary-file
78      * directory will be used (see the documentation for
79      * <code>File.createTempFile</code> for details).
80      *
81      * @param stream an <code>InputStream</code> to read from.
82      * @param cacheDir a <code>File</code> indicating where the
83      * cache file should be created, or <code>null</code> to use the
84      * system directory.
85      *
86      * @exception IllegalArgumentException if <code>stream</code> is
87      * <code>null</code>.
88      * @exception IllegalArgumentException if <code>cacheDir</code> is
89      * non-<code>null</code> but is not a directory.
90      * @exception 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</code> bytes are cached,
122      * or the end of the source is reached.  The return value
123      * is equal to the smaller of <code>pos</code> and the
124      * length of the source file.
125      */
readUntil(long pos)126     private long readUntil(long pos) throws IOException {
127         // We've already got enough data cached
128         if (pos < length) {
129             return pos;
130         }
131         // pos >= length but length isn't getting any bigger, so return it
132         if (foundEOF) {
133             return length;
134         }
135 
136         long len = pos - length;
137         cache.seek(length);
138         while (len > 0) {
139             // Copy a buffer's worth of data from the source to the cache
140             // BUFFER_LENGTH will always fit into an int so this is safe
141             int nbytes =
142                 stream.read(buf, 0, (int)Math.min(len, (long)BUFFER_LENGTH));
143             if (nbytes == -1) {
144                 foundEOF = true;
145                 return length;
146             }
147 
148             cache.write(buf, 0, nbytes);
149             len -= nbytes;
150             length += nbytes;
151         }
152 
153         return pos;
154     }
155 
read()156     public int read() throws IOException {
157         checkClosed();
158         bitOffset = 0;
159         long next = streamPos + 1;
160         long pos = readUntil(next);
161         if (pos >= next) {
162             cache.seek(streamPos++);
163             return cache.read();
164         } else {
165             return -1;
166         }
167     }
168 
read(byte[] b, int off, int len)169     public int read(byte[] b, int off, int len) throws IOException {
170         checkClosed();
171 
172         if (b == null) {
173             throw new NullPointerException("b == null!");
174         }
175         // Fix 4430357 - if off + len < 0, overflow occurred
176         if (off < 0 || len < 0 || off + len > b.length || off + len < 0) {
177             throw new IndexOutOfBoundsException
178                 ("off < 0 || len < 0 || off+len > b.length || off+len < 0!");
179         }
180 
181         bitOffset = 0;
182 
183         if (len == 0) {
184             return 0;
185         }
186 
187         long pos = readUntil(streamPos + len);
188 
189         // len will always fit into an int so this is safe
190         len = (int)Math.min((long)len, pos - streamPos);
191         if (len > 0) {
192             cache.seek(streamPos);
193             cache.readFully(b, off, len);
194             streamPos += len;
195             return len;
196         } else {
197             return -1;
198         }
199     }
200 
201     /**
202      * Returns <code>true</code> since this
203      * <code>ImageInputStream</code> caches data in order to allow
204      * seeking backwards.
205      *
206      * @return <code>true</code>.
207      *
208      * @see #isCachedMemory
209      * @see #isCachedFile
210      */
isCached()211     public boolean isCached() {
212         return true;
213     }
214 
215     /**
216      * Returns <code>true</code> since this
217      * <code>ImageInputStream</code> maintains a file cache.
218      *
219      * @return <code>true</code>.
220      *
221      * @see #isCached
222      * @see #isCachedMemory
223      */
isCachedFile()224     public boolean isCachedFile() {
225         return true;
226     }
227 
228     /**
229      * Returns <code>false</code> since this
230      * <code>ImageInputStream</code> does not maintain a main memory
231      * cache.
232      *
233      * @return <code>false</code>.
234      *
235      * @see #isCached
236      * @see #isCachedFile
237      */
isCachedMemory()238     public boolean isCachedMemory() {
239         return false;
240     }
241 
242     /**
243      * Closes this <code>FileCacheImageInputStream</code>, closing
244      * and removing the cache file.  The source <code>InputStream</code>
245      * is not closed.
246      *
247      * @exception IOException if an error occurs.
248      */
close()249     public void close() throws IOException {
250         super.close();
251         disposerRecord.dispose(); // this will close/delete the cache file
252         stream = null;
253         cache = null;
254         cacheFile = null;
255         StreamCloser.removeFromQueue(closeAction);
256     }
257 
258     /**
259      * {@inheritDoc}
260      */
finalize()261     protected void finalize() throws Throwable {
262         // Empty finalizer: for performance reasons we instead use the
263         // Disposer mechanism for ensuring that the underlying
264         // RandomAccessFile is closed/deleted prior to garbage collection
265     }
266 
267     private static class StreamDisposerRecord implements DisposerRecord {
268         private File cacheFile;
269         private RandomAccessFile cache;
270 
StreamDisposerRecord(File cacheFile, RandomAccessFile cache)271         public StreamDisposerRecord(File cacheFile, RandomAccessFile cache) {
272             this.cacheFile = cacheFile;
273             this.cache = cache;
274         }
275 
dispose()276         public synchronized void dispose() {
277             if (cache != null) {
278                 try {
279                     cache.close();
280                 } catch (IOException e) {
281                 } finally {
282                     cache = null;
283                 }
284             }
285             if (cacheFile != null) {
286                 cacheFile.delete();
287                 cacheFile = null;
288             }
289             // Note: Explicit removal of the stream from the StreamCloser
290             // queue is not mandatory in this case, as it will be removed
291             // automatically by GC shortly after this method is called.
292         }
293     }
294 }
295