/** * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.camel.converter.stream; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; import org.apache.camel.Exchange; import org.apache.camel.RuntimeCamelException; import org.apache.camel.StreamCache; import org.apache.camel.spi.StreamCachingStrategy; import org.apache.camel.spi.Synchronization; import org.apache.camel.spi.UnitOfWork; import org.apache.camel.support.SynchronizationAdapter; import org.apache.camel.util.FileUtil; import org.apache.camel.util.IOHelper; import org.apache.camel.util.ObjectHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A {@link StreamCache} for {@link File}s */ public final class FileInputStreamCache extends InputStream implements StreamCache { private InputStream stream; private final long length; private final FileInputStreamCache.TempFileManager tempFileManager; private final File file; private final CipherPair ciphers; /** Only for testing purposes.*/ public FileInputStreamCache(File file) throws FileNotFoundException { this(new TempFileManager(file, true)); } FileInputStreamCache(TempFileManager closer) throws FileNotFoundException { this.file = closer.getTempFile(); this.stream = null; this.ciphers = closer.getCiphers(); this.length = file.length(); this.tempFileManager = closer; this.tempFileManager.add(this); } @Override public void close() { if (stream != null) { IOHelper.close(stream); } } @Override public void reset() { // reset by closing and creating a new stream based on the file close(); // reset by creating a new stream based on the file stream = null; if (!file.exists()) { throw new RuntimeCamelException("Cannot reset stream from file " + file); } } public void writeTo(OutputStream os) throws IOException { if (stream == null && ciphers == null) { FileInputStream s = new FileInputStream(file); long len = file.length(); WritableByteChannel out; if (os instanceof WritableByteChannel) { out = (WritableByteChannel)os; } else { out = Channels.newChannel(os); } FileChannel fc = s.getChannel(); long pos = 0; while (pos < len) { long i = fc.transferTo(pos, len - pos, out); pos += i; } s.close(); fc.close(); } else { IOHelper.copy(getInputStream(), os); } } public StreamCache copy(Exchange exchange) throws IOException { tempFileManager.addExchange(exchange); FileInputStreamCache copy = new FileInputStreamCache(tempFileManager); return copy; } public boolean inMemory() { return false; } public long length() { return length; } @Override public int available() throws IOException { return getInputStream().available(); } @Override public int read() throws IOException { return getInputStream().read(); } protected InputStream getInputStream() throws IOException { if (stream == null) { stream = createInputStream(file); } return stream; } private InputStream createInputStream(File file) throws IOException { InputStream in = new BufferedInputStream(new FileInputStream(file)); if (ciphers != null) { in = new CipherInputStream(in, ciphers.getDecryptor()) { boolean closed; public void close() throws IOException { if (!closed) { super.close(); closed = true; } } }; } return in; } /** * Manages the temporary file for the file input stream caches. * * Collects all FileInputStreamCache instances of the temporary file. * Counts the number of exchanges which have a FileInputStreamCache instance of the temporary file. * Deletes the temporary file, if all exchanges are done. * * @see CachedOutputStream */ static class TempFileManager { private static final Logger LOG = LoggerFactory.getLogger(TempFileManager.class); /** Indicator whether the file input stream caches are closed on completion of the exchanges. */ private final boolean closedOnCompletion; private AtomicInteger exchangeCounter = new AtomicInteger(); private File tempFile; private OutputStream outputStream; // file output stream private CipherPair ciphers; // there can be several input streams, for example in the multi-cast, or wiretap parallel processing private List<FileInputStreamCache> fileInputStreamCaches; /** Only for testing.*/ private TempFileManager(File file, boolean closedOnCompletion) { this(closedOnCompletion); this.tempFile = file; } TempFileManager(boolean closedOnCompletion) { this.closedOnCompletion = closedOnCompletion; } /** Adds a FileInputStreamCache instance to the closer. * <p> * Must be synchronized, because can be accessed by several threads. */ synchronized void add(FileInputStreamCache fileInputStreamCache) { if (fileInputStreamCaches == null) { fileInputStreamCaches = new ArrayList<FileInputStreamCache>(3); } fileInputStreamCaches.add(fileInputStreamCache); } void addExchange(Exchange exchange) { if (closedOnCompletion) { exchangeCounter.incrementAndGet(); // add on completion so we can cleanup after the exchange is done such as deleting temporary files Synchronization onCompletion = new SynchronizationAdapter() { @Override public void onDone(Exchange exchange) { int actualExchanges = exchangeCounter.decrementAndGet(); if (actualExchanges == 0) { // only one exchange (one thread) left, therefore we must not synchronize the following lines of code try { closeFileInputStreams(); if (outputStream != null) { outputStream.close(); } try { cleanUpTempFile(); } catch (Exception e) { LOG.warn("Error deleting temporary cache file: " + tempFile + ". This exception will be ignored.", e); } } catch (Exception e) { LOG.warn("Error closing streams. This exception will be ignored.", e); } } } @Override public String toString() { return "OnCompletion[CachedOutputStream]"; } }; UnitOfWork streamCacheUnitOfWork = exchange.getProperty(Exchange.STREAM_CACHE_UNIT_OF_WORK, UnitOfWork.class); if (streamCacheUnitOfWork != null) { // The stream cache must sometimes not be closed when the exchange is deleted. This is for example the // case in the splitter and multi-cast case with AggregationStrategy where the result of the sub-routes // are aggregated later in the main route. Here, the cached streams of the sub-routes must be closed with // the Unit of Work of the main route. streamCacheUnitOfWork.addSynchronization(onCompletion); } else { // add on completion so we can cleanup after the exchange is done such as deleting temporary files exchange.addOnCompletion(onCompletion); } } } OutputStream createOutputStream(StreamCachingStrategy strategy) throws IOException { // should only be called once if (tempFile != null) { throw new IllegalStateException("The method 'createOutputStream' can only be called once!"); } tempFile = FileUtil.createTempFile("cos", ".tmp", strategy.getSpoolDirectory()); LOG.trace("Creating temporary stream cache file: {}", tempFile); OutputStream out = new BufferedOutputStream(new FileOutputStream(tempFile)); if (ObjectHelper.isNotEmpty(strategy.getSpoolChiper())) { try { if (ciphers == null) { ciphers = new CipherPair(strategy.getSpoolChiper()); } } catch (GeneralSecurityException e) { throw new IOException(e.getMessage(), e); } out = new CipherOutputStream(out, ciphers.getEncryptor()) { boolean closed; public void close() throws IOException { if (!closed) { super.close(); closed = true; } } }; } outputStream = out; return out; } FileInputStreamCache newStreamCache() throws IOException { try { return new FileInputStreamCache(this); } catch (FileNotFoundException e) { throw new IOException("Cached file " + tempFile + " not found", e); } } void closeFileInputStreams() { if (fileInputStreamCaches != null) { for (FileInputStreamCache fileInputStreamCache : fileInputStreamCaches) { fileInputStreamCache.close(); } fileInputStreamCaches.clear(); } } void cleanUpTempFile() { // cleanup temporary file try { if (tempFile != null) { FileUtil.deleteFile(tempFile); tempFile = null; } } catch (Exception e) { LOG.warn("Error deleting temporary cache file: " + tempFile + ". This exception will be ignored.", e); } } File getTempFile() { return tempFile; } CipherPair getCiphers() { return ciphers; } } }