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</code> that writes its
  37  * output to a regular <code>OutputStream</code>.  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</code> that will write
  58      * to a given <code>outputStream</code>.
  59      *
  60      * <p> A temporary file is used as a cache.  If
  61      * <code>cacheDir</code>is non-<code>null</code> and is a
  62      * directory, the file will be created there.  If it is
  63      * <code>null</code>, the system-dependent default temporary-file
  64      * directory will be used (see the documentation for
  65      * <code>File.createTempFile</code> for details).
  66      *
  67      * @param stream an <code>OutputStream</code> to write to.
  68      * @param cacheDir a <code>File</code> indicating where the
  69      * cache file should be created, or <code>null</code> to use the
  70      * system directory.
  71      *
  72      * @exception IllegalArgumentException if <code>stream</code>
  73      * is <code>null</code>.
  74      * @exception IllegalArgumentException if <code>cacheDir</code> is
  75      * non-<code>null</code> 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</code> 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</code> 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</code> since this
 181      * <code>ImageOutputStream</code> caches data in order to allow
 182      * seeking backwards.
 183      *
 184      * @return <code>true</code>.
 185      *
 186      * @see #isCachedMemory
 187      * @see #isCachedFile
 188      */
 189     public boolean isCached() {
 190         return true;
 191     }
 192 
 193     /**
 194      * Returns <code>true</code> since this
 195      * <code>ImageOutputStream</code> maintains a file cache.
 196      *
 197      * @return <code>true</code>.
 198      *
 199      * @see #isCached
 200      * @see #isCachedMemory
 201      */
 202     public boolean isCachedFile() {
 203         return true;
 204     }
 205 
 206     /**
 207      * Returns <code>false</code> since this
 208      * <code>ImageOutputStream</code> does not maintain a main memory
 209      * cache.
 210      *
 211      * @return <code>false</code>.
 212      *
 213      * @see #isCached
 214      * @see #isCachedFile
 215      */
 216     public boolean isCachedMemory() {
 217         return false;
 218     }
 219 
 220     /**
 221      * Closes this <code>FileCacheImageOutputStream</code>.  All
 222      * pending data is flushed to the output, and the cache file
 223      * is closed and removed.  The destination <code>OutputStream</code>
 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 }