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.IOException;
30 import java.io.OutputStream;
31 import java.io.RandomAccessFile;
32 import java.nio.file.Files;
33 import com.sun.imageio.stream.StreamCloser;
34
35 /**
36  * An implementation of {@code ImageOutputStream} that writes its
37  * output to a regular {@code OutputStream}.  A file is used to
38  * cache data until it is flushed to the output stream.
39  *
40  */

41 public class FileCacheImageOutputStream extends ImageOutputStreamImpl {
42
43     private OutputStream stream;
44
45     private File cacheFile;
46
47     private RandomAccessFile cache;
48
49     // Pos after last (rightmost) byte written
50     private long maxStreamPos = 0L;
51
52     /** The CloseAction that closes the stream in
53      *  the StreamCloser's shutdown hook                     */

54     private final StreamCloser.CloseAction closeAction;
55
56     /**
57      * Constructs a {@code FileCacheImageOutputStream} that will write
58      * to a given {@code outputStream}.
59      *
60      * <p> A temporary file is used as a cache.  If
61      * {@code cacheDir} is non-{@code null} and is a
62      * directory, the file will be created there.  If it is
63      * {@code null}, the system-dependent default temporary-file
64      * directory will be used (see the documentation for
65      * {@code File.createTempFile} for details).
66      *
67      * @param stream an {@code OutputStream} to write to.
68      * @param cacheDir a {@code File} indicating where the
69      * cache file should be created, or {@code null} to use the
70      * system directory.
71      *
72      * @exception IllegalArgumentException if {@code stream}
73      * is {@code null}.
74      * @exception IllegalArgumentException if {@code cacheDir} is
75      * non-{@code null} but is not a directory.
76      * @exception IOException if a cache file cannot be created.
77      */

78     public FileCacheImageOutputStream(OutputStream stream, File cacheDir)
79         throws IOException {
80         if (stream == null) {
81             throw new IllegalArgumentException("stream == null!");
82         }
83         if ((cacheDir != null) && !(cacheDir.isDirectory())) {
84             throw new IllegalArgumentException("Not a directory!");
85         }
86         this.stream = stream;
87         if (cacheDir == null)
88             this.cacheFile = Files.createTempFile("imageio"".tmp").toFile();
89         else
90             this.cacheFile = Files.createTempFile(cacheDir.toPath(), "imageio"".tmp")
91                                   .toFile();
92         this.cache = new RandomAccessFile(cacheFile, "rw");
93
94         this.closeAction = StreamCloser.createCloseAction(this);
95         StreamCloser.addToQueue(closeAction);
96     }
97
98     public int read() throws IOException {
99         checkClosed();
100         bitOffset = 0;
101         int val =  cache.read();
102         if (val != -1) {
103             ++streamPos;
104         }
105         return val;
106     }
107
108     public int read(byte[] b, int off, int len) throws IOException {
109         checkClosed();
110
111         if (b == null) {
112             throw new NullPointerException("b == null!");
113         }
114         if (off < 0 || len < 0 || off + len > b.length || off + len < 0) {
115             throw new IndexOutOfBoundsException
116                 ("off < 0 || len < 0 || off+len > b.length || off+len < 0!");
117         }
118
119         bitOffset = 0;
120
121         if (len == 0) {
122             return 0;
123         }
124
125         int nbytes = cache.read(b, off, len);
126         if (nbytes != -1) {
127             streamPos += nbytes;
128         }
129         return nbytes;
130     }
131
132     public void write(int b) throws IOException {
133         flushBits(); // this will call checkClosed() for us
134         cache.write(b);
135         ++streamPos;
136         maxStreamPos = Math.max(maxStreamPos, streamPos);
137     }
138
139     public void write(byte[] b, int off, int len) throws IOException {
140         flushBits(); // this will call checkClosed() for us
141         cache.write(b, off, len);
142         streamPos += len;
143         maxStreamPos = Math.max(maxStreamPos, streamPos);
144     }
145
146     public long length() {
147         try {
148             checkClosed();
149             return cache.length();
150         } catch (IOException e) {
151             return -1L;
152         }
153     }
154
155     /**
156      * Sets the current stream position and resets the bit offset to
157      * 0.  It is legal to seek past the end of the file; an
158      * {@code EOFException} will be thrown only if a read is
159      * performed.  The file length will not be increased until a write
160      * is performed.
161      *
162      * @exception IndexOutOfBoundsException if {@code pos} is smaller
163      * than the flushed position.
164      * @exception IOException if any other I/O error occurs.
165      */

166     public void seek(long pos) throws IOException {
167         checkClosed();
168
169         if (pos < flushedPos) {
170             throw new IndexOutOfBoundsException();
171         }
172
173         cache.seek(pos);
174         this.streamPos = cache.getFilePointer();
175         maxStreamPos = Math.max(maxStreamPos, streamPos);
176         this.bitOffset = 0;
177     }
178
179     /**
180      * Returns {@code true} since this
181      * {@code ImageOutputStream} caches data in order to allow
182      * seeking backwards.
183      *
184      * @return {@code true}.
185      *
186      * @see #isCachedMemory
187      * @see #isCachedFile
188      */

189     public boolean isCached() {
190         return true;
191     }
192
193     /**
194      * Returns {@code true} since this
195      * {@code ImageOutputStream} maintains a file cache.
196      *
197      * @return {@code true}.
198      *
199      * @see #isCached
200      * @see #isCachedMemory
201      */

202     public boolean isCachedFile() {
203         return true;
204     }
205
206     /**
207      * Returns {@code false} since this
208      * {@code ImageOutputStream} does not maintain a main memory
209      * cache.
210      *
211      * @return {@code false}.
212      *
213      * @see #isCached
214      * @see #isCachedFile
215      */

216     public boolean isCachedMemory() {
217         return false;
218     }
219
220     /**
221      * Closes this {@code FileCacheImageOutputStream}.  All
222      * pending data is flushed to the output, and the cache file
223      * is closed and removed.  The destination {@code OutputStream}
224      * is not closed.
225      *
226      * @exception IOException if an error occurs.
227      */

228     public void close() throws IOException {
229         maxStreamPos = cache.length();
230
231         seek(maxStreamPos);
232         flushBefore(maxStreamPos);
233         super.close();
234         cache.close();
235         cache = null;
236         cacheFile.delete();
237         cacheFile = null;
238         stream.flush();
239         stream = null;
240         StreamCloser.removeFromQueue(closeAction);
241     }
242
243     public void flushBefore(long pos) throws IOException {
244         long oFlushedPos = flushedPos;
245         super.flushBefore(pos); // this will call checkClosed() for us
246
247         long flushBytes = flushedPos - oFlushedPos;
248         if (flushBytes > 0) {
249             int bufLen = 512;
250             byte[] buf = new byte[bufLen];
251             cache.seek(oFlushedPos);
252             while (flushBytes > 0) {
253                 int len = (int)Math.min(flushBytes, bufLen);
254                 cache.readFully(buf, 0, len);
255                 stream.write(buf, 0, len);
256                 flushBytes -= len;
257             }
258             stream.flush();
259         }
260     }
261 }
262