001: /*
002: * $RCSfile: FileCacheSeekableStream.java,v $
003: *
004: * Copyright (c) 2005 Sun Microsystems, Inc. All rights reserved.
005: *
006: * Use is subject to license terms.
007: *
008: * $Revision: 1.1 $
009: * $Date: 2005/02/11 04:55:29 $
010: * $State: Exp $
011: */
012: package com.sun.media.jai.codec;
013:
014: import java.io.File;
015: import java.io.FileInputStream;
016: import java.io.InputStream;
017: import java.io.IOException;
018: import java.io.RandomAccessFile;
019: import java.lang.reflect.Method;
020: import java.util.HashSet;
021: import java.util.Iterator;
022:
023: /**
024: * A subclass of <code>SeekableStream</code> that may be used to wrap
025: * a regular <code>InputStream</code>. Seeking backwards is supported
026: * by means of a file cache. In circumstances that do not allow the
027: * creation of a temporary file (for example, due to security
028: * consideration or the absence of local disk), the
029: * <code>MemoryCacheSeekableStream</code> class may be used instead.
030: *
031: * <p> The <code>mark()</code> and <code>reset()</code> methods are
032: * supported.
033: *
034: * <p><b> This class is not a committed part of the JAI API. It may
035: * be removed or changed in future releases of JAI.</b>
036: */
037: public final class FileCacheSeekableStream extends SeekableStream {
038:
039: /** A thread to clean up all temporary files on VM exit (VM 1.3+) */
040: private static TempFileCleanupThread cleanupThread = null;
041:
042: /** The source stream. */
043: private InputStream stream;
044:
045: /** The cache File. */
046: private File cacheFile;
047:
048: /** The cache as a RandomAcessFile. */
049: private RandomAccessFile cache;
050:
051: /** The length of the read buffer. */
052: private int bufLen = 1024;
053:
054: /** The read buffer. */
055: private byte[] buf = new byte[bufLen];
056:
057: /** Number of bytes in the cache. */
058: private long length = 0;
059:
060: /** Next byte to be read. */
061: private long pointer = 0;
062:
063: /** True if we've encountered the end of the source stream. */
064: private boolean foundEOF = false;
065:
066: // Create the cleanup thread. Use reflection to preserve compile-time
067: // compatibility with JDK 1.2.
068: static {
069: try {
070: Method shutdownMethod = Runtime.class.getDeclaredMethod(
071: "addShutdownHook", new Class[] { Thread.class });
072:
073: cleanupThread = new TempFileCleanupThread();
074:
075: shutdownMethod.invoke(Runtime.getRuntime(),
076: new Object[] { cleanupThread });
077: } catch (Exception e) {
078: // Reset the Thread to null if Method.invoke failed.
079: cleanupThread = null;
080: }
081: }
082:
083: /**
084: * Constructs a <code>MemoryCacheSeekableStream</code> that takes
085: * its source data from a regular <code>InputStream</code>.
086: * Seeking backwards is supported by means of an file cache.
087: *
088: * <p> An <code>IOException</code> will be thrown if the
089: * attempt to create the cache file fails for any reason.
090: */
091: public FileCacheSeekableStream(InputStream stream)
092: throws IOException {
093: this .stream = stream;
094: this .cacheFile = File.createTempFile("jai-FCSS-", ".tmp");
095: cacheFile.deleteOnExit();
096: this .cache = new RandomAccessFile(cacheFile, "rw");
097:
098: // Add cache file to cleanup thread for deletion at VM shutdown.
099: if (cleanupThread != null) {
100: cleanupThread.addFile(this .cacheFile);
101: }
102: }
103:
104: /**
105: * Ensures that at least <code>pos</code> bytes are cached,
106: * or the end of the source is reached. The return value
107: * is equal to the smaller of <code>pos</code> and the
108: * length of the source file.
109: */
110: private long readUntil(long pos) throws IOException {
111: // We've already got enough data cached
112: if (pos < length) {
113: return pos;
114: }
115: // pos >= length but length isn't getting any bigger, so return it
116: if (foundEOF) {
117: return length;
118: }
119:
120: long len = pos - length;
121: cache.seek(length);
122: while (len > 0) {
123: // Copy a buffer's worth of data from the source to the cache
124: // bufLen will always fit into an int so this is safe
125: int nbytes = stream.read(buf, 0, (int) Math.min(len,
126: (long) bufLen));
127: if (nbytes == -1) {
128: foundEOF = true;
129: return length;
130: }
131:
132: cache.setLength(cache.length() + nbytes);
133: cache.write(buf, 0, nbytes);
134: len -= nbytes;
135: length += nbytes;
136: }
137:
138: return pos;
139: }
140:
141: /**
142: * Returns <code>true</code> since all
143: * <code>FileCacheSeekableStream</code> instances support seeking
144: * backwards.
145: */
146: public boolean canSeekBackwards() {
147: return true;
148: }
149:
150: /**
151: * Returns the current offset in this file.
152: *
153: * @return the offset from the beginning of the file, in bytes,
154: * at which the next read occurs.
155: */
156: public long getFilePointer() {
157: return pointer;
158: }
159:
160: /**
161: * Sets the file-pointer offset, measured from the beginning of this
162: * file, at which the next read occurs.
163: *
164: * @param pos the offset position, measured in bytes from the
165: * beginning of the file, at which to set the file
166: * pointer.
167: * @exception IOException if <code>pos</code> is less than
168: * <code>0</code> or if an I/O error occurs.
169: */
170: public void seek(long pos) throws IOException {
171: if (pos < 0) {
172: throw new IOException(JaiI18N
173: .getString("FileCacheSeekableStream0"));
174: }
175: pointer = pos;
176: }
177:
178: /**
179: * Reads the next byte of data from the input stream. The value byte is
180: * returned as an <code>int</code> in the range <code>0</code> to
181: * <code>255</code>. If no byte is available because the end of the stream
182: * has been reached, the value <code>-1</code> is returned. This method
183: * blocks until input data is available, the end of the stream is detected,
184: * or an exception is thrown.
185: *
186: * @return the next byte of data, or <code>-1</code> if the end of the
187: * stream is reached.
188: * @exception IOException if an I/O error occurs.
189: */
190: public int read() throws IOException {
191: long next = pointer + 1;
192: long pos = readUntil(next);
193: if (pos >= next) {
194: cache.seek(pointer++);
195: return cache.read();
196: } else {
197: return -1;
198: }
199: }
200:
201: /**
202: * Reads up to <code>len</code> bytes of data from the input stream into
203: * an array of bytes. An attempt is made to read as many as
204: * <code>len</code> bytes, but a smaller number may be read, possibly
205: * zero. The number of bytes actually read is returned as an integer.
206: *
207: * <p> This method blocks until input data is available, end of file is
208: * detected, or an exception is thrown.
209: *
210: * <p> If <code>b</code> is <code>null</code>, a
211: * <code>NullPointerException</code> is thrown.
212: *
213: * <p> If <code>off</code> is negative, or <code>len</code> is negative, or
214: * <code>off+len</code> is greater than the length of the array
215: * <code>b</code>, then an <code>IndexOutOfBoundsException</code> is
216: * thrown.
217: *
218: * <p> If <code>len</code> is zero, then no bytes are read and
219: * <code>0</code> is returned; otherwise, there is an attempt to read at
220: * least one byte. If no byte is available because the stream is at end of
221: * file, the value <code>-1</code> is returned; otherwise, at least one
222: * byte is read and stored into <code>b</code>.
223: *
224: * <p> The first byte read is stored into element <code>b[off]</code>, the
225: * next one into <code>b[off+1]</code>, and so on. The number of bytes read
226: * is, at most, equal to <code>len</code>. Let <i>k</i> be the number of
227: * bytes actually read; these bytes will be stored in elements
228: * <code>b[off]</code> through <code>b[off+</code><i>k</i><code>-1]</code>,
229: * leaving elements <code>b[off+</code><i>k</i><code>]</code> through
230: * <code>b[off+len-1]</code> unaffected.
231: *
232: * <p> In every case, elements <code>b[0]</code> through
233: * <code>b[off]</code> and elements <code>b[off+len]</code> through
234: * <code>b[b.length-1]</code> are unaffected.
235: *
236: * <p> If the first byte cannot be read for any reason other than end of
237: * file, then an <code>IOException</code> is thrown. In particular, an
238: * <code>IOException</code> is thrown if the input stream has been closed.
239: *
240: * @param b the buffer into which the data is read.
241: * @param off the start offset in array <code>b</code>
242: * at which the data is written.
243: * @param len the maximum number of bytes to read.
244: * @return the total number of bytes read into the buffer, or
245: * <code>-1</code> if there is no more data because the end of
246: * the stream has been reached.
247: * @exception IOException if an I/O error occurs.
248: */
249: public int read(byte[] b, int off, int len) throws IOException {
250: if (b == null) {
251: throw new NullPointerException();
252: }
253: if ((off < 0) || (len < 0) || (off + len > b.length)) {
254: throw new IndexOutOfBoundsException();
255: }
256: if (len == 0) {
257: return 0;
258: }
259:
260: long pos = readUntil(pointer + len);
261:
262: // len will always fit into an int so this is safe
263: len = (int) Math.min((long) len, pos - pointer);
264: if (len > 0) {
265: cache.seek(pointer);
266: cache.readFully(b, off, len);
267: pointer += len;
268: return len;
269: } else {
270: return -1;
271: }
272: }
273:
274: /**
275: * Closes this stream and releases any system resources
276: * associated with the stream.
277: *
278: * @throws IOException if an I/O error occurs.
279: */
280: public void close() throws IOException {
281: super .close();
282: cache.close();
283: cacheFile.delete();
284:
285: // Remove cache file from list of files to delete at VM shutdown.
286: if (cleanupThread != null) {
287: cleanupThread.removeFile(this .cacheFile);
288: }
289: }
290: }
291:
292: /**
293: * A singleton <code>Thread</code> passed to
294: * <code>Runtime.addShutdownHook()</code> which removes any still extant
295: * files created by <code>File.createTempFile()</code> in the
296: * <code>FileCacheSeekableStream</code> constructor.
297: */
298: class TempFileCleanupThread extends Thread {
299: /**
300: * A <code>Set</code> of temporary <code>File</code>s.
301: */
302: private HashSet tempFiles = null;
303:
304: TempFileCleanupThread() {
305: super ();
306: setPriority(MIN_PRIORITY);
307: }
308:
309: /**
310: * Deletes all <code>File</code>s in the internal cache.
311: */
312: public void run() {
313: if (tempFiles != null && tempFiles.size() > 0) {
314: Iterator fileIter = tempFiles.iterator();
315: while (fileIter.hasNext()) {
316: try {
317: File file = (File) fileIter.next();
318: file.delete();
319: } catch (Exception e) {
320: // Ignore
321: }
322: }
323: }
324: }
325:
326: /**
327: * Add a file to be deleted at shutdown.
328: */
329: synchronized void addFile(File file) {
330: if (tempFiles == null) {
331: tempFiles = new HashSet();
332: }
333: tempFiles.add(file);
334: }
335:
336: /**
337: * Remove a file to be deleted at shutdown.
338: */
339: synchronized void removeFile(File file) {
340: tempFiles.remove(file);
341: }
342: }
|