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.FileInputStream;
024    import java.io.FileNotFoundException;
025    import java.io.FileOutputStream;
026    import java.io.IOException;
027    import java.io.InputStream;
028    import java.io.OutputStream;
029    import java.util.ArrayList;
030    import java.util.List;
031    import java.util.Map;
032    
033    import org.apache.camel.converter.IOConverter;
034    import org.apache.camel.util.FileUtil;
035    import org.apache.camel.util.IOHelper;
036    
037    /**
038     * This output stream will store the content into a File if the stream context size is exceed the
039     * THRESHOLD which's default value is 64K. The temp file will store in the temp directory, you 
040     * can configure it by setting the TEMP_DIR property. If you don't set the TEMP_DIR property,
041     * it will choice the directory which is set by the system property of "java.io.tmpdir".
042     * You can get a cached input stream of this stream. The temp file which is created with this 
043     * output stream will be deleted when you close this output stream or the cached inputStream.
044     */
045    public class CachedOutputStream extends OutputStream {
046        public static final String THRESHOLD = "CamelCachedOutputStreamThreshold";
047        public static final String TEMP_DIR = "CamelCachedOutputStreamOutputDirectory";
048       
049        protected boolean outputLocked;
050        protected OutputStream currentStream;
051    
052        private long threshold = 64 * 1024;
053    
054        private int totalLength;
055    
056        private boolean inmem;
057    
058        private File tempFile;
059    
060        private File outputDir;   
061        
062        private List<Object> streamList = new ArrayList<Object>();
063    
064        
065        public CachedOutputStream() {
066            currentStream = new ByteArrayOutputStream(2048);
067            inmem = true;
068        }
069    
070        public CachedOutputStream(long threshold) {
071            this();
072            this.threshold = threshold;        
073        }
074        
075        public CachedOutputStream(Map<String, String> properties) {
076            this();
077            String value = properties.get(THRESHOLD);
078            if (value != null) {
079                int i = Integer.parseInt(value);
080                if (i > 0) {
081                    threshold = i;
082                }
083            }
084            value = properties.get(TEMP_DIR);
085            if (value != null) {
086                File f = new File(value);
087                if (f.exists() && f.isDirectory()) {
088                    outputDir = f;
089                } else {
090                    outputDir = null;
091                }
092            } else {
093                outputDir = null;
094            }        
095        }
096    
097        /**
098         * Perform any actions required on stream flush (freeze headers, reset
099         * output stream ... etc.)
100         */
101        protected void doFlush() throws IOException {
102            
103        }
104    
105        public void flush() throws IOException {
106            currentStream.flush();       
107            doFlush();
108        }
109    
110        /**
111         * Perform any actions required on stream closure (handle response etc.)
112         */
113        protected void doClose() throws IOException {
114            
115        }
116        
117        /**
118         * Perform any actions required after stream closure (close the other related stream etc.)
119         */
120        protected void postClose() throws IOException {
121            
122        }
123    
124        /**
125         * Locks the output stream to prevent additional writes, but maintains
126         * a pointer to it so an InputStream can be obtained
127         * @throws IOException
128         */
129        public void lockOutputStream() throws IOException {
130            currentStream.flush();
131            outputLocked = true;
132            streamList.remove(currentStream);
133        }
134        
135        public void close() throws IOException {
136            currentStream.flush();        
137            doClose();
138            currentStream.close();
139            maybeDeleteTempFile(currentStream);
140            postClose();
141        }
142    
143        public boolean equals(Object obj) {
144            return currentStream.equals(obj);
145        }
146    
147        /**
148         * Replace the original stream with the new one, optionally copying the content of the old one
149         * into the new one.
150         * When with Attachment, needs to replace the xml writer stream with the stream used by
151         * AttachmentSerializer or copy the cached output stream to the "real"
152         * output stream, i.e. onto the wire.
153         * 
154         * @param out the new output stream
155         * @param copyOldContent flag indicating if the old content should be copied
156         * @throws IOException
157         */
158        public void resetOut(OutputStream out, boolean copyOldContent) throws IOException {
159            if (out == null) {
160                out = new ByteArrayOutputStream();
161            }
162    
163            if (currentStream instanceof CachedOutputStream) {
164                CachedOutputStream ac = (CachedOutputStream) currentStream;
165                InputStream in = ac.getInputStream();
166                IOHelper.copyAndCloseInput(in, out);
167            } else {
168                if (inmem) {
169                    if (currentStream instanceof ByteArrayOutputStream) {
170                        ByteArrayOutputStream byteOut = (ByteArrayOutputStream) currentStream;
171                        if (copyOldContent && byteOut.size() > 0) {
172                            byteOut.writeTo(out);
173                        }
174                    } else {
175                        throw new IOException("Unknown format of currentStream");
176                    }
177                } else {
178                    // read the file
179                    currentStream.close();
180                    FileInputStream fin = new FileInputStream(tempFile);
181                    if (copyOldContent) {
182                        IOHelper.copyAndCloseInput(fin, out);
183                    }
184                    streamList.remove(currentStream);
185                    tempFile.delete();
186                    tempFile = null;
187                    inmem = true;
188                }
189            }
190            currentStream = out;
191            outputLocked = false;
192        }
193    
194        public static void copyStream(InputStream in, OutputStream out, int bufferSize) throws IOException {
195            IOHelper.copyAndCloseInput(in, out, bufferSize);
196        }
197    
198        public int size() {
199            return totalLength;
200        }
201        
202        public byte[] getBytes() throws IOException {
203            flush();
204            if (inmem) {
205                if (currentStream instanceof ByteArrayOutputStream) {
206                    return ((ByteArrayOutputStream)currentStream).toByteArray();
207                } else {
208                    throw new IOException("Unknown format of currentStream");
209                }
210            } else {
211                // read the file
212                FileInputStream fin = new FileInputStream(tempFile);
213                return IOConverter.toBytes(fin);
214            }
215        }
216        
217        public void writeCacheTo(OutputStream out) throws IOException {
218            flush();
219            if (inmem) {
220                if (currentStream instanceof ByteArrayOutputStream) {
221                    ((ByteArrayOutputStream)currentStream).writeTo(out);
222                } else {
223                    throw new IOException("Unknown format of currentStream");
224                }
225            } else {
226                // read the file
227                FileInputStream fin = new FileInputStream(tempFile);
228                IOHelper.copyAndCloseInput(fin, out);
229            }
230        }
231        
232        
233        public void writeCacheTo(StringBuilder out, int limit) throws IOException {
234            flush();
235            if (totalLength < limit
236                || limit == -1) {
237                writeCacheTo(out);
238                return;
239            }
240            
241            int count = 0;
242            if (inmem) {
243                if (currentStream instanceof ByteArrayOutputStream) {
244                    byte bytes[] = ((ByteArrayOutputStream)currentStream).toByteArray();
245                    out.append(IOHelper.newStringFromBytes(bytes, 0, limit));
246                } else {
247                    throw new IOException("Unknown format of currentStream");
248                }
249            } else {
250                // read the file
251                FileInputStream fin = new FileInputStream(tempFile);
252                byte bytes[] = new byte[1024];
253                int x = fin.read(bytes);
254                while (x != -1) {
255                    if ((count + x) > limit) {
256                        x = limit - count;
257                    }
258                    out.append(IOHelper.newStringFromBytes(bytes, 0, x));
259                    count += x;
260                    
261                    if (count >= limit) {
262                        x = -1;
263                    } else {
264                        x = fin.read(bytes);
265                    }
266                }
267                fin.close();
268            }
269        }
270        public void writeCacheTo(StringBuilder out) throws IOException {
271            flush();
272            if (inmem) {
273                if (currentStream instanceof ByteArrayOutputStream) {
274                    byte[] bytes = ((ByteArrayOutputStream)currentStream).toByteArray();
275                    out.append(IOHelper.newStringFromBytes(bytes));
276                } else {
277                    throw new IOException("Unknown format of currentStream");
278                }
279            } else {
280                // read the file
281                FileInputStream fin = new FileInputStream(tempFile);
282                byte bytes[] = new byte[1024];
283                int x = fin.read(bytes);
284                while (x != -1) {
285                    out.append(IOHelper.newStringFromBytes(bytes, 0, x));
286                    x = fin.read(bytes);
287                }
288                fin.close();
289            }
290        }    
291        
292    
293        /**
294         * @return the underlying output stream
295         */
296        public OutputStream getOut() {
297            return currentStream;
298        }
299    
300        public int hashCode() {
301            return currentStream.hashCode();
302        }
303    
304        public String toString() {
305            StringBuilder builder = new StringBuilder().append("[")
306                .append(CachedOutputStream.class.getName())
307                .append(" Content: ");
308            try {
309                writeCacheTo(builder);
310            } catch (IOException e) {
311                //ignore
312            }
313            return builder.append("]").toString();
314        }
315    
316        protected void onWrite() throws IOException {
317            
318        }
319    
320        public void write(byte[] b, int off, int len) throws IOException {
321            if (!outputLocked) {
322                onWrite();
323                this.totalLength += len;
324                if (inmem && totalLength > threshold && currentStream instanceof ByteArrayOutputStream) {
325                    createFileOutputStream();
326                }
327                currentStream.write(b, off, len);
328            }
329        }
330    
331        public void write(byte[] b) throws IOException {
332            if (!outputLocked) {
333                onWrite();
334                this.totalLength += b.length;
335                if (inmem && totalLength > threshold && currentStream instanceof ByteArrayOutputStream) {
336                    createFileOutputStream();
337                }
338                currentStream.write(b);
339            }
340        }
341    
342        public void write(int b) throws IOException {
343            if (!outputLocked) {
344                onWrite();
345                this.totalLength++;
346                if (inmem && totalLength > threshold && currentStream instanceof ByteArrayOutputStream) {
347                    createFileOutputStream();
348                }
349                currentStream.write(b);
350            }
351        }
352    
353        private void createFileOutputStream() throws IOException {
354            ByteArrayOutputStream bout = (ByteArrayOutputStream)currentStream;
355            if (outputDir == null) {
356                tempFile = FileUtil.createTempFile("cos", "tmp");
357            } else {
358                tempFile = FileUtil.createTempFile("cos", "tmp", outputDir, false);
359            }
360            
361            currentStream = new BufferedOutputStream(new FileOutputStream(tempFile));
362            bout.writeTo(currentStream);
363            inmem = false;
364            streamList.add(currentStream);
365        }
366    
367        public File getTempFile() {
368            return tempFile != null && tempFile.exists() ? tempFile : null;
369        }
370    
371        public InputStream getInputStream() throws IOException {
372            flush();
373            if (inmem) {
374                if (currentStream instanceof ByteArrayOutputStream) {
375                    return new ByteArrayInputStream(((ByteArrayOutputStream) currentStream).toByteArray());
376                } else {
377                    return null;
378                }
379            } else {
380                try {
381                    FileInputStream fileInputStream = new FileInputStream(tempFile) {
382                        public void close() throws IOException {
383                            super.close();
384                            maybeDeleteTempFile(this);
385                        }
386                    };
387                    streamList.add(fileInputStream);
388                    return fileInputStream;
389                } catch (FileNotFoundException e) {
390                    throw new IOException("Cached file was deleted, " + e.toString());
391                }
392            }
393        }
394        
395        public StreamCache getStreamCache() throws IOException {
396            flush();
397            if (inmem) {
398                if (currentStream instanceof ByteArrayOutputStream) {
399                    return new InputStreamCache(((ByteArrayOutputStream) currentStream).toByteArray());
400                } else {
401                    return null;
402                }
403            } else {
404                try {
405                    FileInputStreamCache fileInputStream = new FileInputStreamCache(tempFile, this);
406                    return fileInputStream;
407                } catch (FileNotFoundException e) {
408                    throw new IOException("Cached file was deleted, " + e.toString());
409                }
410            }
411        }
412        
413        private void maybeDeleteTempFile(Object stream) {        
414            streamList.remove(stream);        
415            if (!inmem && tempFile != null && streamList.isEmpty()) {            
416                tempFile.delete();
417                tempFile = null;
418                currentStream = new ByteArrayOutputStream(1024);
419                inmem = true;
420            }
421        }
422    
423        public void setOutputDir(File outputDir) throws IOException {
424            this.outputDir = outputDir;
425        }
426        public void setThreshold(long threshold) {
427            this.threshold = threshold;
428        }
429    
430    }