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