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      * @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</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      * 
 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</code> since this
 206      * <code>ImageInputStream</code> caches data in order to allow
 207      * seeking backwards.
 208      *
 209      * @return <code>true</code>.
 210      *
 211      * @see #isCachedMemory
 212      * @see #isCachedFile
 213      */
 214     public boolean isCached() {
 215         return true;
 216     }
 217 
 218     /**
 219      * Returns <code>true</code> since this
 220      * <code>ImageInputStream</code> maintains a file cache.
 221      *
 222      * @return <code>true</code>.
 223      *
 224      * @see #isCached
 225      * @see #isCachedMemory
 226      */
 227     public boolean isCachedFile() {
 228         return true;
 229     }
 230 
 231     /**
 232      * Returns <code>false</code> since this
 233      * <code>ImageInputStream</code> does not maintain a main memory
 234      * cache.
 235      *
 236      * @return <code>false</code>.
 237      *
 238      * @see #isCached
 239      * @see #isCachedFile
 240      */
 241     public boolean isCachedMemory() {
 242         return false;
 243     }
 244 
 245     /**
 246      * Closes this <code>FileCacheImageInputStream</code>, closing
 247      * and removing the cache file.  The source <code>InputStream</code>
 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     protected void finalize() throws Throwable {
 265         // Empty finalizer: for performance reasons we instead use the
 266         // Disposer mechanism for ensuring that the underlying
 267         // RandomAccessFile is closed/deleted prior to garbage collection
 268     }
 269 
 270     private static class StreamDisposerRecord implements DisposerRecord {
 271         private File cacheFile;
 272         private RandomAccessFile cache;
 273 
 274         public StreamDisposerRecord(File cacheFile, RandomAccessFile cache) {
 275             this.cacheFile = cacheFile;
 276             this.cache = cache;
 277         }
 278 
 279         public synchronized void dispose() {
 280             if (cache != null) {
 281                 try {
 282                     cache.close();
 283                 } catch (IOException e) {
 284                 } finally {
 285                     cache = null;
 286                 }
 287             }
 288             if (cacheFile != null) {
 289                 cacheFile.delete();
 290                 cacheFile = null;
 291             }
 292             // Note: Explicit removal of the stream from the StreamCloser
 293             // queue is not mandatory in this case, as it will be removed
 294             // automatically by GC shortly after this method is called.
 295         }
 296     }
 297 }