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