/* * Copyright 2006-2012 ICEsoft Technologies Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the * License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS * IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language * governing permissions and limitations under the License. */ package org.icepdf.core.util; import java.io.*; import java.util.logging.Logger; import java.util.logging.Level; /** * The <code>ByteCache</code> class is designed to cache a byte array input * to file when a specified size has been reached (Cached). This cached byte * stream can be read from file when the byte array is needed by a program. * <p/> * The <code>ByteCache</code> creates a temporary file in the users temporary * directory and the file is deleted when the application is closed or when * the deleteFileCache is called. * <p/> * File caching can be forced by calling the forceByteCaching() method, but * caching is normally done automatically when the byteArray reaches a * specified length. The length is specified by the system property * org.icepdf.core.util.byteFileCachingSize where the associated value is byte * array length in which the byte array will be cached to a temporary file. * <p/> * Every time a new bye cache is created the full path to the file * is recored in the CacheManager class which allows the CacheManager to delete * a documents cached bytes when the document is closed. * * @since 1.1 */ public class ByteCache { private static final Logger logger = Logger.getLogger(ByteCache.class.toString()); // length of byte array that is in ByteCache private int length = 0; // temp file that is created for the cache private File tempFile = null; // is caching enabled or disabled private boolean isCached = false; // auto caching kick in size private static int fileCachingSize; // There are cases where, when we are low in memory, // that we need to write to file, even when our data // size is less than fileCachingSize private static int fileCachingFallbackSize = 512 * 1024; // disable/inable file cahcing, overrides fileCachingSize. private static boolean isCachingEnabled; // stream used for reading from file private FileOutputStream fileOutputStream = null; // stream used for writing to file private FileInputStream fileInputStream = null; // byte array used for reading bytes private ByteArrayInputStream byteArrayInputStream = null; // byte array used for writing bytes private ByteArrayOutputStream byteArrayOutputStream = null; // cache manager reference private CacheManager cacheManager = null; static { // set initial caching size at 1MB for now fileCachingSize = Defs.sysPropertyInt("org.icepdf.core.streamcache.thresholdSize", 1000000); if (fileCachingFallbackSize > fileCachingSize) fileCachingFallbackSize = fileCachingSize; // sets if file caching is enabled or disabled. isCachingEnabled = Defs.sysPropertyBoolean("org.icepdf.core.streamcache.enabled", true); } /** * Creates a new instance of <code>ByteCache</code> object. * * @param bytes bytes that will be stored in ByteCache . */ public ByteCache(byte[] bytes, Library library) { // get pointer to cache manager. cacheManager = library.getCacheManager(); try { int numNewBytes = (bytes == null) ? 0 : bytes.length; if (numNewBytes <= 0) return; calcIfFileCachingAndPotentiallyForce(numNewBytes); OutputStream out = getCorrectOutputStream(numNewBytes); out.write(bytes); length = numNewBytes; } catch (IOException e) { logger.log(Level.FINE, "Error creating ByteCache temporary file.", e); } } /** * Creates a new instance of <code>ByteCache</code> object. This is an empty * place holder for a byte stream. The length value will be used to pre * determine if a temp file will be created even though no data has been * cached. * * @param numNewBytes estimated length of a byte array that will be read at a * later instnace. */ public ByteCache(int numNewBytes, Library library) { // get pointer to cache manager. cacheManager = library.getCacheManager(); try { if (numNewBytes <= 0) return; calcIfFileCachingAndPotentiallyForce(numNewBytes); // Force creation of output stream, even though we don't use it right away getCorrectOutputStream(numNewBytes); } catch (IOException e) { logger.log(Level.FINE, "Error creating ByteCache temporary file.", e); } } /** * Writes <code>length</code> bytes from the specified byte array * starting at offset <code>offset</code> to this byte cache. * * @param in InputStream which holds the data. * @param numNewBytes the number of bytes to write. */ public void writeBytes(InputStream in, int numNewBytes) { try { if (numNewBytes <= 0) return; calcIfFileCachingAndPotentiallyForce(numNewBytes); OutputStream out = getCorrectOutputStream(numNewBytes); byte[] buffer = new byte[Math.min(numNewBytes, 4096)]; int totalRead = 0; while (totalRead < numNewBytes) { int currNumToRead = Math.min(buffer.length, numNewBytes - totalRead); int currRead = in.read(buffer, 0, currNumToRead); if (currRead <= 0) break; out.write(buffer, 0, currRead); totalRead += currRead; } length += totalRead; } catch (IOException e) { logger.log(Level.FINE, "Error writing to temporary file ", e); } } /** * Writes <code>length</code> bytes from the specified byte array * starting at offset <code>offset</code> to this byte cache. * * @param bytes the data. * @param offset the start offset in the data. * @param numNewBytes the number of bytes to write. */ public void writeBytes(byte[] bytes, int offset, int numNewBytes) { try { // if in cached mode write the tmp file if (numNewBytes <= 0) return; calcIfFileCachingAndPotentiallyForce(numNewBytes); OutputStream out = getCorrectOutputStream(Math.max(numNewBytes, 256)); out.write(bytes, offset, numNewBytes); length += numNewBytes; } catch (IOException e) { logger.log(Level.FINE, "Error writing to temporary file.", e); } } /** * Writes <code>bytes.length</code> bytes from the specified byte array * to this byte cache.. * * @param bytes the data. */ public void writeBytes(byte[] bytes) { try { int numNewBytes = (bytes == null) ? 0 : bytes.length; if (numNewBytes <= 0) return; calcIfFileCachingAndPotentiallyForce(numNewBytes); OutputStream out = getCorrectOutputStream(Math.max(numNewBytes, 256)); out.write(bytes); length += numNewBytes; } catch (IOException e) { logger.log(Level.FINE, "Error writing to temporary file ", e); } } /** * Writes the specified byte to this byte cache. * * @param bytes the byte to be written. */ public void writeBytes(int bytes) { try { int numNewBytes = 1; calcIfFileCachingAndPotentiallyForce(numNewBytes); OutputStream out = getCorrectOutputStream(256); out.write(bytes); length += numNewBytes; } catch (IOException e) { logger.log(Level.FINE, "Error writing to temporary file ", e); } } /** * Reads up to <code>length</code> bytes of data from this byte cache * into an array of bytes. This method blocks until some input is * available. * * @param bytes the buffer into which the data is read. * @param offset the start offset of the data. * @param length the maximum number of bytes read. * @return the total number of bytes read into the buffer, or * <code>-1</code> if there is no more data because the end of * the file has been reached. */ public int readBytes(byte[] bytes, int offset, int length) { // default return value int returnValue = -1; try { InputStream in = getCorrectInputStream(); if (in != null) returnValue = in.read(bytes, offset, length); } catch (IOException e) { logger.log(Level.FINE, "Error reading from temporary file ", e); } return returnValue; } /** * Reads up to <code>bytes.length</code> bytes of data from this input * stream into an array of bytes. This method blocks until some input * is available. * * @param bytes the buffer into which the data is read. * @return the total number of bytes read into the buffer, or * <code>-1</code> if there is no more data because the end of * the file has been reached. */ public int readBytes(byte[] bytes) { int returnValue = -1; try { InputStream in = getCorrectInputStream(); if (in != null) returnValue = in.read(bytes); } catch (IOException e) { logger.log(Level.FINE, "Error reading from temporary file ", e); } return returnValue; } /** * Reads a byte of data from this input stream. This method blocks * if no input is yet available. * * @return the next byte of data, or <code>-1</code> if the end of the * file is reached. */ public int readByte() { int returnValue = -1; try { InputStream in = getCorrectInputStream(); if (in != null) returnValue = in.read(); } catch (IOException e) { logger.log(Level.FINE, "Error reading from temporary file ", e); } return returnValue; } /** * Forces the caching of byte array to a temp file. If the system property * corg.icepdf.core.util.bytecache.caching is set to false this method will * not write to file. */ public void forceByteCaching() { if (isCachingEnabled && !isCached) { try { // Create a writable file channel if (tempFile == null) { createTempFile(); } // create output stream in needed if (fileOutputStream == null) { fileOutputStream = new FileOutputStream(tempFile); } // write the bytes to the cache. if (byteArrayOutputStream != null) fileOutputStream.write(byteArrayOutputStream.toByteArray()); isCached = true; // clear byte arrays to free up some memory. byteArrayOutputStream = null; byteArrayInputStream = null; } catch (IOException e) { logger.log(Level.FINE, "Error creating the temp file.", e); } } } /** * Dispose this objects internal references and free up any available memory. * * @param cache Whether to make it so we can continue to use this object later */ public void dispose(boolean cache) { try { // If not saved to file, but we'll need it later, and it's worth putting to file, then save to file if (!isCached && cache && length > fileCachingFallbackSize) { forceByteCaching(); } if (fileOutputStream != null) { fileOutputStream.flush(); fileOutputStream.close(); fileOutputStream = null; } if (fileInputStream != null) { fileInputStream.close(); fileInputStream = null; } if (byteArrayInputStream != null) { byteArrayInputStream.close(); byteArrayInputStream = null; } if (byteArrayOutputStream != null) { byteArrayOutputStream.flush(); byteArrayOutputStream.close(); // If saved to file, or we don't need it later, then nuke our mem copy if (isCached || !cache) { byteArrayOutputStream = null; } } if (!cache) { deleteFileCache(); } } catch (IOException e) { logger.log(Level.FINE, "Error closing file streams ", e); } } /** * Creates a newly allocated byte array. Its size is the current * size of this output stream and the valid contents of the buffer * have been copied into it. * * @return the current contents of this byte cache, as a byte array. */ public byte[] toByteArray() { byte[] returnValue = null; try { // if in cached mode read from the tmp file if (isCached) { if (tempFile != null) { // Create a writable file channel, can't use instance as // it will have funky side effects, for muliple calls. FileInputStream fileInputStream = new FileInputStream(tempFile); returnValue = new byte[length]; // copy the bytes fileInputStream.read(returnValue); fileInputStream.close(); } } else { // copy origional data to output stream if (byteArrayOutputStream != null) returnValue = byteArrayOutputStream.toByteArray(); } } catch (IOException e) { logger.log(Level.FINE, "Error reading from temporary file.", e); } return returnValue; } /** * Dispose of temp file and bytes array. */ public void deleteFileCache() { if (tempFile != null) { tempFile.delete(); } } /** * Closes this file input stream and releases any system resources * associated with the stream. */ public void close() { try { if (fileOutputStream != null) { fileOutputStream.close(); } if (fileInputStream != null) { fileInputStream.close(); } if (byteArrayInputStream != null) { byteArrayInputStream.close(); } if (byteArrayOutputStream != null) { byteArrayOutputStream.close(); } } catch (IOException e) { logger.log(Level.FINE, "Error closing file streams.", e); } } /** * Resets the buffer to the marked position. The marked position * is 0 unless another position was marked. */ public void reset() { try { // reseting non cached byte stream if (byteArrayInputStream != null) { byteArrayInputStream.reset(); } // reseting cached byte stream if (fileInputStream != null) { fileInputStream.close(); fileInputStream = null; fileInputStream = new FileInputStream(tempFile); } } catch (IOException e) { logger.log(Level.FINE, "Error closing file streams.", e); } } /** * Return if this byte cache has stored its byte data in a a temp file. * * @return true if the byte data has been cached to file; false otherwise. */ public boolean isCached() { return isCached; } /** * Gets the length of the byte array stored in this byte cache. * * @return */ public int getLength() { return length; } /** * @return true if all our bytes are in memory, so we won't have to go to disk for them */ public boolean inMemory() { return byteArrayOutputStream != null; } /** * Utility method for creating tempfile */ private void createTempFile() { try { // create tmp file and write bytes to it. tempFile = File.createTempFile("PDFByteStream" + this.getClass().hashCode(), ".tmp"); // Delete temp file on exits, but dispose should do this too tempFile.deleteOnExit(); // write file name to cache manager cacheManager.addCachedFile(tempFile.getAbsolutePath()); } catch (IOException e) { logger.log(Level.FINE, "Error creating byte cache tmp file"); } } private void calcIfFileCachingAndPotentiallyForce(int numNewBytes) { if (!isCached && (length + numNewBytes) > fileCachingSize) forceByteCaching(); } private OutputStream getCorrectOutputStream(int optionalPresizing) throws IOException { if (isCached) { // create the tmp file if not initiated. if (tempFile == null) { createTempFile(); } // create the new file stream if (fileOutputStream == null) { fileOutputStream = new FileOutputStream(tempFile.getAbsolutePath(), true); } return fileOutputStream; } else { // create the byte array if not initiated if (byteArrayOutputStream == null) { byteArrayOutputStream = new ByteArrayOutputStream(optionalPresizing); } return byteArrayOutputStream; } } private InputStream getCorrectInputStream() throws IOException { // if in cached mode use the tmp file if (isCached) { if (tempFile != null) { // initiate a new input stream in needed if (fileInputStream == null) { fileInputStream = new FileInputStream(tempFile); } return fileInputStream; } } else { if (byteArrayOutputStream != null) { if (byteArrayInputStream == null) { byteArrayInputStream = new ByteArrayInputStream( byteArrayOutputStream.toByteArray()); } return byteArrayInputStream; } } return null; } }