001/** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.camel.converter.stream; 018 019import java.io.BufferedOutputStream; 020import java.io.ByteArrayOutputStream; 021import java.io.File; 022import java.io.FileNotFoundException; 023import java.io.FileOutputStream; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.OutputStream; 027import java.security.GeneralSecurityException; 028 029import javax.crypto.CipherOutputStream; 030 031import org.apache.camel.Exchange; 032import org.apache.camel.StreamCache; 033import org.apache.camel.converter.stream.FileInputStreamCache.FileInputStreamCloser; 034import org.apache.camel.spi.StreamCachingStrategy; 035import org.apache.camel.spi.Synchronization; 036import org.apache.camel.spi.UnitOfWork; 037import org.apache.camel.support.SynchronizationAdapter; 038import org.apache.camel.util.FileUtil; 039import org.apache.camel.util.ObjectHelper; 040import org.slf4j.Logger; 041import org.slf4j.LoggerFactory; 042 043/** 044 * This output stream will store the content into a File if the stream context size is exceed the 045 * THRESHOLD value. The default THRESHOLD value is {@link StreamCache#DEFAULT_SPOOL_THRESHOLD} bytes . 046 * <p/> 047 * The temp file will store in the temp directory, you can configure it by setting the TEMP_DIR property. 048 * If you don't set the TEMP_DIR property, it will choose the directory which is set by the 049 * system property of "java.io.tmpdir". 050 * <p/> 051 * You can get a cached input stream of this stream. The temp file which is created with this 052 * output stream will be deleted when you close this output stream or the cached 053 * fileInputStream(s) is/are closed after the exchange is completed. 054 */ 055public class CachedOutputStream extends OutputStream { 056 @Deprecated 057 public static final String THRESHOLD = "CamelCachedOutputStreamThreshold"; 058 @Deprecated 059 public static final String BUFFER_SIZE = "CamelCachedOutputStreamBufferSize"; 060 @Deprecated 061 public static final String TEMP_DIR = "CamelCachedOutputStreamOutputDirectory"; 062 @Deprecated 063 public static final String CIPHER_TRANSFORMATION = "CamelCachedOutputStreamCipherTransformation"; 064 private static final Logger LOG = LoggerFactory.getLogger(CachedOutputStream.class); 065 066 private final StreamCachingStrategy strategy; 067 private OutputStream currentStream; 068 private boolean inMemory = true; 069 private int totalLength; 070 private File tempFile; 071 private FileInputStreamCache fileInputStreamCache; 072 private final FileInputStreamCloser fileInputStreamCloser = new FileInputStreamCloser(); 073 private CipherPair ciphers; 074 private final boolean closedOnCompletion; 075 076 public CachedOutputStream(Exchange exchange) { 077 this(exchange, true); 078 } 079 080 public CachedOutputStream(Exchange exchange, final boolean closedOnCompletion) { 081 this.closedOnCompletion = closedOnCompletion; 082 this.strategy = exchange.getContext().getStreamCachingStrategy(); 083 currentStream = new CachedByteArrayOutputStream(strategy.getBufferSize()); 084 if (closedOnCompletion) { 085 // add on completion so we can cleanup after the exchange is done such as deleting temporary files 086 Synchronization onCompletion = new SynchronizationAdapter() { 087 @Override 088 public void onDone(Exchange exchange) { 089 try { 090 closeFileInputStreams(); 091 close(); 092 try { 093 cleanUpTempFile(); 094 } catch (Exception e) { 095 LOG.warn("Error deleting temporary cache file: " + tempFile + ". This exception will be ignored.", e); 096 } 097 } catch (Exception e) { 098 LOG.warn("Error closing streams. This exception will be ignored.", e); 099 } 100 } 101 102 @Override 103 public String toString() { 104 return "OnCompletion[CachedOutputStream]"; 105 } 106 }; 107 108 UnitOfWork streamCacheUnitOfWork = exchange.getProperty(Exchange.STREAM_CACHE_UNIT_OF_WORK, UnitOfWork.class); 109 if (streamCacheUnitOfWork != null) { 110 // The stream cache must sometimes not be closed when the exchange is deleted. This is for example the 111 // case in the splitter and multi-cast case with AggregationStrategy where the result of the sub-routes 112 // are aggregated later in the main route. Here, the cached streams of the sub-routes must be closed with 113 // the Unit of Work of the main route. 114 streamCacheUnitOfWork.addSynchronization(onCompletion); 115 } else { 116 // add on completion so we can cleanup after the exchange is done such as deleting temporary files 117 exchange.addOnCompletion(onCompletion); 118 } 119 } 120 } 121 122 public void flush() throws IOException { 123 currentStream.flush(); 124 } 125 126 public void close() throws IOException { 127 currentStream.close(); 128 // need to clean up the temp file this time 129 if (!closedOnCompletion) { 130 closeFileInputStreams(); 131 try { 132 cleanUpTempFile(); 133 } catch (Exception e) { 134 LOG.warn("Error deleting temporary cache file: " + tempFile + ". This exception will be ignored.", e); 135 } 136 } 137 } 138 139 public boolean equals(Object obj) { 140 return currentStream.equals(obj); 141 } 142 143 public int hashCode() { 144 return currentStream.hashCode(); 145 } 146 147 public OutputStream getCurrentStream() { 148 return currentStream; 149 } 150 151 public String toString() { 152 return "CachedOutputStream[size: " + totalLength + "]"; 153 } 154 155 public void write(byte[] b, int off, int len) throws IOException { 156 this.totalLength += len; 157 if (inMemory && currentStream instanceof ByteArrayOutputStream && strategy.shouldSpoolCache(totalLength)) { 158 pageToFileStream(); 159 } 160 currentStream.write(b, off, len); 161 } 162 163 public void write(byte[] b) throws IOException { 164 this.totalLength += b.length; 165 if (inMemory && currentStream instanceof ByteArrayOutputStream && strategy.shouldSpoolCache(totalLength)) { 166 pageToFileStream(); 167 } 168 currentStream.write(b); 169 } 170 171 public void write(int b) throws IOException { 172 this.totalLength++; 173 if (inMemory && currentStream instanceof ByteArrayOutputStream && strategy.shouldSpoolCache(totalLength)) { 174 pageToFileStream(); 175 } 176 currentStream.write(b); 177 } 178 179 public InputStream getInputStream() throws IOException { 180 return (InputStream)newStreamCache(); 181 } 182 183 public InputStream getWrappedInputStream() throws IOException { 184 // The WrappedInputStream will close the CachedOutputStream when it is closed 185 return new WrappedInputStream(this, (InputStream)newStreamCache()); 186 } 187 188 /** 189 * @deprecated use {@link #newStreamCache()} 190 */ 191 @Deprecated 192 public StreamCache getStreamCache() throws IOException { 193 return newStreamCache(); 194 } 195 196 /** 197 * Creates a new {@link StreamCache} from the data cached in this {@link OutputStream}. 198 */ 199 public StreamCache newStreamCache() throws IOException { 200 flush(); 201 202 if (inMemory) { 203 if (currentStream instanceof CachedByteArrayOutputStream) { 204 return ((CachedByteArrayOutputStream) currentStream).newInputStreamCache(); 205 } else { 206 throw new IllegalStateException("CurrentStream should be an instance of CachedByteArrayOutputStream but is: " + currentStream.getClass().getName()); 207 } 208 } else { 209 try { 210 if (fileInputStreamCache == null) { 211 fileInputStreamCache = new FileInputStreamCache(tempFile, ciphers, fileInputStreamCloser); 212 } 213 return fileInputStreamCache; 214 } catch (FileNotFoundException e) { 215 throw new IOException("Cached file " + tempFile + " not found", e); 216 } 217 } 218 } 219 220 private void closeFileInputStreams() { 221 fileInputStreamCloser.close(); 222 fileInputStreamCache = null; 223 } 224 225 private void cleanUpTempFile() { 226 // cleanup temporary file 227 if (tempFile != null) { 228 FileUtil.deleteFile(tempFile); 229 tempFile = null; 230 } 231 } 232 233 private void pageToFileStream() throws IOException { 234 flush(); 235 236 ByteArrayOutputStream bout = (ByteArrayOutputStream)currentStream; 237 tempFile = FileUtil.createTempFile("cos", ".tmp", strategy.getSpoolDirectory()); 238 239 LOG.trace("Creating temporary stream cache file: {}", tempFile); 240 241 try { 242 currentStream = createOutputStream(tempFile); 243 bout.writeTo(currentStream); 244 } finally { 245 // ensure flag is flipped to file based 246 inMemory = false; 247 } 248 } 249 250 /** 251 * @deprecated use {@link #getStrategyBufferSize()} 252 */ 253 @Deprecated 254 public int getBufferSize() { 255 return getStrategyBufferSize(); 256 } 257 258 public int getStrategyBufferSize() { 259 return strategy.getBufferSize(); 260 } 261 262 // This class will close the CachedOutputStream when it is closed 263 private static class WrappedInputStream extends InputStream { 264 private CachedOutputStream cachedOutputStream; 265 private InputStream inputStream; 266 267 WrappedInputStream(CachedOutputStream cos, InputStream is) { 268 cachedOutputStream = cos; 269 inputStream = is; 270 } 271 272 @Override 273 public int read() throws IOException { 274 return inputStream.read(); 275 } 276 277 @Override 278 public int available() throws IOException { 279 return inputStream.available(); 280 } 281 282 @Override 283 public void reset() throws IOException { 284 inputStream.reset(); 285 } 286 287 @Override 288 public void close() throws IOException { 289 inputStream.close(); 290 cachedOutputStream.close(); 291 } 292 } 293 294 private OutputStream createOutputStream(File file) throws IOException { 295 OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); 296 if (ObjectHelper.isNotEmpty(strategy.getSpoolChiper())) { 297 try { 298 if (ciphers == null) { 299 ciphers = new CipherPair(strategy.getSpoolChiper()); 300 } 301 } catch (GeneralSecurityException e) { 302 throw new IOException(e.getMessage(), e); 303 } 304 out = new CipherOutputStream(out, ciphers.getEncryptor()) { 305 boolean closed; 306 public void close() throws IOException { 307 if (!closed) { 308 super.close(); 309 closed = true; 310 } 311 } 312 }; 313 } 314 return out; 315 } 316}