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