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.util;
018    
019    import java.io.BufferedInputStream;
020    import java.io.BufferedOutputStream;
021    import java.io.BufferedReader;
022    import java.io.BufferedWriter;
023    import java.io.ByteArrayInputStream;
024    import java.io.Closeable;
025    import java.io.FileOutputStream;
026    import java.io.IOException;
027    import java.io.InputStream;
028    import java.io.InputStreamReader;
029    import java.io.OutputStream;
030    import java.io.Reader;
031    import java.io.UnsupportedEncodingException;
032    import java.io.Writer;
033    import java.nio.channels.FileChannel;
034    import java.nio.charset.Charset;
035    import java.nio.charset.UnsupportedCharsetException;
036    
037    import org.apache.camel.Exchange;
038    import org.slf4j.Logger;
039    import org.slf4j.LoggerFactory;
040    
041    /**
042     * IO helper class.
043     *
044     * @version 
045     */
046    public 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    }