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