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