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.BufferedOutputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.File;
022import java.io.FileNotFoundException;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.InputStream;
026import java.io.OutputStream;
027import java.security.GeneralSecurityException;
028
029import javax.crypto.CipherOutputStream;
030
031import org.apache.camel.Exchange;
032import org.apache.camel.StreamCache;
033import org.apache.camel.converter.stream.FileInputStreamCache.FileInputStreamCloser;
034import org.apache.camel.spi.StreamCachingStrategy;
035import org.apache.camel.spi.Synchronization;
036import org.apache.camel.spi.UnitOfWork;
037import org.apache.camel.support.SynchronizationAdapter;
038import org.apache.camel.util.FileUtil;
039import org.apache.camel.util.ObjectHelper;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043/**
044 * This output stream will store the content into a File if the stream context size is exceed the
045 * THRESHOLD value. The default THRESHOLD value is {@link StreamCache#DEFAULT_SPOOL_THRESHOLD} bytes .
046 * <p/>
047 * The temp file will store in the temp directory, you can configure it by setting the TEMP_DIR property.
048 * If you don't set the TEMP_DIR property, it will choose the directory which is set by the
049 * system property of "java.io.tmpdir".
050 * <p/>
051 * You can get a cached input stream of this stream. The temp file which is created with this 
052 * output stream will be deleted when you close this output stream or the cached 
053 * fileInputStream(s) is/are closed after the exchange is completed.
054 */
055public class CachedOutputStream extends OutputStream {
056    @Deprecated
057    public static final String THRESHOLD = "CamelCachedOutputStreamThreshold";
058    @Deprecated
059    public static final String BUFFER_SIZE = "CamelCachedOutputStreamBufferSize";
060    @Deprecated
061    public static final String TEMP_DIR = "CamelCachedOutputStreamOutputDirectory";
062    @Deprecated
063    public static final String CIPHER_TRANSFORMATION = "CamelCachedOutputStreamCipherTransformation";
064    private static final Logger LOG = LoggerFactory.getLogger(CachedOutputStream.class);
065
066    private final StreamCachingStrategy strategy;
067    private OutputStream currentStream;
068    private boolean inMemory = true;
069    private int totalLength;
070    private File tempFile;
071    private FileInputStreamCache fileInputStreamCache;
072    private final FileInputStreamCloser fileInputStreamCloser = new FileInputStreamCloser();
073    private CipherPair ciphers;
074    private final boolean closedOnCompletion;
075
076    public CachedOutputStream(Exchange exchange) {
077        this(exchange, true);
078    }
079
080    public CachedOutputStream(Exchange exchange, final boolean closedOnCompletion) {
081        this.closedOnCompletion = closedOnCompletion;
082        this.strategy = exchange.getContext().getStreamCachingStrategy();
083        currentStream = new CachedByteArrayOutputStream(strategy.getBufferSize());
084        if (closedOnCompletion) {
085            // add on completion so we can cleanup after the exchange is done such as deleting temporary files
086            Synchronization onCompletion = new SynchronizationAdapter() {
087                @Override
088                public void onDone(Exchange exchange) {
089                    try {
090                        closeFileInputStreams();
091                        close();
092                        try {
093                            cleanUpTempFile();
094                        } catch (Exception e) {
095                            LOG.warn("Error deleting temporary cache file: " + tempFile + ". This exception will be ignored.", e);
096                        }
097                    } catch (Exception e) {
098                        LOG.warn("Error closing streams. This exception will be ignored.", e);
099                    }
100                }
101
102                @Override
103                public String toString() {
104                    return "OnCompletion[CachedOutputStream]";
105                }
106            };
107
108            UnitOfWork streamCacheUnitOfWork = exchange.getProperty(Exchange.STREAM_CACHE_UNIT_OF_WORK, UnitOfWork.class);
109            if (streamCacheUnitOfWork != null) {
110                // The stream cache must sometimes not be closed when the exchange is deleted. This is for example the
111                // case in the splitter and multi-cast case with AggregationStrategy where the result of the sub-routes
112                // are aggregated later in the main route. Here, the cached streams of the sub-routes must be closed with
113                // the Unit of Work of the main route.
114                streamCacheUnitOfWork.addSynchronization(onCompletion);
115            } else {
116                // add on completion so we can cleanup after the exchange is done such as deleting temporary files
117                exchange.addOnCompletion(onCompletion);
118            }
119        }
120    }
121
122    public void flush() throws IOException {
123        currentStream.flush();       
124    }
125
126    public void close() throws IOException {
127        currentStream.close();
128        // need to clean up the temp file this time
129        if (!closedOnCompletion) {
130            closeFileInputStreams();
131            try {
132                cleanUpTempFile();
133            } catch (Exception e) {
134                LOG.warn("Error deleting temporary cache file: " + tempFile + ". This exception will be ignored.", e);
135            }
136        }
137    }
138
139    public boolean equals(Object obj) {
140        return currentStream.equals(obj);
141    }
142
143    public int hashCode() {
144        return currentStream.hashCode();
145    }
146
147    public OutputStream getCurrentStream() {
148        return currentStream;
149    }
150
151    public String toString() {
152        return "CachedOutputStream[size: " + totalLength + "]";
153    }
154
155    public void write(byte[] b, int off, int len) throws IOException {
156        this.totalLength += len;
157        if (inMemory && currentStream instanceof ByteArrayOutputStream && strategy.shouldSpoolCache(totalLength)) {
158            pageToFileStream();
159        }
160        currentStream.write(b, off, len);
161    }
162
163    public void write(byte[] b) throws IOException {
164        this.totalLength += b.length;
165        if (inMemory && currentStream instanceof ByteArrayOutputStream && strategy.shouldSpoolCache(totalLength)) {
166            pageToFileStream();
167        }
168        currentStream.write(b);
169    }
170
171    public void write(int b) throws IOException {
172        this.totalLength++;
173        if (inMemory && currentStream instanceof ByteArrayOutputStream && strategy.shouldSpoolCache(totalLength)) {
174            pageToFileStream();
175        }
176        currentStream.write(b);
177    }
178
179    public InputStream getInputStream() throws IOException {
180        return (InputStream)newStreamCache();
181    }    
182
183    public InputStream getWrappedInputStream() throws IOException {
184        // The WrappedInputStream will close the CachedOutputStream when it is closed
185        return new WrappedInputStream(this, (InputStream)newStreamCache());
186    }
187
188    /**
189     * @deprecated  use {@link #newStreamCache()}
190     */
191    @Deprecated
192    public StreamCache getStreamCache() throws IOException {
193        return newStreamCache();
194    }
195
196    /**
197     * Creates a new {@link StreamCache} from the data cached in this {@link OutputStream}.
198     */
199    public StreamCache newStreamCache() throws IOException {
200        flush();
201
202        if (inMemory) {
203            if (currentStream instanceof CachedByteArrayOutputStream) {
204                return ((CachedByteArrayOutputStream) currentStream).newInputStreamCache();
205            } else {
206                throw new IllegalStateException("CurrentStream should be an instance of CachedByteArrayOutputStream but is: " + currentStream.getClass().getName());
207            }
208        } else {
209            try {
210                if (fileInputStreamCache == null) {
211                    fileInputStreamCache = new FileInputStreamCache(tempFile, ciphers, fileInputStreamCloser);
212                }
213                return fileInputStreamCache;
214            } catch (FileNotFoundException e) {
215                throw new IOException("Cached file " + tempFile + " not found", e);
216            }
217        }
218    }
219    
220    private void closeFileInputStreams() {
221        fileInputStreamCloser.close();
222        fileInputStreamCache = null;
223    } 
224
225    private void cleanUpTempFile() {
226        // cleanup temporary file
227        if (tempFile != null) {
228            FileUtil.deleteFile(tempFile);
229            tempFile = null;
230        }
231    }
232
233    private void pageToFileStream() throws IOException {
234        flush();
235
236        ByteArrayOutputStream bout = (ByteArrayOutputStream)currentStream;
237        tempFile = FileUtil.createTempFile("cos", ".tmp", strategy.getSpoolDirectory());
238
239        LOG.trace("Creating temporary stream cache file: {}", tempFile);
240
241        try {
242            currentStream = createOutputStream(tempFile);
243            bout.writeTo(currentStream);
244        } finally {
245            // ensure flag is flipped to file based
246            inMemory = false;
247        }
248    }
249
250    /**
251     * @deprecated  use {@link #getStrategyBufferSize()}
252     */
253    @Deprecated
254    public int getBufferSize() {
255        return getStrategyBufferSize();
256    }
257    
258    public int getStrategyBufferSize() {
259        return strategy.getBufferSize();
260    }
261
262    // This class will close the CachedOutputStream when it is closed
263    private static class WrappedInputStream extends InputStream {
264        private CachedOutputStream cachedOutputStream;
265        private InputStream inputStream;
266        
267        WrappedInputStream(CachedOutputStream cos, InputStream is) {
268            cachedOutputStream = cos;
269            inputStream = is;
270        }
271        
272        @Override
273        public int read() throws IOException {
274            return inputStream.read();
275        }
276        
277        @Override
278        public int available() throws IOException {
279            return inputStream.available();
280        }
281        
282        @Override
283        public void reset() throws IOException {
284            inputStream.reset();
285        }
286        
287        @Override
288        public void close() throws IOException {
289            inputStream.close();
290            cachedOutputStream.close();
291        }
292    }
293
294    private OutputStream createOutputStream(File file) throws IOException {
295        OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
296        if (ObjectHelper.isNotEmpty(strategy.getSpoolChiper())) {
297            try {
298                if (ciphers == null) {
299                    ciphers = new CipherPair(strategy.getSpoolChiper());
300                }
301            } catch (GeneralSecurityException e) {
302                throw new IOException(e.getMessage(), e);
303            }
304            out = new CipherOutputStream(out, ciphers.getEncryptor()) {
305                boolean closed;
306                public void close() throws IOException {
307                    if (!closed) {
308                        super.close();
309                        closed = true;
310                    }
311                }
312            };
313        }
314        return out;
315    }
316}