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      */
  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     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 
 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 
 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      */
 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      */
 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      */
 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      */
 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      */
 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 
 271         public StreamDisposerRecord(File cacheFile, RandomAccessFile cache) {
 272             this.cacheFile = cacheFile;
 273             this.cache = cache;
 274         }
 275 
 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 }