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.spi.StreamCachingStrategy; 034import org.apache.camel.spi.Synchronization; 035import org.apache.camel.spi.UnitOfWork; 036import org.apache.camel.support.SynchronizationAdapter; 037import org.apache.camel.util.FileUtil; 038import org.apache.camel.util.ObjectHelper; 039import org.slf4j.Logger; 040import org.slf4j.LoggerFactory; 041 042/** 043 * This output stream will store the content into a File if the stream context size is exceed the 044 * THRESHOLD value. The default THRESHOLD value is {@link StreamCache#DEFAULT_SPOOL_THRESHOLD} bytes . 045 * <p/> 046 * The temp file will store in the temp directory, you can configure it by setting the TEMP_DIR property. 047 * If you don't set the TEMP_DIR property, it will choose the directory which is set by the 048 * system property of "java.io.tmpdir". 049 * <p/> 050 * You can get a cached input stream of this stream. The temp file which is created with this 051 * output stream will be deleted when you close this output stream or the all cached 052 * fileInputStream is closed after the exchange is completed. 053 */ 054public class CachedOutputStream extends OutputStream { 055 @Deprecated 056 public static final String THRESHOLD = "CamelCachedOutputStreamThreshold"; 057 @Deprecated 058 public static final String BUFFER_SIZE = "CamelCachedOutputStreamBufferSize"; 059 @Deprecated 060 public static final String TEMP_DIR = "CamelCachedOutputStreamOutputDirectory"; 061 @Deprecated 062 public static final String CIPHER_TRANSFORMATION = "CamelCachedOutputStreamCipherTransformation"; 063 private static final Logger LOG = LoggerFactory.getLogger(CachedOutputStream.class); 064 065 private final StreamCachingStrategy strategy; 066 private OutputStream currentStream; 067 private boolean inMemory = true; 068 private int totalLength; 069 private File tempFile; 070 private FileInputStreamCache fileInputStreamCache; 071 private CipherPair ciphers; 072 private final boolean closedOnCompletion; 073 074 public CachedOutputStream(Exchange exchange) { 075 this(exchange, true); 076 } 077 078 public CachedOutputStream(Exchange exchange, final boolean closedOnCompletion) { 079 this.closedOnCompletion = closedOnCompletion; 080 this.strategy = exchange.getContext().getStreamCachingStrategy(); 081 currentStream = new CachedByteArrayOutputStream(strategy.getBufferSize()); 082 if (closedOnCompletion) { 083 // add on completion so we can cleanup after the exchange is done such as deleting temporary files 084 Synchronization onCompletion = new SynchronizationAdapter() { 085 @Override 086 public void onDone(Exchange exchange) { 087 try { 088 if (fileInputStreamCache != null) { 089 fileInputStreamCache.close(); 090 } 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 if (fileInputStreamCache != null) { 131 fileInputStreamCache.close(); 132 } 133 try { 134 cleanUpTempFile(); 135 } catch (Exception e) { 136 LOG.warn("Error deleting temporary cache file: " + tempFile + ". This exception will be ignored.", e); 137 } 138 } 139 } 140 141 public boolean equals(Object obj) { 142 return currentStream.equals(obj); 143 } 144 145 public int hashCode() { 146 return currentStream.hashCode(); 147 } 148 149 public OutputStream getCurrentStream() { 150 return currentStream; 151 } 152 153 public String toString() { 154 return "CachedOutputStream[size: " + totalLength + "]"; 155 } 156 157 public void write(byte[] b, int off, int len) throws IOException { 158 this.totalLength += len; 159 if (inMemory && currentStream instanceof ByteArrayOutputStream && strategy.shouldSpoolCache(totalLength)) { 160 pageToFileStream(); 161 } 162 currentStream.write(b, off, len); 163 } 164 165 public void write(byte[] b) throws IOException { 166 this.totalLength += b.length; 167 if (inMemory && currentStream instanceof ByteArrayOutputStream && strategy.shouldSpoolCache(totalLength)) { 168 pageToFileStream(); 169 } 170 currentStream.write(b); 171 } 172 173 public void write(int b) throws IOException { 174 this.totalLength++; 175 if (inMemory && currentStream instanceof ByteArrayOutputStream && strategy.shouldSpoolCache(totalLength)) { 176 pageToFileStream(); 177 } 178 currentStream.write(b); 179 } 180 181 public InputStream getInputStream() throws IOException { 182 flush(); 183 184 if (inMemory) { 185 if (currentStream instanceof CachedByteArrayOutputStream) { 186 return ((CachedByteArrayOutputStream) currentStream).newInputStreamCache(); 187 } else { 188 throw new IllegalStateException("CurrentStream should be an instance of CachedByteArrayOutputStream but is: " + currentStream.getClass().getName()); 189 } 190 } else { 191 try { 192 if (fileInputStreamCache == null) { 193 fileInputStreamCache = new FileInputStreamCache(tempFile, ciphers); 194 } 195 return fileInputStreamCache; 196 } catch (FileNotFoundException e) { 197 throw new IOException("Cached file " + tempFile + " not found", e); 198 } 199 } 200 } 201 202 public InputStream getWrappedInputStream() throws IOException { 203 // The WrappedInputStream will close the CachedOutputStream when it is closed 204 return new WrappedInputStream(this, getInputStream()); 205 } 206 207 /** 208 * @deprecated use {@link #newStreamCache()} 209 */ 210 @Deprecated 211 public StreamCache getStreamCache() throws IOException { 212 return newStreamCache(); 213 } 214 215 /** 216 * Creates a new {@link StreamCache} from the data cached in this {@link OutputStream}. 217 */ 218 public StreamCache newStreamCache() throws IOException { 219 flush(); 220 221 if (inMemory) { 222 if (currentStream instanceof CachedByteArrayOutputStream) { 223 return ((CachedByteArrayOutputStream) currentStream).newInputStreamCache(); 224 } else { 225 throw new IllegalStateException("CurrentStream should be an instance of CachedByteArrayOutputStream but is: " + currentStream.getClass().getName()); 226 } 227 } else { 228 try { 229 if (fileInputStreamCache == null) { 230 fileInputStreamCache = new FileInputStreamCache(tempFile, ciphers); 231 } 232 return fileInputStreamCache; 233 } catch (FileNotFoundException e) { 234 throw new IOException("Cached file " + tempFile + " not found", e); 235 } 236 } 237 } 238 239 private void cleanUpTempFile() { 240 // cleanup temporary file 241 if (tempFile != null) { 242 FileUtil.deleteFile(tempFile); 243 tempFile = null; 244 } 245 } 246 247 private void pageToFileStream() throws IOException { 248 flush(); 249 250 ByteArrayOutputStream bout = (ByteArrayOutputStream)currentStream; 251 tempFile = FileUtil.createTempFile("cos", ".tmp", strategy.getSpoolDirectory()); 252 253 LOG.trace("Creating temporary stream cache file: {}", tempFile); 254 255 try { 256 currentStream = createOutputStream(tempFile); 257 bout.writeTo(currentStream); 258 } finally { 259 // ensure flag is flipped to file based 260 inMemory = false; 261 } 262 } 263 264 /** 265 * @deprecated use {@link #getStrategyBufferSize()} 266 */ 267 @Deprecated 268 public int getBufferSize() { 269 return getStrategyBufferSize(); 270 } 271 272 public int getStrategyBufferSize() { 273 return strategy.getBufferSize(); 274 } 275 276 // This class will close the CachedOutputStream when it is closed 277 private static class WrappedInputStream extends InputStream { 278 private CachedOutputStream cachedOutputStream; 279 private InputStream inputStream; 280 281 WrappedInputStream(CachedOutputStream cos, InputStream is) { 282 cachedOutputStream = cos; 283 inputStream = is; 284 } 285 286 @Override 287 public int read() throws IOException { 288 return inputStream.read(); 289 } 290 291 @Override 292 public int available() throws IOException { 293 return inputStream.available(); 294 } 295 296 @Override 297 public void reset() throws IOException { 298 inputStream.reset(); 299 } 300 301 @Override 302 public void close() throws IOException { 303 inputStream.close(); 304 cachedOutputStream.close(); 305 } 306 } 307 308 private OutputStream createOutputStream(File file) throws IOException { 309 OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); 310 if (ObjectHelper.isNotEmpty(strategy.getSpoolChiper())) { 311 try { 312 if (ciphers == null) { 313 ciphers = new CipherPair(strategy.getSpoolChiper()); 314 } 315 } catch (GeneralSecurityException e) { 316 throw new IOException(e.getMessage(), e); 317 } 318 out = new CipherOutputStream(out, ciphers.getEncryptor()) { 319 boolean closed; 320 public void close() throws IOException { 321 if (!closed) { 322 super.close(); 323 closed = true; 324 } 325 } 326 }; 327 } 328 return out; 329 } 330}