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}