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     */
017    package org.apache.camel.converter.stream;
018    
019    import java.io.ByteArrayInputStream;
020    import java.io.ByteArrayOutputStream;
021    import java.io.File;
022    import java.io.FileNotFoundException;
023    import java.io.FileOutputStream;
024    import java.io.IOException;
025    import java.io.InputStream;
026    import java.io.OutputStream;
027    
028    import org.apache.camel.Exchange;
029    import org.apache.camel.StreamCache;
030    import org.apache.camel.support.SynchronizationAdapter;
031    import org.apache.camel.util.FileUtil;
032    import org.apache.camel.util.IOHelper;
033    import org.slf4j.Logger;
034    import org.slf4j.LoggerFactory;
035    
036    /**
037     * This output stream will store the content into a File if the stream context size is exceed the
038     * THRESHOLD which's default value is 64K. The temp file will store in the temp directory, you 
039     * can configure it by setting the TEMP_DIR property. If you don't set the TEMP_DIR property,
040     * it will choose the directory which is set by the system property of "java.io.tmpdir".
041     * You can get a cached input stream of this stream. The temp file which is created with this 
042     * output stream will be deleted when you close this output stream or the all cached 
043     * fileInputStream is closed after the exchange is completed.
044     */
045    public class CachedOutputStream extends OutputStream {
046        public static final String THRESHOLD = "CamelCachedOutputStreamThreshold";
047        public static final String TEMP_DIR = "CamelCachedOutputStreamOutputDirectory";
048        private static final transient Logger LOG = LoggerFactory.getLogger(CachedOutputStream.class);
049        
050        private OutputStream currentStream = new ByteArrayOutputStream(2048);
051        private boolean inMemory = true;
052        private int totalLength;
053        private File tempFile;
054        private FileInputStreamCache fileInputStreamCache;
055    
056        private long threshold = 64 * 1024;
057        private File outputDir;
058        
059        public CachedOutputStream(Exchange exchange) {
060            this(exchange, true);
061        }
062    
063        public CachedOutputStream(Exchange exchange, boolean closedOnCompletion) {
064            String hold = exchange.getContext().getProperties().get(THRESHOLD);
065            String dir = exchange.getContext().getProperties().get(TEMP_DIR);
066            if (hold != null) {
067                this.threshold = exchange.getContext().getTypeConverter().convertTo(Long.class, hold);
068            }
069            if (dir != null) {
070                this.outputDir = exchange.getContext().getTypeConverter().convertTo(File.class, dir);
071            }
072            
073            if (closedOnCompletion) {
074                // add on completion so we can cleanup after the exchange is done such as deleting temporary files
075                exchange.addOnCompletion(new SynchronizationAdapter() {
076                    @Override
077                    public void onDone(Exchange exchange) {
078                        try {
079                            if (fileInputStreamCache != null) {
080                                fileInputStreamCache.close();
081                            }
082                            close();
083                        } catch (Exception e) {
084                            LOG.warn("Error deleting temporary cache file: " + tempFile, e);
085                        }
086                    }
087        
088                    @Override
089                    public String toString() {
090                        return "OnCompletion[CachedOutputStream]";
091                    }
092                });
093            }
094        }
095    
096        public void flush() throws IOException {
097            currentStream.flush();       
098        }
099    
100        public void close() throws IOException {
101            currentStream.close();
102            cleanUpTempFile();
103        }
104    
105        public boolean equals(Object obj) {
106            return currentStream.equals(obj);
107        }
108    
109        public int hashCode() {
110            return currentStream.hashCode();
111        }
112    
113        public String toString() {
114            return "CachedOutputStream[size: " + totalLength + "]";
115        }
116    
117        public void write(byte[] b, int off, int len) throws IOException {
118            this.totalLength += len;
119            if (threshold > 0 && inMemory && totalLength > threshold && currentStream instanceof ByteArrayOutputStream) {
120                pageToFileStream();
121            }
122            currentStream.write(b, off, len);
123        }
124    
125        public void write(byte[] b) throws IOException {
126            this.totalLength += b.length;
127            if (threshold > 0 && inMemory && totalLength > threshold && currentStream instanceof ByteArrayOutputStream) {
128                pageToFileStream();
129            }
130            currentStream.write(b);
131        }
132    
133        public void write(int b) throws IOException {
134            this.totalLength++;
135            if (threshold > 0 && inMemory && totalLength > threshold && currentStream instanceof ByteArrayOutputStream) {
136                pageToFileStream();
137            }
138            currentStream.write(b);
139        }
140    
141        public InputStream getInputStream() throws IOException {
142            flush();
143    
144            if (inMemory) {
145                if (currentStream instanceof ByteArrayOutputStream) {
146                    return new ByteArrayInputStream(((ByteArrayOutputStream) currentStream).toByteArray());
147                } else {
148                    throw new IllegalStateException("CurrentStream should be an instance of ByteArrayOutputStream but is: " + currentStream.getClass().getName());
149                }
150            } else {
151                try {
152                    if (fileInputStreamCache == null) {
153                        fileInputStreamCache = new FileInputStreamCache(tempFile);
154                    }
155                    return fileInputStreamCache;
156                } catch (FileNotFoundException e) {
157                    throw new IOException("Cached file " + tempFile + " not found", e);
158                }
159            }
160        }    
161        
162        public InputStream getWrappedInputStream() throws IOException {
163            // The WrappedInputStream will close the CachedOuputStream when it is closed
164            return new WrappedInputStream(this, getInputStream());
165        }
166    
167    
168        public StreamCache getStreamCache() throws IOException {
169            flush();
170    
171            if (inMemory) {
172                if (currentStream instanceof ByteArrayOutputStream) {
173                    return new InputStreamCache(((ByteArrayOutputStream) currentStream).toByteArray());
174                } else {
175                    throw new IllegalStateException("CurrentStream should be an instance of ByteArrayOutputStream but is: " + currentStream.getClass().getName());
176                }
177            } else {
178                try {
179                    if (fileInputStreamCache == null) {
180                        fileInputStreamCache = new FileInputStreamCache(tempFile);
181                    }
182                    return fileInputStreamCache;
183                } catch (FileNotFoundException e) {
184                    throw new IOException("Cached file " + tempFile + " not found", e);
185                }
186            }
187        }
188    
189        private void cleanUpTempFile() {
190            // cleanup temporary file
191            if (tempFile != null) {
192                FileUtil.deleteFile(tempFile);
193                tempFile = null;
194            }
195        }
196    
197        private void pageToFileStream() throws IOException {
198            flush();
199    
200            ByteArrayOutputStream bout = (ByteArrayOutputStream)currentStream;
201            if (outputDir == null) {
202                tempFile = FileUtil.createTempFile("cos", ".tmp");
203            } else {
204                tempFile = FileUtil.createTempFile("cos", ".tmp", outputDir);
205            }
206    
207            LOG.trace("Creating temporary stream cache file: {}", tempFile);
208    
209            try {
210                currentStream = IOHelper.buffered(new FileOutputStream(tempFile));
211                bout.writeTo(currentStream);
212            } finally {
213                // ensure flag is flipped to file based
214                inMemory = false;
215            }
216        }
217        
218        // This class will close the CachedOutputStream when it is closed
219        private static class WrappedInputStream extends InputStream {
220            private CachedOutputStream cachedOutputStream;
221            private InputStream inputStream;
222            
223            WrappedInputStream(CachedOutputStream cos, InputStream is) {
224                cachedOutputStream = cos;
225                inputStream = is;
226            }
227            
228            @Override
229            public int read() throws IOException {
230                return inputStream.read();
231            }
232            
233            @Override
234            public int available() throws IOException {
235                return inputStream.available();
236            }
237            
238            @Override
239            public void reset() throws IOException {
240                inputStream.reset();
241            }
242            
243            @Override
244            public void close() throws IOException {
245                inputStream.close();
246                cachedOutputStream.close();
247            }
248        }
249    
250    }