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 Finalization has been deprecated for removal.  See
265      * {@link java.lang.Object#finalize} for background information and details
266      * about migration options.
267      */
268     @Deprecated(since="9", forRemoval=true)
269     @SuppressWarnings("removal")
270     protected void finalize() throws Throwable {
271         // Empty finalizer: for performance reasons we instead use the
272         // Disposer mechanism for ensuring that the underlying
273         // RandomAccessFile is closed/deleted prior to garbage collection
274     }
275 
276     private static class StreamDisposerRecord implements DisposerRecord {
277         private File cacheFile;
278         private RandomAccessFile cache;
279 
280         public StreamDisposerRecord(File cacheFile, RandomAccessFile cache) {
281             this.cacheFile = cacheFile;
282             this.cache = cache;
283         }
284 
285         public synchronized void dispose() {
286             if (cache != null) {
287                 try {
288                     cache.close();
289                 } catch (IOException e) {
290                 } finally {
291                     cache = null;
292                 }
293             }
294             if (cacheFile != null) {
295                 cacheFile.delete();
296                 cacheFile = null;
297             }
298             // Note: Explicit removal of the stream from the StreamCloser
299             // queue is not mandatory in this case, as it will be removed
300             // automatically by GC shortly after this method is called.
301         }
302     }
303 }