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.util;
018
019import java.io.BufferedInputStream;
020import java.io.BufferedOutputStream;
021import java.io.BufferedReader;
022import java.io.BufferedWriter;
023import java.io.ByteArrayInputStream;
024import java.io.Closeable;
025import java.io.FileOutputStream;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.InputStreamReader;
029import java.io.OutputStream;
030import java.io.Reader;
031import java.io.UnsupportedEncodingException;
032import java.io.Writer;
033import java.nio.channels.FileChannel;
034import java.nio.charset.Charset;
035import java.nio.charset.UnsupportedCharsetException;
036
037import org.apache.camel.Exchange;
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041/**
042 * IO helper class.
043 *
044 * @version 
045 */
046public final class IOHelper {
047
048    public static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
049
050    private static final Logger LOG = LoggerFactory.getLogger(IOHelper.class);
051    private static final Charset UTF8_CHARSET = Charset.forName("UTF-8");
052
053    private IOHelper() {
054        // Utility Class
055    }
056    
057    /**
058     * Use this function instead of new String(byte[]) to avoid surprises from non-standard default encodings.
059     */
060    public static String newStringFromBytes(byte[] bytes) {
061        try {
062            return new String(bytes, UTF8_CHARSET.name());
063        } catch (UnsupportedEncodingException e) {
064            throw new RuntimeException("Impossible failure: Charset.forName(\"UTF-8\") returns invalid name.", e);
065        }
066    }
067
068    /**
069     * Use this function instead of new String(byte[], int, int) 
070     * to avoid surprises from non-standard default encodings.
071     */
072    public static String newStringFromBytes(byte[] bytes, int start, int length) {
073        try {
074            return new String(bytes, start, length, UTF8_CHARSET.name());
075        } catch (UnsupportedEncodingException e) {
076            throw new RuntimeException("Impossible failure: Charset.forName(\"UTF-8\") returns invalid name.", e);
077        }
078    }
079
080    /**
081     * Wraps the passed <code>in</code> into a {@link BufferedInputStream}
082     * object and returns that. If the passed <code>in</code> is already an
083     * instance of {@link BufferedInputStream} returns the same passed
084     * <code>in</code> reference as is (avoiding double wrapping).
085     * 
086     * @param in the wrapee to be used for the buffering support
087     * @return the passed <code>in</code> decorated through a
088     *         {@link BufferedInputStream} object as wrapper
089     */
090    public static BufferedInputStream buffered(InputStream in) {
091        ObjectHelper.notNull(in, "in");
092        return (in instanceof BufferedInputStream) ? (BufferedInputStream)in : new BufferedInputStream(in);
093    }
094
095    /**
096     * Wraps the passed <code>out</code> into a {@link BufferedOutputStream}
097     * object and returns that. If the passed <code>out</code> is already an
098     * instance of {@link BufferedOutputStream} returns the same passed
099     * <code>out</code> reference as is (avoiding double wrapping).
100     * 
101     * @param out the wrapee to be used for the buffering support
102     * @return the passed <code>out</code> decorated through a
103     *         {@link BufferedOutputStream} object as wrapper
104     */
105    public static BufferedOutputStream buffered(OutputStream out) {
106        ObjectHelper.notNull(out, "out");
107        return (out instanceof BufferedOutputStream) ? (BufferedOutputStream)out : new BufferedOutputStream(out);
108    }
109
110    /**
111     * Wraps the passed <code>reader</code> into a {@link BufferedReader} object
112     * and returns that. If the passed <code>reader</code> is already an
113     * instance of {@link BufferedReader} returns the same passed
114     * <code>reader</code> reference as is (avoiding double wrapping).
115     * 
116     * @param reader the wrapee to be used for the buffering support
117     * @return the passed <code>reader</code> decorated through a
118     *         {@link BufferedReader} object as wrapper
119     */
120    public static BufferedReader buffered(Reader reader) {
121        ObjectHelper.notNull(reader, "reader");
122        return (reader instanceof BufferedReader) ? (BufferedReader)reader : new BufferedReader(reader);
123    }
124
125    /**
126     * Wraps the passed <code>writer</code> into a {@link BufferedWriter} object
127     * and returns that. If the passed <code>writer</code> is already an
128     * instance of {@link BufferedWriter} returns the same passed
129     * <code>writer</code> reference as is (avoiding double wrapping).
130     * 
131     * @param writer the wrapee to be used for the buffering support
132     * @return the passed <code>writer</code> decorated through a
133     *         {@link BufferedWriter} object as wrapper
134     */
135    public static BufferedWriter buffered(Writer writer) {
136        ObjectHelper.notNull(writer, "writer");
137        return (writer instanceof BufferedWriter) ? (BufferedWriter)writer : new BufferedWriter(writer);
138    }
139
140    /**
141     * A factory method which creates an {@link IOException} from the given
142     * exception and message
143     *
144     * @deprecated IOException support nested exception in Java 1.6. Will be removed in Camel 3.0
145     */
146    @Deprecated
147    public static IOException createIOException(Throwable cause) {
148        return createIOException(cause.getMessage(), cause);
149    }
150
151    /**
152     * A factory method which creates an {@link IOException} from the given
153     * exception and message
154     *
155     * @deprecated IOException support nested exception in Java 1.6. Will be removed in Camel 3.0
156     */
157    @Deprecated
158    public static IOException createIOException(String message, Throwable cause) {
159        IOException answer = new IOException(message);
160        answer.initCause(cause);
161        return answer;
162    }
163
164    public static int copy(InputStream input, OutputStream output) throws IOException {
165        return copy(input, output, DEFAULT_BUFFER_SIZE);
166    }
167
168    public static int copy(final InputStream input, final OutputStream output, int bufferSize) throws IOException {
169        return copy(input, output, bufferSize, false);
170    }
171
172    public static int copy(final InputStream input, final OutputStream output, int bufferSize, boolean flushOnEachWrite) throws IOException {
173        if (input instanceof ByteArrayInputStream) {
174            // optimized for byte array as we only need the max size it can be
175            input.mark(0);
176            input.reset();
177            bufferSize = input.available();
178        } else {
179            int avail = input.available();
180            if (avail > bufferSize) {
181                bufferSize = avail;
182            }
183        }
184
185        if (bufferSize > 262144) {
186            // upper cap to avoid buffers too big
187            bufferSize = 262144;
188        }
189
190        if (LOG.isTraceEnabled()) {
191            LOG.trace("Copying InputStream: {} -> OutputStream: {} with buffer: {} and flush on each write {}",
192                    new Object[]{input, output, bufferSize, flushOnEachWrite});
193        }
194
195        final byte[] buffer = new byte[bufferSize];
196        int n = input.read(buffer);
197        int total = 0;
198        while (-1 != n) {
199            output.write(buffer, 0, n);
200            if (flushOnEachWrite) {
201                output.flush();
202            }
203            total += n;
204            n = input.read(buffer);
205        }
206        if (!flushOnEachWrite) {
207            // flush at end, if we didn't do it during the writing
208            output.flush();
209        }
210        return total;
211    }
212    
213    public static void copyAndCloseInput(InputStream input, OutputStream output) throws IOException {
214        copyAndCloseInput(input, output, DEFAULT_BUFFER_SIZE);
215    }
216    
217    public static void copyAndCloseInput(InputStream input, OutputStream output, int bufferSize) throws IOException {
218        copy(input, output, bufferSize);
219        close(input, null, LOG);
220    }
221
222    public static int copy(final Reader input, final Writer output, int bufferSize) throws IOException {
223        final char[] buffer = new char[bufferSize];
224        int n = input.read(buffer);
225        int total = 0;
226        while (-1 != n) {
227            output.write(buffer, 0, n);
228            total += n;
229            n = input.read(buffer);
230        }
231        output.flush();
232        return total;
233    }
234
235    /**
236     * Forces any updates to this channel's file to be written to the storage device that contains it.
237     *
238     * @param channel the file channel
239     * @param name the name of the resource
240     * @param log the log to use when reporting warnings, will use this class's own {@link Logger} if <tt>log == null</tt>
241     */
242    public static void force(FileChannel channel, String name, Logger log) {
243        try {
244            if (channel != null) {
245                channel.force(true);
246            }
247        } catch (Exception e) {
248            if (log == null) {
249                // then fallback to use the own Logger
250                log = LOG;
251            }
252            if (name != null) {
253                log.warn("Cannot force FileChannel: " + name + ". Reason: " + e.getMessage(), e);
254            } else {
255                log.warn("Cannot force FileChannel. Reason: " + e.getMessage(), e);
256            }
257        }
258    }
259
260    /**
261     * Forces any updates to a FileOutputStream be written to the storage device that contains it.
262     *
263     * @param os the file output stream
264     * @param name the name of the resource
265     * @param log the log to use when reporting warnings, will use this class's own {@link Logger} if <tt>log == null</tt>
266     */
267    public static void force(FileOutputStream os, String name, Logger log) {
268        try {
269            if (os != null) {
270                os.getFD().sync();
271            }
272        } catch (Exception e) {
273            if (log == null) {
274                // then fallback to use the own Logger
275                log = LOG;
276            }
277            if (name != null) {
278                log.warn("Cannot sync FileDescriptor: " + name + ". Reason: " + e.getMessage(), e);
279            } else {
280                log.warn("Cannot sync FileDescriptor. Reason: " + e.getMessage(), e);
281            }
282        }
283    }
284
285    /**
286     * Closes the given writer, logging any closing exceptions to the given log.
287     * An associated FileOutputStream can optionally be forced to disk.
288     *
289     * @param writer the writer to close
290     * @param os an underlying FileOutputStream that will to be forced to disk according to the the force parameter
291     * @param name the name of the resource
292     * @param log the log to use when reporting warnings, will use this class's own {@link Logger} if <tt>log == null</tt>
293     * @param force forces the FileOutputStream to disk
294     */
295    public static void close(Writer writer, FileOutputStream os, String name, Logger log, boolean force) {
296        if (writer != null && force) {
297            // flush the writer prior to syncing the FD
298            try {
299                writer.flush();
300            } catch (Exception e) {
301                if (log == null) {
302                    // then fallback to use the own Logger
303                    log = LOG;
304                }
305                if (name != null) {
306                    log.warn("Cannot flush Writer: " + name + ". Reason: " + e.getMessage(), e);
307                } else {
308                    log.warn("Cannot flush Writer. Reason: " + e.getMessage(), e);
309                }
310            }
311            force(os, name, log);
312        }
313        close(writer, name, log);
314    }
315
316    /**
317     * Closes the given resource if it is available, logging any closing exceptions to the given log.
318     *
319     * @param closeable the object to close
320     * @param name the name of the resource
321     * @param log the log to use when reporting closure warnings, will use this class's own {@link Logger} if <tt>log == null</tt>
322     */
323    public static void close(Closeable closeable, String name, Logger log) {
324        if (closeable != null) {
325            try {
326                closeable.close();
327            } catch (IOException e) {
328                if (log == null) {
329                    // then fallback to use the own Logger
330                    log = LOG;
331                }
332                if (name != null) {
333                    log.warn("Cannot close: " + name + ". Reason: " + e.getMessage(), e);
334                } else {
335                    log.warn("Cannot close. Reason: " + e.getMessage(), e);
336                }
337            }
338        }
339    }
340    
341    /**
342     * Closes the given resource if it is available and don't catch the exception
343     *
344     * @param closeable the object to close
345     * @throws IOException
346      */
347    public static void closeWithException(Closeable closeable) throws IOException {
348        if (closeable != null) {
349            try {
350                closeable.close();
351            } catch (IOException e) {
352                // don't catch the exception here
353                throw e;
354            }
355        }
356    }
357
358    /**
359     * Closes the given channel if it is available, logging any closing exceptions to the given log.
360     * The file's channel can optionally be forced to disk.
361     *
362     * @param channel the file channel
363     * @param name the name of the resource
364     * @param log the log to use when reporting warnings, will use this class's own {@link Logger} if <tt>log == null</tt>
365     * @param force forces the file channel to disk
366     */
367    public static void close(FileChannel channel, String name, Logger log, boolean force) {
368        if (force) {
369            force(channel, name, log);
370        }
371        close(channel, name, log);
372    }
373
374    /**
375     * Closes the given resource if it is available.
376     *
377     * @param closeable the object to close
378     * @param name the name of the resource
379     */
380    public static void close(Closeable closeable, String name) {
381        close(closeable, name, LOG);
382    }
383
384    /**
385     * Closes the given resource if it is available.
386     *
387     * @param closeable the object to close
388     */
389    public static void close(Closeable closeable) {
390        close(closeable, null, LOG);
391    }
392
393    /**
394     * Closes the given resources if they are available.
395     * 
396     * @param closeables the objects to close
397     */
398    public static void close(Closeable... closeables) {
399        for (Closeable closeable : closeables) {
400            close(closeable);
401        }
402    }
403
404    public static void validateCharset(String charset) throws UnsupportedCharsetException {
405        if (charset != null) {
406            if (Charset.isSupported(charset)) {
407                Charset.forName(charset);
408                return;
409            }
410        }
411        throw new UnsupportedCharsetException(charset);
412    }
413
414    /**
415     * This method will take off the quotes and double quotes of the charset
416     */
417    public static String normalizeCharset(String charset) {
418        if (charset != null) {
419            String answer = charset.trim();
420            if (answer.startsWith("'") || answer.startsWith("\"")) {
421                answer = answer.substring(1);
422            }
423            if (answer.endsWith("'") || answer.endsWith("\"")) {
424                answer = answer.substring(0, answer.length() - 1);
425            }
426            return answer.trim();
427        } else {
428            return null;
429        }
430    }
431
432    /**
433     * @see #getCharsetName(org.apache.camel.Exchange, boolean)
434     */
435    public static String getCharsetName(Exchange exchange) {
436        return getCharsetName(exchange, true);
437    }
438
439    /**
440     * Gets the charset name if set as header or property {@link Exchange#CHARSET_NAME}.
441     * <b>Notice:</b> The lookup from the header has priority over the property.
442     *
443     * @param exchange  the exchange
444     * @param useDefault should we fallback and use JVM default charset if no property existed?
445     * @return the charset, or <tt>null</tt> if no found
446     */
447    public static String getCharsetName(Exchange exchange, boolean useDefault) {
448        if (exchange != null) {
449            // header takes precedence
450            String charsetName = exchange.getIn().getHeader(Exchange.CHARSET_NAME, String.class);
451            if (charsetName == null) {
452                charsetName = exchange.getProperty(Exchange.CHARSET_NAME, String.class);
453            }
454            if (charsetName != null) {
455                return IOHelper.normalizeCharset(charsetName);
456            }
457        }
458        if (useDefault) {
459            return getDefaultCharsetName();
460        } else {
461            return null;
462        }
463    }
464    
465    private static String getDefaultCharsetName() {
466        return ObjectHelper.getSystemProperty(Exchange.DEFAULT_CHARSET_PROPERTY, "UTF-8");
467    }
468
469    /**
470     * Loads the entire stream into memory as a String and returns it.
471     * <p/>
472     * <b>Notice:</b> This implementation appends a <tt>\n</tt> as line
473     * terminator at the of the text.
474     * <p/>
475     * Warning, don't use for crazy big streams :)
476     */
477    public static String loadText(InputStream in) throws IOException {
478        StringBuilder builder = new StringBuilder();
479        InputStreamReader isr = new InputStreamReader(in);
480        try {
481            BufferedReader reader = buffered(isr);
482            while (true) {
483                String line = reader.readLine();
484                if (line != null) {
485                    builder.append(line);
486                    builder.append("\n");
487                } else {
488                    break;
489                }
490            }
491            return builder.toString();
492        } finally {
493            close(isr, in);
494        }
495    }
496    
497    /**
498     * Get the charset name from the content type string
499     * @param contentType
500     * @return the charset name, or <tt>UTF-8</tt> if no found
501     */
502    public static String getCharsetNameFromContentType(String contentType) {
503        String[] values = contentType.split(";"); 
504        String charset = "";
505
506        for (String value : values) {
507            value = value.trim();
508            if (value.toLowerCase().startsWith("charset=")) {
509                // Take the charset name
510                charset = value.substring(8);
511            }
512        }
513        if ("".equals(charset)) {
514            charset = "UTF-8"; 
515        }
516        return IOHelper.normalizeCharset(charset);
517
518    }
519}