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     * Writes the text to the file.
477     */
478    public static void writeText(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, false));
486    }
487
488    /**
489     * Writes the text to the stream.
490     */
491    public static void writeText(String text, OutputStream os) throws IOException {
492        try {
493            os.write(text.getBytes());
494        } finally {
495            close(os);
496        }
497    }
498
499    /**
500     * Get the charset name from the content type string
501     *
502     * @param  contentType the content type
503     * @return             the charset name, or <tt>UTF-8</tt> if no found
504     */
505    public static String getCharsetNameFromContentType(String contentType) {
506        // try optimized for direct match without using splitting
507        int pos = contentType.indexOf("charset=");
508        if (pos != -1) {
509            // special optimization for utf-8 which is a common charset
510            if (contentType.regionMatches(true, pos + 8, "utf-8", 0, 5)) {
511                return "UTF-8";
512            }
513
514            int end = contentType.indexOf(';', pos);
515            String charset;
516            if (end > pos) {
517                charset = contentType.substring(pos + 8, end);
518            } else {
519                charset = contentType.substring(pos + 8);
520            }
521            return normalizeCharset(charset);
522        }
523
524        String[] values = contentType.split(";");
525        for (String value : values) {
526            value = value.trim();
527            // Perform a case insensitive "startsWith" check that works for different locales
528            String prefix = "charset=";
529            if (value.regionMatches(true, 0, prefix, 0, prefix.length())) {
530                // Take the charset name
531                String charset = value.substring(8);
532                return normalizeCharset(charset);
533            }
534        }
535        // use UTF-8 as default
536        return "UTF-8";
537    }
538
539    /**
540     * This method will take off the quotes and double quotes of the charset
541     */
542    public static String normalizeCharset(String charset) {
543        if (charset != null) {
544            boolean trim = false;
545            String answer = charset.trim();
546            if (answer.startsWith("'") || answer.startsWith("\"")) {
547                answer = answer.substring(1);
548                trim = true;
549            }
550            if (answer.endsWith("'") || answer.endsWith("\"")) {
551                answer = answer.substring(0, answer.length() - 1);
552                trim = true;
553            }
554            return trim ? answer.trim() : answer;
555        } else {
556            return null;
557        }
558    }
559
560    /**
561     * Lookup the OS environment variable in a safe manner by using upper case keys and underscore instead of dash.
562     */
563    public static String lookupEnvironmentVariable(String key) {
564        // lookup OS env with upper case key
565        String upperKey = key.toUpperCase();
566        String value = System.getenv(upperKey);
567
568        if (value == null) {
569            // some OS do not support dashes in keys, so replace with underscore
570            String normalizedKey = upperKey.replace('-', '_');
571
572            // and replace dots with underscores so keys like my.key are
573            // translated to MY_KEY
574            normalizedKey = normalizedKey.replace('.', '_');
575
576            value = System.getenv(normalizedKey);
577        }
578        return value;
579    }
580
581    /**
582     * Encoding-aware input stream.
583     */
584    public static class EncodingInputStream extends InputStream {
585
586        private final File file;
587        private final BufferedReader reader;
588        private final Charset defaultStreamCharset;
589
590        private ByteBuffer bufferBytes;
591        private CharBuffer bufferedChars = CharBuffer.allocate(4096);
592
593        public EncodingInputStream(File file, String charset) throws IOException {
594            this.file = file;
595            reader = toReader(file, charset);
596            defaultStreamCharset = defaultCharset.get();
597        }
598
599        @Override
600        public int read() throws IOException {
601            if (bufferBytes == null || bufferBytes.remaining() <= 0) {
602                BufferCaster.cast(bufferedChars).clear();
603                int len = reader.read(bufferedChars);
604                bufferedChars.flip();
605                if (len == -1) {
606                    return -1;
607                }
608                bufferBytes = defaultStreamCharset.encode(bufferedChars);
609            }
610            return bufferBytes.get() & 0xFF;
611        }
612
613        @Override
614        public void close() throws IOException {
615            reader.close();
616        }
617
618        @Override
619        public synchronized void reset() throws IOException {
620            reader.reset();
621        }
622
623        public InputStream toOriginalInputStream() throws FileNotFoundException {
624            return new FileInputStream(file);
625        }
626    }
627
628    /**
629     * Encoding-aware file reader.
630     */
631    public static class EncodingFileReader extends InputStreamReader {
632
633        private final FileInputStream in;
634
635        /**
636         * @param in      file to read
637         * @param charset character set to use
638         */
639        public EncodingFileReader(FileInputStream in, String charset) throws UnsupportedEncodingException {
640            super(in, charset);
641            this.in = in;
642        }
643
644        /**
645         * @param in      file to read
646         * @param charset character set to use
647         */
648        public EncodingFileReader(FileInputStream in, Charset charset) {
649            super(in, charset);
650            this.in = in;
651        }
652
653        @Override
654        public void close() throws IOException {
655            try {
656                super.close();
657            } finally {
658                in.close();
659            }
660        }
661    }
662
663    /**
664     * Encoding-aware file writer.
665     */
666    public static class EncodingFileWriter extends OutputStreamWriter {
667
668        private final FileOutputStream out;
669
670        /**
671         * @param out     file to write
672         * @param charset character set to use
673         */
674        public EncodingFileWriter(FileOutputStream out, String charset) throws UnsupportedEncodingException {
675            super(out, charset);
676            this.out = out;
677        }
678
679        /**
680         * @param out     file to write
681         * @param charset character set to use
682         */
683        public EncodingFileWriter(FileOutputStream out, Charset charset) {
684            super(out, charset);
685            this.out = out;
686        }
687
688        @Override
689        public void close() throws IOException {
690            try {
691                super.close();
692            } finally {
693                out.close();
694            }
695        }
696    }
697
698    /**
699     * Converts the given {@link File} with the given charset to {@link InputStream} with the JVM default charset
700     *
701     * @param  file    the file to be converted
702     * @param  charset the charset the file is read with
703     * @return         the input stream with the JVM default charset
704     */
705    public static InputStream toInputStream(File file, String charset) throws IOException {
706        if (charset != null) {
707            return new EncodingInputStream(file, charset);
708        } else {
709            return buffered(new FileInputStream(file));
710        }
711    }
712
713    public static BufferedReader toReader(File file, String charset) throws IOException {
714        FileInputStream in = new FileInputStream(file);
715        return IOHelper.buffered(new EncodingFileReader(in, charset));
716    }
717
718    public static BufferedReader toReader(File file, Charset charset) throws IOException {
719        FileInputStream in = new FileInputStream(file);
720        return IOHelper.buffered(new EncodingFileReader(in, charset));
721    }
722
723    public static BufferedWriter toWriter(FileOutputStream os, String charset) throws IOException {
724        return IOHelper.buffered(new EncodingFileWriter(os, charset));
725    }
726
727    public static BufferedWriter toWriter(FileOutputStream os, Charset charset) throws IOException {
728        return IOHelper.buffered(new EncodingFileWriter(os, charset));
729    }
730}