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.BufferedInputStream; 020import java.io.BufferedOutputStream; 021import java.io.File; 022import java.io.FileInputStream; 023import java.io.FileNotFoundException; 024import java.io.FileOutputStream; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.OutputStream; 028import java.nio.channels.Channels; 029import java.nio.channels.FileChannel; 030import java.nio.channels.WritableByteChannel; 031import java.security.GeneralSecurityException; 032import java.util.ArrayList; 033import java.util.List; 034import java.util.concurrent.atomic.AtomicInteger; 035 036import javax.crypto.CipherInputStream; 037import javax.crypto.CipherOutputStream; 038 039import org.apache.camel.Exchange; 040import org.apache.camel.RuntimeCamelException; 041import org.apache.camel.StreamCache; 042import org.apache.camel.spi.StreamCachingStrategy; 043import org.apache.camel.spi.Synchronization; 044import org.apache.camel.spi.UnitOfWork; 045import org.apache.camel.support.SynchronizationAdapter; 046import org.apache.camel.util.FileUtil; 047import org.apache.camel.util.IOHelper; 048import org.apache.camel.util.ObjectHelper; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051 052/** 053 * A {@link StreamCache} for {@link File}s 054 */ 055public final class FileInputStreamCache extends InputStream implements StreamCache { 056 private InputStream stream; 057 private final long length; 058 private final FileInputStreamCache.TempFileManager tempFileManager; 059 private final File file; 060 private final CipherPair ciphers; 061 062 /** Only for testing purposes.*/ 063 public FileInputStreamCache(File file) throws FileNotFoundException { 064 this(new TempFileManager(file, true)); 065 } 066 067 FileInputStreamCache(TempFileManager closer) throws FileNotFoundException { 068 this.file = closer.getTempFile(); 069 this.stream = null; 070 this.ciphers = closer.getCiphers(); 071 this.length = file.length(); 072 this.tempFileManager = closer; 073 this.tempFileManager.add(this); 074 } 075 076 @Override 077 public void close() { 078 if (stream != null) { 079 IOHelper.close(stream); 080 } 081 } 082 083 @Override 084 public void reset() { 085 // reset by closing and creating a new stream based on the file 086 close(); 087 // reset by creating a new stream based on the file 088 stream = null; 089 090 if (!file.exists()) { 091 throw new RuntimeCamelException("Cannot reset stream from file " + file); 092 } 093 } 094 095 public void writeTo(OutputStream os) throws IOException { 096 if (stream == null && ciphers == null) { 097 FileInputStream s = new FileInputStream(file); 098 long len = file.length(); 099 WritableByteChannel out; 100 if (os instanceof WritableByteChannel) { 101 out = (WritableByteChannel)os; 102 } else { 103 out = Channels.newChannel(os); 104 } 105 FileChannel fc = s.getChannel(); 106 long pos = 0; 107 while (pos < len) { 108 long i = fc.transferTo(pos, len - pos, out); 109 pos += i; 110 } 111 s.close(); 112 fc.close(); 113 } else { 114 IOHelper.copy(getInputStream(), os); 115 } 116 } 117 118 public StreamCache copy(Exchange exchange) throws IOException { 119 tempFileManager.addExchange(exchange); 120 FileInputStreamCache copy = new FileInputStreamCache(tempFileManager); 121 return copy; 122 } 123 124 public boolean inMemory() { 125 return false; 126 } 127 128 public long length() { 129 return length; 130 } 131 132 @Override 133 public int available() throws IOException { 134 return getInputStream().available(); 135 } 136 137 @Override 138 public int read() throws IOException { 139 return getInputStream().read(); 140 } 141 142 protected InputStream getInputStream() throws IOException { 143 if (stream == null) { 144 stream = createInputStream(file); 145 } 146 return stream; 147 } 148 149 private InputStream createInputStream(File file) throws IOException { 150 InputStream in = new BufferedInputStream(new FileInputStream(file)); 151 if (ciphers != null) { 152 in = new CipherInputStream(in, ciphers.getDecryptor()) { 153 boolean closed; 154 public void close() throws IOException { 155 if (!closed) { 156 super.close(); 157 closed = true; 158 } 159 } 160 }; 161 } 162 return in; 163 } 164 165 /** 166 * Manages the temporary file for the file input stream caches. 167 * 168 * Collects all FileInputStreamCache instances of the temporary file. 169 * Counts the number of exchanges which have a FileInputStreamCache instance of the temporary file. 170 * Deletes the temporary file, if all exchanges are done. 171 * 172 * @see CachedOutputStream 173 */ 174 static class TempFileManager { 175 176 private static final Logger LOG = LoggerFactory.getLogger(TempFileManager.class); 177 /** Indicator whether the file input stream caches are closed on completion of the exchanges. */ 178 private final boolean closedOnCompletion; 179 private AtomicInteger exchangeCounter = new AtomicInteger(); 180 private File tempFile; 181 private OutputStream outputStream; // file output stream 182 private CipherPair ciphers; 183 184 // there can be several input streams, for example in the multi-cast, or wiretap parallel processing 185 private List<FileInputStreamCache> fileInputStreamCaches; 186 187 /** Only for testing.*/ 188 private TempFileManager(File file, boolean closedOnCompletion) { 189 this(closedOnCompletion); 190 this.tempFile = file; 191 } 192 193 TempFileManager(boolean closedOnCompletion) { 194 this.closedOnCompletion = closedOnCompletion; 195 } 196 197 /** Adds a FileInputStreamCache instance to the closer. 198 * <p> 199 * Must be synchronized, because can be accessed by several threads. 200 */ 201 synchronized void add(FileInputStreamCache fileInputStreamCache) { 202 if (fileInputStreamCaches == null) { 203 fileInputStreamCaches = new ArrayList<FileInputStreamCache>(3); 204 } 205 fileInputStreamCaches.add(fileInputStreamCache); 206 } 207 208 void addExchange(Exchange exchange) { 209 if (closedOnCompletion) { 210 exchangeCounter.incrementAndGet(); 211 // add on completion so we can cleanup after the exchange is done such as deleting temporary files 212 Synchronization onCompletion = new SynchronizationAdapter() { 213 @Override 214 public void onDone(Exchange exchange) { 215 int actualExchanges = exchangeCounter.decrementAndGet(); 216 if (actualExchanges == 0) { 217 // only one exchange (one thread) left, therefore we must not synchronize the following lines of code 218 try { 219 closeFileInputStreams(); 220 if (outputStream != null) { 221 outputStream.close(); 222 } 223 try { 224 cleanUpTempFile(); 225 } catch (Exception e) { 226 LOG.warn("Error deleting temporary cache file: " + tempFile + ". This exception will be ignored.", e); 227 } 228 } catch (Exception e) { 229 LOG.warn("Error closing streams. This exception will be ignored.", e); 230 } 231 } 232 } 233 234 @Override 235 public String toString() { 236 return "OnCompletion[CachedOutputStream]"; 237 } 238 }; 239 UnitOfWork streamCacheUnitOfWork = exchange.getProperty(Exchange.STREAM_CACHE_UNIT_OF_WORK, UnitOfWork.class); 240 if (streamCacheUnitOfWork != null) { 241 // The stream cache must sometimes not be closed when the exchange is deleted. This is for example the 242 // case in the splitter and multi-cast case with AggregationStrategy where the result of the sub-routes 243 // are aggregated later in the main route. Here, the cached streams of the sub-routes must be closed with 244 // the Unit of Work of the main route. 245 streamCacheUnitOfWork.addSynchronization(onCompletion); 246 } else { 247 // add on completion so we can cleanup after the exchange is done such as deleting temporary files 248 exchange.addOnCompletion(onCompletion); 249 } 250 } 251 } 252 253 OutputStream createOutputStream(StreamCachingStrategy strategy) throws IOException { 254 // should only be called once 255 if (tempFile != null) { 256 throw new IllegalStateException("The method 'createOutputStream' can only be called once!"); 257 } 258 tempFile = FileUtil.createTempFile("cos", ".tmp", strategy.getSpoolDirectory()); 259 260 LOG.trace("Creating temporary stream cache file: {}", tempFile); 261 OutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile)); 262 if (ObjectHelper.isNotEmpty(strategy.getSpoolChiper())) { 263 try { 264 if (ciphers == null) { 265 ciphers = new CipherPair(strategy.getSpoolChiper()); 266 } 267 } catch (GeneralSecurityException e) { 268 throw new IOException(e.getMessage(), e); 269 } 270 out = new CipherOutputStream(out, ciphers.getEncryptor()) { 271 boolean closed; 272 public void close() throws IOException { 273 if (!closed) { 274 super.close(); 275 closed = true; 276 } 277 } 278 }; 279 } 280 outputStream = out; 281 return out; 282 } 283 284 FileInputStreamCache newStreamCache() throws IOException { 285 try { 286 return new FileInputStreamCache(this); 287 } catch (FileNotFoundException e) { 288 throw new IOException("Cached file " + tempFile + " not found", e); 289 } 290 } 291 292 void closeFileInputStreams() { 293 if (fileInputStreamCaches != null) { 294 for (FileInputStreamCache fileInputStreamCache : fileInputStreamCaches) { 295 fileInputStreamCache.close(); 296 } 297 fileInputStreamCaches.clear(); 298 } 299 } 300 301 void cleanUpTempFile() { 302 // cleanup temporary file 303 try { 304 if (tempFile != null) { 305 FileUtil.deleteFile(tempFile); 306 tempFile = null; 307 } 308 } catch (Exception e) { 309 LOG.warn("Error deleting temporary cache file: " + tempFile + ". This exception will be ignored.", e); 310 } 311 } 312 313 File getTempFile() { 314 return tempFile; 315 } 316 317 CipherPair getCiphers() { 318 return ciphers; 319 } 320 321 } 322 323}