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 }