blob: ad03d65c04d3fdad95f066c4624be08552055261 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 2000-2007 Sun Microsystems, Inc. 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. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26package javax.imageio.stream;
27
28import java.io.File;
29import java.io.InputStream;
30import java.io.IOException;
31import java.io.RandomAccessFile;
32import com.sun.imageio.stream.StreamCloser;
33import com.sun.imageio.stream.StreamFinalizer;
34import sun.java2d.Disposer;
35import sun.java2d.DisposerRecord;
36
37/**
38 * An implementation of <code>ImageInputStream</code> that gets its
39 * input from a regular <code>InputStream</code>. A file is used to
40 * cache previously read data.
41 *
42 */
43public class FileCacheImageInputStream extends ImageInputStreamImpl {
44
45 private InputStream stream;
46
47 private File cacheFile;
48
49 private RandomAccessFile cache;
50
51 private static final int BUFFER_LENGTH = 1024;
52
53 private byte[] buf = new byte[BUFFER_LENGTH];
54
55 private long length = 0L;
56
57 private boolean foundEOF = false;
58
59 /** The referent to be registered with the Disposer. */
60 private final Object disposerReferent;
61
62 /** The DisposerRecord that closes the underlying cache. */
63 private final DisposerRecord disposerRecord;
64
65 /**
66 * Constructs a <code>FileCacheImageInputStream</code> that will read
67 * from a given <code>InputStream</code>.
68 *
69 * <p> A temporary file is used as a cache. If
70 * <code>cacheDir</code>is non-<code>null</code> and is a
71 * directory, the file will be created there. If it is
72 * <code>null</code>, the system-dependent default temporary-file
73 * directory will be used (see the documentation for
74 * <code>File.createTempFile</code> for details).
75 *
76 * @param stream an <code>InputStream</code> to read from.
77 * @param cacheDir a <code>File</code> indicating where the
78 * cache file should be created, or <code>null</code> to use the
79 * system directory.
80 *
81 * @exception IllegalArgumentException if <code>stream</code> is
82 * <code>null</code>.
83 * @exception IllegalArgumentException if <code>cacheDir</code> is
84 * non-<code>null</code> but is not a directory.
85 * @exception IOException if a cache file cannot be created.
86 */
87 public FileCacheImageInputStream(InputStream stream, File cacheDir)
88 throws IOException {
89 if (stream == null) {
90 throw new IllegalArgumentException("stream == null!");
91 }
92 if ((cacheDir != null) && !(cacheDir.isDirectory())) {
93 throw new IllegalArgumentException("Not a directory!");
94 }
95 this.stream = stream;
96 this.cacheFile =
97 File.createTempFile("imageio", ".tmp", cacheDir);
98 this.cache = new RandomAccessFile(cacheFile, "rw");
99 StreamCloser.addToQueue(this);
100
101 disposerRecord = new StreamDisposerRecord(cacheFile, cache);
102 if (getClass() == FileCacheImageInputStream.class) {
103 disposerReferent = new Object();
104 Disposer.addRecord(disposerReferent, disposerRecord);
105 } else {
106 disposerReferent = new StreamFinalizer(this);
107 }
108 }
109
110 /**
111 * Ensures that at least <code>pos</code> bytes are cached,
112 * or the end of the source is reached. The return value
113 * is equal to the smaller of <code>pos</code> and the
114 * length of the source file.
115 */
116 private long readUntil(long pos) throws IOException {
117 // We've already got enough data cached
118 if (pos < length) {
119 return pos;
120 }
121 // pos >= length but length isn't getting any bigger, so return it
122 if (foundEOF) {
123 return length;
124 }
125
126 long len = pos - length;
127 cache.seek(length);
128 while (len > 0) {
129 // Copy a buffer's worth of data from the source to the cache
130 // BUFFER_LENGTH will always fit into an int so this is safe
131 int nbytes =
132 stream.read(buf, 0, (int)Math.min(len, (long)BUFFER_LENGTH));
133 if (nbytes == -1) {
134 foundEOF = true;
135 return length;
136 }
137
138 cache.write(buf, 0, nbytes);
139 len -= nbytes;
140 length += nbytes;
141 }
142
143 return pos;
144 }
145
146 public int read() throws IOException {
147 checkClosed();
148 bitOffset = 0;
149 long next = streamPos + 1;
150 long pos = readUntil(next);
151 if (pos >= next) {
152 cache.seek(streamPos++);
153 return cache.read();
154 } else {
155 return -1;
156 }
157 }
158
159 public int read(byte[] b, int off, int len) throws IOException {
160 checkClosed();
161
162 if (b == null) {
163 throw new NullPointerException("b == null!");
164 }
165 // Fix 4430357 - if off + len < 0, overflow occurred
166 if (off < 0 || len < 0 || off + len > b.length || off + len < 0) {
167 throw new IndexOutOfBoundsException
168 ("off < 0 || len < 0 || off+len > b.length || off+len < 0!");
169 }
170
171 bitOffset = 0;
172
173 if (len == 0) {
174 return 0;
175 }
176
177 long pos = readUntil(streamPos + len);
178
179 // len will always fit into an int so this is safe
180 len = (int)Math.min((long)len, pos - streamPos);
181 if (len > 0) {
182 cache.seek(streamPos);
183 cache.readFully(b, off, len);
184 streamPos += len;
185 return len;
186 } else {
187 return -1;
188 }
189 }
190
191 /**
192 * Returns <code>true</code> since this
193 * <code>ImageInputStream</code> caches data in order to allow
194 * seeking backwards.
195 *
196 * @return <code>true</code>.
197 *
198 * @see #isCachedMemory
199 * @see #isCachedFile
200 */
201 public boolean isCached() {
202 return true;
203 }
204
205 /**
206 * Returns <code>true</code> since this
207 * <code>ImageInputStream</code> maintains a file cache.
208 *
209 * @return <code>true</code>.
210 *
211 * @see #isCached
212 * @see #isCachedMemory
213 */
214 public boolean isCachedFile() {
215 return true;
216 }
217
218 /**
219 * Returns <code>false</code> since this
220 * <code>ImageInputStream</code> does not maintain a main memory
221 * cache.
222 *
223 * @return <code>false</code>.
224 *
225 * @see #isCached
226 * @see #isCachedFile
227 */
228 public boolean isCachedMemory() {
229 return false;
230 }
231
232 /**
233 * Closes this <code>FileCacheImageInputStream</code>, closing
234 * and removing the cache file. The source <code>InputStream</code>
235 * is not closed.
236 *
237 * @exception IOException if an error occurs.
238 */
239 public void close() throws IOException {
240 super.close();
241 disposerRecord.dispose(); // this will close/delete the cache file
242 stream = null;
243 cache = null;
244 cacheFile = null;
245 StreamCloser.removeFromQueue(this);
246 }
247
248 /**
249 * {@inheritDoc}
250 */
251 protected void finalize() throws Throwable {
252 // Empty finalizer: for performance reasons we instead use the
253 // Disposer mechanism for ensuring that the underlying
254 // RandomAccessFile is closed/deleted prior to garbage collection
255 }
256
257 private static class StreamDisposerRecord implements DisposerRecord {
258 private File cacheFile;
259 private RandomAccessFile cache;
260
261 public StreamDisposerRecord(File cacheFile, RandomAccessFile cache) {
262 this.cacheFile = cacheFile;
263 this.cache = cache;
264 }
265
266 public synchronized void dispose() {
267 if (cache != null) {
268 try {
269 cache.close();
270 } catch (IOException e) {
271 } finally {
272 cache = null;
273 }
274 }
275 if (cacheFile != null) {
276 cacheFile.delete();
277 cacheFile = null;
278 }
279 // Note: Explicit removal of the stream from the StreamCloser
280 // queue is not mandatory in this case, as it will be removed
281 // automatically by GC shortly after this method is called.
282 }
283 }
284}