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}