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.channels.ReadableByteChannel;
041import java.nio.channels.WritableByteChannel;
042import java.nio.charset.Charset;
043import java.nio.charset.UnsupportedCharsetException;
044import java.util.function.Supplier;
045
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048
049/**
050 * IO helper class.
051 */
052public final class IOHelper {
053
054    public static Supplier<Charset> defaultCharset = Charset::defaultCharset;
055
056    public static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
057
058    public static final long INITIAL_OFFSET = 0;
059
060    private static final Logger LOG = LoggerFactory.getLogger(IOHelper.class);
061
062    // allows to turn on backwards compatible to turn off regarding the first
063    // read byte with value zero (0b0) as EOL.
064    // See more at CAMEL-11672
065    private static final boolean ZERO_BYTE_EOL_ENABLED
066            = "true".equalsIgnoreCase(System.getProperty("camel.zeroByteEOLEnabled", "true"));
067
068    private IOHelper() {
069        // Utility Class
070    }
071
072    /**
073     * Wraps the passed <code>in</code> into a {@link BufferedInputStream} object and returns that. If the passed
074     * <code>in</code> is already an instance of {@link BufferedInputStream} returns the same passed <code>in</code>
075     * reference as is (avoiding double wrapping).
076     *
077     * @param  in the wrapee to be used for the buffering support
078     * @return    the passed <code>in</code> decorated through a {@link BufferedInputStream} object as wrapper
079     */
080    public static BufferedInputStream buffered(InputStream in) {
081        return (in instanceof BufferedInputStream) ? (BufferedInputStream) in : new BufferedInputStream(in);
082    }
083
084    /**
085     * Wraps the passed <code>out</code> into a {@link BufferedOutputStream} object and returns that. If the passed
086     * <code>out</code> is already an instance of {@link BufferedOutputStream} returns the same passed <code>out</code>
087     * reference as is (avoiding double wrapping).
088     *
089     * @param  out the wrapee to be used for the buffering support
090     * @return     the passed <code>out</code> decorated through a {@link BufferedOutputStream} object as wrapper
091     */
092    public static BufferedOutputStream buffered(OutputStream out) {
093        return (out instanceof BufferedOutputStream) ? (BufferedOutputStream) out : new BufferedOutputStream(out);
094    }
095
096    /**
097     * Wraps the passed <code>reader</code> into a {@link BufferedReader} object and returns that. If the passed
098     * <code>reader</code> is already an instance of {@link BufferedReader} returns the same passed <code>reader</code>
099     * reference as is (avoiding double wrapping).
100     *
101     * @param  reader the wrapee to be used for the buffering support
102     * @return        the passed <code>reader</code> decorated through a {@link BufferedReader} object as wrapper
103     */
104    public static BufferedReader buffered(Reader reader) {
105        return (reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader);
106    }
107
108    /**
109     * Wraps the passed <code>writer</code> into a {@link BufferedWriter} object and returns that. If the passed
110     * <code>writer</code> is already an instance of {@link BufferedWriter} returns the same passed <code>writer</code>
111     * reference as is (avoiding double wrapping).
112     *
113     * @param  writer the writer to be used for the buffering support
114     * @return        the passed <code>writer</code> decorated through a {@link BufferedWriter} object as wrapper
115     */
116    public static BufferedWriter buffered(Writer writer) {
117        return (writer instanceof BufferedWriter) ? (BufferedWriter) writer : new BufferedWriter(writer);
118    }
119
120    public static String toString(Reader reader) throws IOException {
121        return toString(reader, INITIAL_OFFSET);
122    }
123
124    public static String toString(Reader reader, long offset) throws IOException {
125        return toString(buffered(reader), offset);
126    }
127
128    public static String toString(BufferedReader reader) throws IOException {
129        return toString(reader, INITIAL_OFFSET);
130    }
131
132    public static String toString(BufferedReader reader, long offset) throws IOException {
133        StringBuilder sb = new StringBuilder(1024);
134
135        reader.skip(offset);
136
137        char[] buf = new char[1024];
138        try {
139            int len;
140            // read until we reach then end which is the -1 marker
141            while ((len = reader.read(buf)) != -1) {
142                sb.append(buf, 0, len);
143            }
144        } finally {
145            IOHelper.close(reader, "reader", LOG);
146        }
147
148        return sb.toString();
149    }
150
151    public static int copy(InputStream input, OutputStream output) throws IOException {
152        return copy(input, output, DEFAULT_BUFFER_SIZE);
153    }
154
155    public static int copy(final InputStream input, final OutputStream output, int bufferSize) throws IOException {
156        return copy(input, output, bufferSize, false);
157    }
158
159    public static int copy(final InputStream input, final OutputStream output, int bufferSize, boolean flushOnEachWrite)
160            throws IOException {
161        return copy(input, output, bufferSize, flushOnEachWrite, -1);
162    }
163
164    public static int copy(
165            final InputStream input, final OutputStream output, int bufferSize, boolean flushOnEachWrite,
166            long maxSize)
167            throws IOException {
168
169        if (input instanceof ByteArrayInputStream) {
170            // optimized for byte array as we only need the max size it can be
171            input.mark(0);
172            input.reset();
173            bufferSize = input.available();
174        } else {
175            int avail = input.available();
176            if (avail > bufferSize) {
177                bufferSize = avail;
178            }
179        }
180
181        if (bufferSize > 262144) {
182            // upper cap to avoid buffers too big
183            bufferSize = 262144;
184        }
185
186        if (LOG.isTraceEnabled()) {
187            LOG.trace("Copying InputStream: {} -> OutputStream: {} with buffer: {} and flush on each write {}", input, output,
188                    bufferSize, flushOnEachWrite);
189        }
190
191        int total = 0;
192        final byte[] buffer = new byte[bufferSize];
193        int n = input.read(buffer);
194
195        boolean hasData;
196        if (ZERO_BYTE_EOL_ENABLED) {
197            // workaround issue on some application servers which can return 0
198            // (instead of -1)
199            // as first byte to indicate end of stream (CAMEL-11672)
200            hasData = n > 0;
201        } else {
202            hasData = n > -1;
203        }
204        if (hasData) {
205            while (-1 != n) {
206                output.write(buffer, 0, n);
207                if (flushOnEachWrite) {
208                    output.flush();
209                }
210                total += n;
211                if (maxSize > 0 && total > maxSize) {
212                    throw new IOException("The InputStream entry being copied exceeds the maximum allowed size");
213                }
214                n = input.read(buffer);
215            }
216        }
217        if (!flushOnEachWrite) {
218            // flush at end, if we didn't do it during the writing
219            output.flush();
220        }
221        return total;
222    }
223
224    public static void copyAndCloseInput(InputStream input, OutputStream output) throws IOException {
225        copyAndCloseInput(input, output, DEFAULT_BUFFER_SIZE);
226    }
227
228    public static void copyAndCloseInput(InputStream input, OutputStream output, int bufferSize) throws IOException {
229        copy(input, output, bufferSize);
230        close(input, null, LOG);
231    }
232
233    public static int copy(final Reader input, final Writer output, int bufferSize) throws IOException {
234        final char[] buffer = new char[bufferSize];
235        int n = input.read(buffer);
236        int total = 0;
237        while (-1 != n) {
238            output.write(buffer, 0, n);
239            total += n;
240            n = input.read(buffer);
241        }
242        output.flush();
243        return total;
244    }
245
246    public static void transfer(ReadableByteChannel input, WritableByteChannel output) throws IOException {
247        ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
248        while (input.read(buffer) >= 0) {
249            buffer.flip();
250            while (buffer.hasRemaining()) {
251                output.write(buffer);
252            }
253            buffer.clear();
254        }
255    }
256
257    /**
258     * Forces any updates to this channel's file to be written to the storage device that contains it.
259     *
260     * @param channel the file channel
261     * @param name    the name of the resource
262     * @param log     the log to use when reporting warnings, will use this class's own {@link Logger} if
263     *                <tt>log == null</tt>
264     */
265    public static void force(FileChannel channel, String name, Logger log) {
266        try {
267            if (channel != null) {
268                channel.force(true);
269            }
270        } catch (Exception e) {
271            if (log == null) {
272                // then fallback to use the own Logger
273                log = LOG;
274            }
275            if (name != null) {
276                log.warn("Cannot force FileChannel: " + name + ". Reason: " + e.getMessage(), e);
277            } else {
278                log.warn("Cannot force FileChannel. Reason: {}", e.getMessage(), e);
279            }
280        }
281    }
282
283    /**
284     * Forces any updates to a FileOutputStream be written to the storage device that contains it.
285     *
286     * @param os   the file output stream
287     * @param name the name of the resource
288     * @param log  the log to use when reporting warnings, will use this class's own {@link Logger} if
289     *             <tt>log == null</tt>
290     */
291    public static void force(FileOutputStream os, String name, Logger log) {
292        try {
293            if (os != null) {
294                os.getFD().sync();
295            }
296        } catch (Exception e) {
297            if (log == null) {
298                // then fallback to use the own Logger
299                log = LOG;
300            }
301            if (name != null) {
302                log.warn("Cannot sync FileDescriptor: " + name + ". Reason: " + e.getMessage(), e);
303            } else {
304                log.warn("Cannot sync FileDescriptor. Reason: {}", e.getMessage(), e);
305            }
306        }
307    }
308
309    /**
310     * Closes the given writer, logging any closing exceptions to the given log. An associated FileOutputStream can
311     * optionally be forced to disk.
312     *
313     * @param writer the writer to close
314     * @param os     an underlying FileOutputStream that will to be forced to disk 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 own {@link Logger} if
317     *               <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 exceptions to the given log.
343     *
344     * @param closeable the object to close
345     * @param name      the name of the resource
346     * @param log       the log to use when reporting closure warnings, will use this class's own {@link Logger} if
347     *                  <tt>log == null</tt>
348     */
349    public static void close(Closeable closeable, String name, Logger log) {
350        if (closeable != null) {
351            try {
352                closeable.close();
353            } catch (IOException e) {
354                if (log == null) {
355                    // then fallback to use the own Logger
356                    log = LOG;
357                }
358                if (name != null) {
359                    log.warn("Cannot close: " + name + ". Reason: " + e.getMessage(), e);
360                } else {
361                    log.warn("Cannot close. Reason: {}", e.getMessage(), e);
362                }
363            }
364        }
365    }
366
367    /**
368     * Closes the given resource if it is available and don't catch the exception
369     *
370     * @param  closeable   the object to close
371     * @throws IOException
372     */
373    public static void closeWithException(Closeable closeable) throws IOException {
374        if (closeable != null) {
375            closeable.close();
376        }
377    }
378
379    /**
380     * Closes the given channel if it is available, logging any closing exceptions to the given log. The file's channel
381     * can optionally be forced to disk.
382     *
383     * @param channel the file channel
384     * @param name    the name of the resource
385     * @param log     the log to use when reporting warnings, will use this class's own {@link Logger} if
386     *                <tt>log == null</tt>
387     * @param force   forces the file channel to disk
388     */
389    public static void close(FileChannel channel, String name, Logger log, boolean force) {
390        if (force) {
391            force(channel, name, log);
392        }
393        close(channel, name, log);
394    }
395
396    /**
397     * Closes the given resource if it is available.
398     *
399     * @param closeable the object to close
400     * @param name      the name of the resource
401     */
402    public static void close(Closeable closeable, String name) {
403        close(closeable, name, LOG);
404    }
405
406    /**
407     * Closes the given resource if it is available.
408     *
409     * @param closeable the object to close
410     */
411    public static void close(Closeable closeable) {
412        close(closeable, null, LOG);
413    }
414
415    /**
416     * Closes the given resources if they are available.
417     *
418     * @param closeables the objects to close
419     */
420    public static void close(Closeable... closeables) {
421        for (Closeable closeable : closeables) {
422            close(closeable);
423        }
424    }
425
426    public static void closeIterator(Object it) throws IOException {
427        if (it instanceof Closeable) {
428            IOHelper.closeWithException((Closeable) it);
429        }
430        if (it instanceof java.util.Scanner) {
431            IOException ioException = ((java.util.Scanner) it).ioException();
432            if (ioException != null) {
433                throw ioException;
434            }
435        }
436    }
437
438    public static void validateCharset(String charset) throws UnsupportedCharsetException {
439        if (charset != null) {
440            if (Charset.isSupported(charset)) {
441                Charset.forName(charset);
442                return;
443            }
444        }
445        throw new UnsupportedCharsetException(charset);
446    }
447
448    /**
449     * Loads the entire stream into memory as a String and returns it.
450     * <p/>
451     * <b>Notice:</b> This implementation appends a <tt>\n</tt> as line terminator at the of the text.
452     * <p/>
453     * Warning, don't use for crazy big streams :)
454     */
455    public static String loadText(InputStream in) throws IOException {
456        StringBuilder builder = new StringBuilder();
457        InputStreamReader isr = new InputStreamReader(in);
458        try {
459            BufferedReader reader = buffered(isr);
460            while (true) {
461                String line = reader.readLine();
462                if (line != null) {
463                    builder.append(line);
464                    builder.append("\n");
465                } else {
466                    break;
467                }
468            }
469            return builder.toString();
470        } finally {
471            close(isr, in);
472        }
473    }
474
475    /**
476     * Appends the text to the file.
477     */
478    public static void appendText(String text, File file) throws IOException {
479        if (!file.exists()) {
480            String path = FileUtil.onlyPath(file.getPath());
481            if (path != null) {
482                new File(path).mkdirs();
483            }
484        }
485        writeText(text, new FileOutputStream(file, true));
486    }
487
488    /**
489     * Writes the text to the file.
490     */
491    public static void writeText(String text, File file) throws IOException {
492        if (!file.exists()) {
493            String path = FileUtil.onlyPath(file.getPath());
494            if (path != null) {
495                new File(path).mkdirs();
496            }
497        }
498        writeText(text, new FileOutputStream(file, false));
499    }
500
501    /**
502     * Writes the text to the stream.
503     */
504    public static void writeText(String text, OutputStream os) throws IOException {
505        try {
506            os.write(text.getBytes());
507        } finally {
508            close(os);
509        }
510    }
511
512    /**
513     * Get the charset name from the content type string
514     *
515     * @param  contentType the content type
516     * @return             the charset name, or <tt>UTF-8</tt> if no found
517     */
518    public static String getCharsetNameFromContentType(String contentType) {
519        // try optimized for direct match without using splitting
520        int pos = contentType.indexOf("charset=");
521        if (pos != -1) {
522            // special optimization for utf-8 which is a common charset
523            if (contentType.regionMatches(true, pos + 8, "utf-8", 0, 5)) {
524                return "UTF-8";
525            }
526
527            int end = contentType.indexOf(';', pos);
528            String charset;
529            if (end > pos) {
530                charset = contentType.substring(pos + 8, end);
531            } else {
532                charset = contentType.substring(pos + 8);
533            }
534            return normalizeCharset(charset);
535        }
536
537        String[] values = contentType.split(";");
538        for (String value : values) {
539            value = value.trim();
540            // Perform a case insensitive "startsWith" check that works for different locales
541            String prefix = "charset=";
542            if (value.regionMatches(true, 0, prefix, 0, prefix.length())) {
543                // Take the charset name
544                String charset = value.substring(8);
545                return normalizeCharset(charset);
546            }
547        }
548        // use UTF-8 as default
549        return "UTF-8";
550    }
551
552    /**
553     * This method will take off the quotes and double quotes of the charset
554     */
555    public static String normalizeCharset(String charset) {
556        if (charset != null) {
557            boolean trim = false;
558            String answer = charset.trim();
559            if (answer.startsWith("'") || answer.startsWith("\"")) {
560                answer = answer.substring(1);
561                trim = true;
562            }
563            if (answer.endsWith("'") || answer.endsWith("\"")) {
564                answer = answer.substring(0, answer.length() - 1);
565                trim = true;
566            }
567            return trim ? answer.trim() : answer;
568        } else {
569            return null;
570        }
571    }
572
573    /**
574     * Lookup the OS environment variable in a safe manner by using upper case keys and underscore instead of dash.
575     */
576    public static String lookupEnvironmentVariable(String key) {
577        // lookup OS env with upper case key
578        String upperKey = key.toUpperCase();
579        String value = System.getenv(upperKey);
580
581        if (value == null) {
582            // some OS do not support dashes in keys, so replace with underscore
583            String normalizedKey = upperKey.replace('-', '_');
584
585            // and replace dots with underscores so keys like my.key are
586            // translated to MY_KEY
587            normalizedKey = normalizedKey.replace('.', '_');
588
589            value = System.getenv(normalizedKey);
590        }
591        return value;
592    }
593
594    /**
595     * Encoding-aware input stream.
596     */
597    public static class EncodingInputStream extends InputStream {
598
599        private final File file;
600        private final BufferedReader reader;
601        private final Charset defaultStreamCharset;
602
603        private ByteBuffer bufferBytes;
604        private CharBuffer bufferedChars = CharBuffer.allocate(4096);
605
606        public EncodingInputStream(File file, String charset) throws IOException {
607            this.file = file;
608            reader = toReader(file, charset);
609            defaultStreamCharset = defaultCharset.get();
610        }
611
612        @Override
613        public int read() throws IOException {
614            if (bufferBytes == null || bufferBytes.remaining() <= 0) {
615                BufferCaster.cast(bufferedChars).clear();
616                int len = reader.read(bufferedChars);
617                bufferedChars.flip();
618                if (len == -1) {
619                    return -1;
620                }
621                bufferBytes = defaultStreamCharset.encode(bufferedChars);
622            }
623            return bufferBytes.get() & 0xFF;
624        }
625
626        @Override
627        public void close() throws IOException {
628            reader.close();
629        }
630
631        @Override
632        public synchronized void reset() throws IOException {
633            reader.reset();
634        }
635
636        public InputStream toOriginalInputStream() throws FileNotFoundException {
637            return new FileInputStream(file);
638        }
639    }
640
641    /**
642     * Encoding-aware file reader.
643     */
644    public static class EncodingFileReader extends InputStreamReader {
645
646        private final FileInputStream in;
647
648        /**
649         * @param in      file to read
650         * @param charset character set to use
651         */
652        public EncodingFileReader(FileInputStream in, String charset) throws UnsupportedEncodingException {
653            super(in, charset);
654            this.in = in;
655        }
656
657        /**
658         * @param in      file to read
659         * @param charset character set to use
660         */
661        public EncodingFileReader(FileInputStream in, Charset charset) {
662            super(in, charset);
663            this.in = in;
664        }
665
666        @Override
667        public void close() throws IOException {
668            try {
669                super.close();
670            } finally {
671                in.close();
672            }
673        }
674    }
675
676    /**
677     * Encoding-aware file writer.
678     */
679    public static class EncodingFileWriter extends OutputStreamWriter {
680
681        private final FileOutputStream out;
682
683        /**
684         * @param out     file to write
685         * @param charset character set to use
686         */
687        public EncodingFileWriter(FileOutputStream out, String charset) throws UnsupportedEncodingException {
688            super(out, charset);
689            this.out = out;
690        }
691
692        /**
693         * @param out     file to write
694         * @param charset character set to use
695         */
696        public EncodingFileWriter(FileOutputStream out, Charset charset) {
697            super(out, charset);
698            this.out = out;
699        }
700
701        @Override
702        public void close() throws IOException {
703            try {
704                super.close();
705            } finally {
706                out.close();
707            }
708        }
709    }
710
711    /**
712     * Converts the given {@link File} with the given charset to {@link InputStream} with the JVM default charset
713     *
714     * @param  file    the file to be converted
715     * @param  charset the charset the file is read with
716     * @return         the input stream with the JVM default charset
717     */
718    public static InputStream toInputStream(File file, String charset) throws IOException {
719        if (charset != null) {
720            return new EncodingInputStream(file, charset);
721        } else {
722            return buffered(new FileInputStream(file));
723        }
724    }
725
726    public static BufferedReader toReader(File file, String charset) throws IOException {
727        FileInputStream in = new FileInputStream(file);
728        return IOHelper.buffered(new EncodingFileReader(in, charset));
729    }
730
731    public static BufferedReader toReader(File file, Charset charset) throws IOException {
732        FileInputStream in = new FileInputStream(file);
733        return IOHelper.buffered(new EncodingFileReader(in, charset));
734    }
735
736    public static BufferedWriter toWriter(FileOutputStream os, String charset) throws IOException {
737        return IOHelper.buffered(new EncodingFileWriter(os, charset));
738    }
739
740    public static BufferedWriter toWriter(FileOutputStream os, Charset charset) {
741        return IOHelper.buffered(new EncodingFileWriter(os, charset));
742    }
743}