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.File;
026import java.io.FileInputStream;
027import java.io.FileNotFoundException;
028import java.io.FileOutputStream;
029import java.io.IOException;
030import java.io.InputStream;
031import java.io.InputStreamReader;
032import java.io.OutputStream;
033import java.io.OutputStreamWriter;
034import java.io.Reader;
035import java.io.UnsupportedEncodingException;
036import java.io.Writer;
037import java.nio.ByteBuffer;
038import java.nio.CharBuffer;
039import java.nio.channels.FileChannel;
040import java.nio.charset.Charset;
041import java.nio.charset.UnsupportedCharsetException;
042import java.util.function.Supplier;
043
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047/**
048 * IO helper class.
049 */
050public final class IOHelper {
051
052    public static Supplier<Charset> defaultCharset = Charset::defaultCharset;
053
054    public static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
055
056    private static final Logger LOG = LoggerFactory.getLogger(IOHelper.class);
057    private static final Charset UTF8_CHARSET = Charset.forName("UTF-8");
058
059    // allows to turn on backwards compatible to turn off regarding the first
060    // read byte with value zero (0b0) as EOL.
061    // See more at CAMEL-11672
062    private static final boolean ZERO_BYTE_EOL_ENABLED = "true".equalsIgnoreCase(System.getProperty("camel.zeroByteEOLEnabled", "true"));
063
064    private IOHelper() {
065        // Utility Class
066    }
067
068    /**
069     * Use this function instead of new String(byte[]) to avoid surprises from
070     * non-standard default encodings.
071     */
072    public static String newStringFromBytes(byte[] bytes) {
073        try {
074            return new String(bytes, 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     * Use this function instead of new String(byte[], int, int) to avoid
082     * surprises from non-standard default encodings.
083     */
084    public static String newStringFromBytes(byte[] bytes, int start, int length) {
085        try {
086            return new String(bytes, start, length, UTF8_CHARSET.name());
087        } catch (UnsupportedEncodingException e) {
088            throw new RuntimeException("Impossible failure: Charset.forName(\"UTF-8\") returns invalid name.", e);
089        }
090    }
091
092    /**
093     * Wraps the passed <code>in</code> into a {@link BufferedInputStream}
094     * object and returns that. If the passed <code>in</code> is already an
095     * instance of {@link BufferedInputStream} returns the same passed
096     * <code>in</code> reference as is (avoiding double wrapping).
097     * 
098     * @param in the wrapee to be used for the buffering support
099     * @return the passed <code>in</code> decorated through a
100     *         {@link BufferedInputStream} object as wrapper
101     */
102    public static BufferedInputStream buffered(InputStream in) {
103        ObjectHelper.notNull(in, "in");
104        return (in instanceof BufferedInputStream) ? (BufferedInputStream)in : new BufferedInputStream(in);
105    }
106
107    /**
108     * Wraps the passed <code>out</code> into a {@link BufferedOutputStream}
109     * object and returns that. If the passed <code>out</code> is already an
110     * instance of {@link BufferedOutputStream} returns the same passed
111     * <code>out</code> reference as is (avoiding double wrapping).
112     * 
113     * @param out the wrapee to be used for the buffering support
114     * @return the passed <code>out</code> decorated through a
115     *         {@link BufferedOutputStream} object as wrapper
116     */
117    public static BufferedOutputStream buffered(OutputStream out) {
118        ObjectHelper.notNull(out, "out");
119        return (out instanceof BufferedOutputStream) ? (BufferedOutputStream)out : new BufferedOutputStream(out);
120    }
121
122    /**
123     * Wraps the passed <code>reader</code> into a {@link BufferedReader} object
124     * and returns that. If the passed <code>reader</code> is already an
125     * instance of {@link BufferedReader} returns the same passed
126     * <code>reader</code> reference as is (avoiding double wrapping).
127     * 
128     * @param reader the wrapee to be used for the buffering support
129     * @return the passed <code>reader</code> decorated through a
130     *         {@link BufferedReader} object as wrapper
131     */
132    public static BufferedReader buffered(Reader reader) {
133        ObjectHelper.notNull(reader, "reader");
134        return (reader instanceof BufferedReader) ? (BufferedReader)reader : new BufferedReader(reader);
135    }
136
137    /**
138     * Wraps the passed <code>writer</code> into a {@link BufferedWriter} object
139     * and returns that. If the passed <code>writer</code> is already an
140     * instance of {@link BufferedWriter} returns the same passed
141     * <code>writer</code> reference as is (avoiding double wrapping).
142     * 
143     * @param writer the wrapee to be used for the buffering support
144     * @return the passed <code>writer</code> decorated through a
145     *         {@link BufferedWriter} object as wrapper
146     */
147    public static BufferedWriter buffered(Writer writer) {
148        ObjectHelper.notNull(writer, "writer");
149        return (writer instanceof BufferedWriter) ? (BufferedWriter)writer : new BufferedWriter(writer);
150    }
151
152    public static String toString(Reader reader) throws IOException {
153        return toString(buffered(reader));
154    }
155
156    public static String toString(BufferedReader reader) throws IOException {
157        StringBuilder sb = new StringBuilder(1024);
158        char[] buf = new char[1024];
159        try {
160            int len;
161            // read until we reach then end which is the -1 marker
162            while ((len = reader.read(buf)) != -1) {
163                sb.append(buf, 0, len);
164            }
165        } finally {
166            IOHelper.close(reader, "reader", LOG);
167        }
168
169        return sb.toString();
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 {}", new Object[] {input, output, bufferSize, flushOnEachWrite});
200        }
201
202        int total = 0;
203        final byte[] buffer = new byte[bufferSize];
204        int n = input.read(buffer);
205
206        boolean hasData;
207        if (ZERO_BYTE_EOL_ENABLED) {
208            // workaround issue on some application servers which can return 0
209            // (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    /**
255     * Forces any updates to this channel's file to be written to the storage
256     * device that contains it.
257     *
258     * @param channel the file channel
259     * @param name the name of the resource
260     * @param log the log to use when reporting warnings, will use this class's
261     *            own {@link Logger} if <tt>log == null</tt>
262     */
263    public static void force(FileChannel channel, String name, Logger log) {
264        try {
265            if (channel != null) {
266                channel.force(true);
267            }
268        } catch (Exception e) {
269            if (log == null) {
270                // then fallback to use the own Logger
271                log = LOG;
272            }
273            if (name != null) {
274                log.warn("Cannot force FileChannel: " + name + ". Reason: " + e.getMessage(), e);
275            } else {
276                log.warn("Cannot force FileChannel. Reason: {}", e.getMessage(), e);
277            }
278        }
279    }
280
281    /**
282     * Forces any updates to a FileOutputStream be written to the storage device
283     * that contains it.
284     *
285     * @param os the file output stream
286     * @param name the name of the resource
287     * @param log the log to use when reporting warnings, will use this class's
288     *            own {@link Logger} if <tt>log == null</tt>
289     */
290    public static void force(FileOutputStream os, String name, Logger log) {
291        try {
292            if (os != null) {
293                os.getFD().sync();
294            }
295        } catch (Exception e) {
296            if (log == null) {
297                // then fallback to use the own Logger
298                log = LOG;
299            }
300            if (name != null) {
301                log.warn("Cannot sync FileDescriptor: " + name + ". Reason: " + e.getMessage(), e);
302            } else {
303                log.warn("Cannot sync FileDescriptor. Reason: {}", e.getMessage(), e);
304            }
305        }
306    }
307
308    /**
309     * Closes the given writer, logging any closing exceptions to the given log.
310     * An associated FileOutputStream can optionally be forced to disk.
311     *
312     * @param writer the writer to close
313     * @param os an underlying FileOutputStream that will to be forced to disk
314     *            according to the force parameter
315     * @param name the name of the resource
316     * @param log the log to use when reporting warnings, will use this class's
317     *            own {@link Logger} if <tt>log == null</tt>
318     * @param force forces the FileOutputStream to disk
319     */
320    public static void close(Writer writer, FileOutputStream os, String name, Logger log, boolean force) {
321        if (writer != null && force) {
322            // flush the writer prior to syncing the FD
323            try {
324                writer.flush();
325            } catch (Exception e) {
326                if (log == null) {
327                    // then fallback to use the own Logger
328                    log = LOG;
329                }
330                if (name != null) {
331                    log.warn("Cannot flush Writer: " + name + ". Reason: " + e.getMessage(), e);
332                } else {
333                    log.warn("Cannot flush Writer. Reason: {}", e.getMessage(), e);
334                }
335            }
336            force(os, name, log);
337        }
338        close(writer, name, log);
339    }
340
341    /**
342     * Closes the given resource if it is available, logging any closing
343     * exceptions to the given log.
344     *
345     * @param closeable the object to close
346     * @param name the name of the resource
347     * @param log the log to use when reporting closure warnings, will use this
348     *            class's own {@link Logger} if <tt>log == null</tt>
349     */
350    public static void close(Closeable closeable, String name, Logger log) {
351        if (closeable != null) {
352            try {
353                closeable.close();
354            } catch (IOException e) {
355                if (log == null) {
356                    // then fallback to use the own Logger
357                    log = LOG;
358                }
359                if (name != null) {
360                    log.warn("Cannot close: " + name + ". Reason: " + e.getMessage(), e);
361                } else {
362                    log.warn("Cannot close. Reason: {}", e.getMessage(), e);
363                }
364            }
365        }
366    }
367
368    /**
369     * Closes the given resource if it is available and don't catch the
370     * exception
371     *
372     * @param closeable the object to close
373     * @throws IOException
374     */
375    public static void closeWithException(Closeable closeable) throws IOException {
376        if (closeable != null) {
377            closeable.close();
378        }
379    }
380
381    /**
382     * Closes the given channel if it is available, logging any closing
383     * exceptions to the given log. The file's channel can optionally be forced
384     * to disk.
385     *
386     * @param channel the file channel
387     * @param name the name of the resource
388     * @param log the log to use when reporting warnings, will use this class's
389     *            own {@link Logger} if <tt>log == null</tt>
390     * @param force forces the file channel to disk
391     */
392    public static void close(FileChannel channel, String name, Logger log, boolean force) {
393        if (force) {
394            force(channel, name, log);
395        }
396        close(channel, name, log);
397    }
398
399    /**
400     * Closes the given resource if it is available.
401     *
402     * @param closeable the object to close
403     * @param name the name of the resource
404     */
405    public static void close(Closeable closeable, String name) {
406        close(closeable, name, LOG);
407    }
408
409    /**
410     * Closes the given resource if it is available.
411     *
412     * @param closeable the object to close
413     */
414    public static void close(Closeable closeable) {
415        close(closeable, null, LOG);
416    }
417
418    /**
419     * Closes the given resources if they are available.
420     * 
421     * @param closeables the objects to close
422     */
423    public static void close(Closeable... closeables) {
424        for (Closeable closeable : closeables) {
425            close(closeable);
426        }
427    }
428
429    public static void closeIterator(Object it) throws IOException {
430        if (it instanceof Closeable) {
431            IOHelper.closeWithException((Closeable)it);
432        }
433        if (it instanceof java.util.Scanner) {
434            IOException ioException = ((java.util.Scanner)it).ioException();
435            if (ioException != null) {
436                throw ioException;
437            }
438        }
439    }
440
441    public static void validateCharset(String charset) throws UnsupportedCharsetException {
442        if (charset != null) {
443            if (Charset.isSupported(charset)) {
444                Charset.forName(charset);
445                return;
446            }
447        }
448        throw new UnsupportedCharsetException(charset);
449    }
450
451    /**
452     * Loads the entire stream into memory as a String and returns it.
453     * <p/>
454     * <b>Notice:</b> This implementation appends a <tt>\n</tt> as line
455     * terminator at the of the text.
456     * <p/>
457     * Warning, don't use for crazy big streams :)
458     */
459    public static String loadText(InputStream in) throws IOException {
460        StringBuilder builder = new StringBuilder();
461        InputStreamReader isr = new InputStreamReader(in);
462        try {
463            BufferedReader reader = buffered(isr);
464            while (true) {
465                String line = reader.readLine();
466                if (line != null) {
467                    builder.append(line);
468                    builder.append("\n");
469                } else {
470                    break;
471                }
472            }
473            return builder.toString();
474        } finally {
475            close(isr, in);
476        }
477    }
478
479    /**
480     * Get the charset name from the content type string
481     * 
482     * @param contentType
483     * @return the charset name, or <tt>UTF-8</tt> if no found
484     */
485    public static String getCharsetNameFromContentType(String contentType) {
486        String[] values = contentType.split(";");
487        String charset = "";
488
489        for (String value : values) {
490            value = value.trim();
491            if (value.toLowerCase().startsWith("charset=")) {
492                // Take the charset name
493                charset = value.substring(8);
494            }
495        }
496        if ("".equals(charset)) {
497            charset = "UTF-8";
498        }
499        return normalizeCharset(charset);
500
501    }
502
503    /**
504     * This method will take off the quotes and double quotes of the charset
505     */
506    public static String normalizeCharset(String charset) {
507        if (charset != null) {
508            String answer = charset.trim();
509            if (answer.startsWith("'") || answer.startsWith("\"")) {
510                answer = answer.substring(1);
511            }
512            if (answer.endsWith("'") || answer.endsWith("\"")) {
513                answer = answer.substring(0, answer.length() - 1);
514            }
515            return answer.trim();
516        } else {
517            return null;
518        }
519    }
520
521    /**
522     * Encoding-aware input stream.
523     */
524    public static class EncodingInputStream extends InputStream {
525
526        private final File file;
527        private final BufferedReader reader;
528        private final Charset defaultStreamCharset;
529
530        private ByteBuffer bufferBytes;
531        private CharBuffer bufferedChars = CharBuffer.allocate(4096);
532
533        public EncodingInputStream(File file, String charset) throws IOException {
534            this.file = file;
535            reader = toReader(file, charset);
536            defaultStreamCharset = defaultCharset.get();
537        }
538
539        @Override
540        public int read() throws IOException {
541            if (bufferBytes == null || bufferBytes.remaining() <= 0) {
542                bufferedChars.clear();
543                int len = reader.read(bufferedChars);
544                bufferedChars.flip();
545                if (len == -1) {
546                    return -1;
547                }
548                bufferBytes = defaultStreamCharset.encode(bufferedChars);
549            }
550            return bufferBytes.get();
551        }
552
553        @Override
554        public void close() throws IOException {
555            reader.close();
556        }
557
558        @Override
559        public void reset() throws IOException {
560            reader.reset();
561        }
562
563        public InputStream toOriginalInputStream() throws FileNotFoundException {
564            return new FileInputStream(file);
565        }
566    }
567
568    /**
569     * Encoding-aware file reader.
570     */
571    public static class EncodingFileReader extends InputStreamReader {
572
573        private final FileInputStream in;
574
575        /**
576         * @param in file to read
577         * @param charset character set to use
578         */
579        public EncodingFileReader(FileInputStream in, String charset) throws FileNotFoundException, UnsupportedEncodingException {
580            super(in, charset);
581            this.in = in;
582        }
583
584        @Override
585        public void close() throws IOException {
586            try {
587                super.close();
588            } finally {
589                in.close();
590            }
591        }
592    }
593
594    /**
595     * Encoding-aware file writer.
596     */
597    public static class EncodingFileWriter extends OutputStreamWriter {
598
599        private final FileOutputStream out;
600
601        /**
602         * @param out file to write
603         * @param charset character set to use
604         */
605        public EncodingFileWriter(FileOutputStream out, String charset) throws FileNotFoundException, UnsupportedEncodingException {
606            super(out, charset);
607            this.out = out;
608        }
609
610        @Override
611        public void close() throws IOException {
612            try {
613                super.close();
614            } finally {
615                out.close();
616            }
617        }
618    }
619
620    public static BufferedReader toReader(File file, String charset) throws IOException {
621        FileInputStream in = new FileInputStream(file);
622        return IOHelper.buffered(new EncodingFileReader(in, charset));
623    }
624}