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