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.FileOutputStream;
028import java.io.IOException;
029import java.io.InputStream;
030import java.io.InputStreamReader;
031import java.io.OutputStream;
032import java.io.OutputStreamWriter;
033import java.io.Reader;
034import java.io.UnsupportedEncodingException;
035import java.io.Writer;
036import java.nio.ByteBuffer;
037import java.nio.CharBuffer;
038import java.nio.channels.FileChannel;
039import java.nio.channels.ReadableByteChannel;
040import java.nio.channels.WritableByteChannel;
041import java.nio.charset.Charset;
042import java.nio.charset.UnsupportedCharsetException;
043import java.nio.file.Files;
044import java.nio.file.Path;
045import java.util.concurrent.locks.Lock;
046import java.util.concurrent.locks.ReentrantLock;
047import java.util.function.Supplier;
048import java.util.stream.Stream;
049
050import org.slf4j.Logger;
051import org.slf4j.LoggerFactory;
052
053/**
054 * IO helper class.
055 */
056public final class IOHelper {
057
058    public static Supplier<Charset> defaultCharset = Charset::defaultCharset;
059
060    // Use the same default buffer size as the JVM
061    public static final int DEFAULT_BUFFER_SIZE = 16384;
062
063    public static final long INITIAL_OFFSET = 0;
064
065    private static final Logger LOG = LoggerFactory.getLogger(IOHelper.class);
066
067    // allows to turn on backwards compatible to turn off regarding the first
068    // read byte with value zero (0b0) as EOL.
069    // See more at CAMEL-11672
070    private static final boolean ZERO_BYTE_EOL_ENABLED
071            = "true".equalsIgnoreCase(System.getProperty("camel.zeroByteEOLEnabled", "true"));
072
073    private IOHelper() {
074        // Utility Class
075    }
076
077    /**
078     * Wraps the passed <code>in</code> into a {@link BufferedInputStream} object and returns that. If the passed
079     * <code>in</code> is already an instance of {@link BufferedInputStream} returns the same passed <code>in</code>
080     * reference as is (avoiding double wrapping).
081     *
082     * @param  in the wrapee to be used for the buffering support
083     * @return    the passed <code>in</code> decorated through a {@link BufferedInputStream} object as wrapper
084     */
085    public static BufferedInputStream buffered(InputStream in) {
086        return (in instanceof BufferedInputStream bi) ? bi : new BufferedInputStream(in);
087    }
088
089    /**
090     * Wraps the passed <code>out</code> into a {@link BufferedOutputStream} object and returns that. If the passed
091     * <code>out</code> is already an instance of {@link BufferedOutputStream} returns the same passed <code>out</code>
092     * reference as is (avoiding double wrapping).
093     *
094     * @param  out the wrapee to be used for the buffering support
095     * @return     the passed <code>out</code> decorated through a {@link BufferedOutputStream} object as wrapper
096     */
097    public static BufferedOutputStream buffered(OutputStream out) {
098        return (out instanceof BufferedOutputStream bo) ? bo : new BufferedOutputStream(out);
099    }
100
101    /**
102     * Wraps the passed <code>reader</code> into a {@link BufferedReader} object and returns that. If the passed
103     * <code>reader</code> is already an instance of {@link BufferedReader} returns the same passed <code>reader</code>
104     * reference as is (avoiding double wrapping).
105     *
106     * @param  reader the wrapee to be used for the buffering support
107     * @return        the passed <code>reader</code> decorated through a {@link BufferedReader} object as wrapper
108     */
109    public static BufferedReader buffered(Reader reader) {
110        return (reader instanceof BufferedReader br) ? br : new BufferedReader(reader);
111    }
112
113    /**
114     * Wraps the passed <code>writer</code> into a {@link BufferedWriter} object and returns that. If the passed
115     * <code>writer</code> is already an instance of {@link BufferedWriter} returns the same passed <code>writer</code>
116     * reference as is (avoiding double wrapping).
117     *
118     * @param  writer the writer to be used for the buffering support
119     * @return        the passed <code>writer</code> decorated through a {@link BufferedWriter} object as wrapper
120     */
121    public static BufferedWriter buffered(Writer writer) {
122        return (writer instanceof BufferedWriter bw) ? bw : new BufferedWriter(writer);
123    }
124
125    public static String toString(Reader reader) throws IOException {
126        return toString(reader, INITIAL_OFFSET);
127    }
128
129    public static String toString(Reader reader, long offset) throws IOException {
130        return toString(buffered(reader), offset);
131    }
132
133    public static String toString(BufferedReader reader) throws IOException {
134        return toString(reader, INITIAL_OFFSET);
135    }
136
137    public static String toString(BufferedReader reader, long offset) throws IOException {
138        StringBuilder sb = new StringBuilder(1024);
139
140        reader.skip(offset);
141
142        char[] buf = new char[1024];
143        try {
144            int len;
145            // read until we reach then end which is the -1 marker
146            while ((len = reader.read(buf)) != -1) {
147                sb.append(buf, 0, len);
148            }
149        } finally {
150            IOHelper.close(reader, "reader", LOG);
151        }
152
153        return sb.toString();
154    }
155
156    /**
157     * Copies the data from the input stream to the output stream. Uses {@link InputStream#transferTo(OutputStream)}.
158     *
159     * @param  input       the input stream buffer
160     * @param  output      the output stream buffer
161     * @return             the number of bytes copied
162     * @throws IOException for I/O errors
163     */
164    public static int copy(InputStream input, OutputStream output) throws IOException {
165        int copied = (int) input.transferTo(output);
166        output.flush();
167        return copied;
168    }
169
170    /**
171     * Copies the data from the input stream to the output stream. Uses the legacy copy logic. Prefer using
172     * {@link IOHelper#copy(InputStream, OutputStream)} unless you have to control how data is flushed the buffer
173     *
174     * @param  input       the input stream buffer
175     * @param  output      the output stream buffer
176     * @param  bufferSize  the size of the buffer used for the copies
177     * @return             the number of bytes copied
178     * @throws IOException for I/O errors
179     */
180    public static int copy(final InputStream input, final OutputStream output, int bufferSize) throws IOException {
181        return copy(input, output, bufferSize, false);
182    }
183
184    /**
185     * Copies the data from the input stream to the output stream. Uses the legacy copy logic. Prefer using
186     * {@link IOHelper#copy(InputStream, OutputStream)} unless you have to control how data is flushed the buffer
187     *
188     * @param  input            the input stream buffer
189     * @param  output           the output stream buffer
190     * @param  bufferSize       the size of the buffer used for the copies
191     * @param  flushOnEachWrite whether to flush the data everytime that data is written to the buffer
192     * @return                  the number of bytes copied
193     * @throws IOException      for I/O errors
194     */
195    public static int copy(final InputStream input, final OutputStream output, int bufferSize, boolean flushOnEachWrite)
196            throws IOException {
197        return copy(input, output, bufferSize, flushOnEachWrite, -1);
198    }
199
200    /**
201     * Copies the data from the input stream to the output stream. Uses the legacy copy logic. Prefer using
202     * {@link IOHelper#copy(InputStream, OutputStream)} unless you have to control how data is flushed the buffer
203     *
204     * @param  input            the input stream buffer
205     * @param  output           the output stream buffer
206     * @param  bufferSize       the size of the buffer used for the copies
207     * @param  flushOnEachWrite whether to flush the data everytime that data is written to the buffer
208     * @return                  the number of bytes copied
209     * @throws IOException      for I/O errors
210     */
211    public static int copy(
212            final InputStream input, final OutputStream output, int bufferSize, boolean flushOnEachWrite,
213            long maxSize)
214            throws IOException {
215
216        if (input instanceof ByteArrayInputStream) {
217            // optimized for byte arrays as we only need the max size it can be
218            input.mark(0);
219            input.reset();
220            bufferSize = input.available();
221        } else {
222            int avail = input.available();
223            if (avail > bufferSize) {
224                bufferSize = avail;
225            }
226        }
227
228        if (bufferSize > 262144) {
229            // upper cap to avoid buffers too big
230            bufferSize = 262144;
231        }
232
233        if (LOG.isTraceEnabled()) {
234            LOG.trace("Copying InputStream: {} -> OutputStream: {} with buffer: {} and flush on each write {}", input, output,
235                    bufferSize, flushOnEachWrite);
236        }
237
238        int total = 0;
239        final byte[] buffer = new byte[bufferSize];
240        int n = input.read(buffer);
241
242        boolean hasData;
243        if (ZERO_BYTE_EOL_ENABLED) {
244            // workaround issue on some application servers which can return 0
245            // (instead of -1)
246            // as first byte to indicate the end of stream (CAMEL-11672)
247            hasData = n > 0;
248        } else {
249            hasData = n > -1;
250        }
251        if (hasData) {
252            while (-1 != n) {
253                output.write(buffer, 0, n);
254                if (flushOnEachWrite) {
255                    output.flush();
256                }
257                total += n;
258                if (maxSize > 0 && total > maxSize) {
259                    throw new IOException("The InputStream entry being copied exceeds the maximum allowed size");
260                }
261                n = input.read(buffer);
262            }
263        }
264        if (!flushOnEachWrite) {
265            // flush at end, if we didn't do it during the writing
266            output.flush();
267        }
268        return total;
269    }
270
271    /**
272     * Copies the data from the input stream to the output stream and closes the input stream afterward. Uses
273     * {@link InputStream#transferTo(OutputStream)}.
274     *
275     * @param  input       the input stream buffer
276     * @param  output      the output stream buffer
277     * @throws IOException
278     */
279    public static void copyAndCloseInput(InputStream input, OutputStream output) throws IOException {
280        copy(input, output);
281        close(input, null, LOG);
282    }
283
284    /**
285     * Copies the data from the input stream to the output stream and closes the input stream afterward. Uses Camel's
286     * own copying logic. Prefer using {@link IOHelper#copyAndCloseInput(InputStream, OutputStream)} unless you need a
287     * specific buffer size.
288     *
289     * @param  input       the input stream buffer
290     * @param  output      the output stream buffer
291     * @param  bufferSize  the size of the buffer used for the copies
292     * @throws IOException
293     */
294    public static void copyAndCloseInput(InputStream input, OutputStream output, int bufferSize) throws IOException {
295        copy(input, output, bufferSize);
296        close(input, null, LOG);
297    }
298
299    public static int copy(final Reader input, final Writer output, int bufferSize) throws IOException {
300        final char[] buffer = new char[bufferSize];
301        int n = input.read(buffer);
302        int total = 0;
303        while (-1 != n) {
304            output.write(buffer, 0, n);
305            total += n;
306            n = input.read(buffer);
307        }
308        output.flush();
309        return total;
310    }
311
312    public static void transfer(ReadableByteChannel input, WritableByteChannel output) throws IOException {
313        ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
314        while (input.read(buffer) >= 0) {
315            buffer.flip();
316            while (buffer.hasRemaining()) {
317                output.write(buffer);
318            }
319            buffer.clear();
320        }
321    }
322
323    /**
324     * Forces any updates to this channel's file to be written to the storage device that contains it.
325     *
326     * @param channel the file channel
327     * @param name    the name of the resource
328     * @param log     the log to use when reporting warnings, will use this class's own {@link Logger} if
329     *                <tt>log == null</tt>
330     */
331    public static void force(FileChannel channel, String name, Logger log) {
332        try {
333            if (channel != null) {
334                channel.force(true);
335            }
336        } catch (Exception e) {
337            if (log == null) {
338                // then fallback to use the own Logger
339                log = LOG;
340            }
341            if (name != null) {
342                log.debug("Cannot force FileChannel: {}. Reason: {}", name, e.getMessage(), e);
343            } else {
344                log.debug("Cannot force FileChannel. Reason: {}", e.getMessage(), e);
345            }
346        }
347    }
348
349    /**
350     * Forces any updates to a FileOutputStream be written to the storage device that contains it.
351     *
352     * @param os   the file output stream
353     * @param name the name of the resource
354     * @param log  the log to use when reporting warnings, will use this class's own {@link Logger} if
355     *             <tt>log == null</tt>
356     */
357    public static void force(FileOutputStream os, String name, Logger log) {
358        try {
359            if (os != null) {
360                os.getFD().sync();
361            }
362        } catch (Exception e) {
363            if (log == null) {
364                // then fallback to use the own Logger
365                log = LOG;
366            }
367            if (name != null) {
368                log.debug("Cannot sync FileDescriptor: {}. Reason: {}", name, e.getMessage(), e);
369            } else {
370                log.debug("Cannot sync FileDescriptor. Reason: {}", e.getMessage(), e);
371            }
372        }
373    }
374
375    /**
376     * Closes the given writer, logging any closing exceptions to the given log. An associated FileOutputStream can
377     * optionally be forced to disk.
378     *
379     * @param writer the writer to close
380     * @param os     an underlying FileOutputStream that will to be forced to disk according to the force parameter
381     * @param name   the name of the resource
382     * @param log    the log to use when reporting warnings, will use this class's own {@link Logger} if
383     *               <tt>log == null</tt>
384     * @param force  forces the FileOutputStream to disk
385     */
386    public static void close(Writer writer, FileOutputStream os, String name, Logger log, boolean force) {
387        if (writer != null && force) {
388            // flush the writer prior to syncing the FD
389            try {
390                writer.flush();
391            } catch (Exception e) {
392                if (log == null) {
393                    // then fallback to use the own Logger
394                    log = LOG;
395                }
396                if (name != null) {
397                    log.debug("Cannot flush Writer: {}. Reason: {}", name, e.getMessage(), e);
398                } else {
399                    log.debug("Cannot flush Writer. Reason: {}", e.getMessage(), e);
400                }
401            }
402            force(os, name, log);
403        }
404        close(writer, name, log);
405    }
406
407    /**
408     * Closes the given resource if it is available, logging any closing exceptions to the given log.
409     *
410     * @param closeable the object to close
411     * @param name      the name of the resource
412     * @param log       the log to use when reporting closure warnings, will use this class's own {@link Logger} if
413     *                  <tt>log == null</tt>
414     */
415    public static void close(Closeable closeable, String name, Logger log) {
416        if (closeable != null) {
417            try {
418                closeable.close();
419            } catch (IOException e) {
420                if (log == null) {
421                    // then fallback to use the own Logger
422                    log = LOG;
423                }
424                if (name != null) {
425                    log.debug("Cannot close: {}. Reason: {}", name, e.getMessage(), e);
426                } else {
427                    log.debug("Cannot close. Reason: {}", e.getMessage(), e);
428                }
429            }
430        }
431    }
432
433    /**
434     * Closes the given resource if it is available and don't catch the exception
435     *
436     * @param closeable the object to close
437     */
438    public static void closeWithException(Closeable closeable) throws IOException {
439        if (closeable != null) {
440            closeable.close();
441        }
442    }
443
444    /**
445     * Closes the given channel if it is available, logging any closing exceptions to the given log. The file's channel
446     * can optionally be forced to disk.
447     *
448     * @param channel the file channel
449     * @param name    the name of the resource
450     * @param log     the log to use when reporting warnings, will use this class's own {@link Logger} if
451     *                <tt>log == null</tt>
452     * @param force   forces the file channel to disk
453     */
454    public static void close(FileChannel channel, String name, Logger log, boolean force) {
455        if (force) {
456            force(channel, name, log);
457        }
458        close(channel, name, log);
459    }
460
461    /**
462     * Closes the given resource if it is available.
463     *
464     * @param closeable the object to close
465     * @param name      the name of the resource
466     */
467    public static void close(Closeable closeable, String name) {
468        close(closeable, name, LOG);
469    }
470
471    /**
472     * Closes the given resource if it is available.
473     *
474     * @param closeable the object to close
475     */
476    public static void close(Closeable closeable) {
477        close(closeable, null, LOG);
478    }
479
480    /**
481     * Closes the given resources if they are available.
482     *
483     * @param closeables the objects to close
484     */
485    public static void close(Closeable... closeables) {
486        for (Closeable closeable : closeables) {
487            close(closeable);
488        }
489    }
490
491    public static void closeIterator(Object it) throws IOException {
492        if (it instanceof Closeable closeable) {
493            IOHelper.closeWithException(closeable);
494        }
495        if (it instanceof java.util.Scanner scanner) {
496            IOException ioException = scanner.ioException();
497            if (ioException != null) {
498                throw ioException;
499            }
500        }
501    }
502
503    public static void validateCharset(String charset) throws UnsupportedCharsetException {
504        if (charset != null) {
505            if (Charset.isSupported(charset)) {
506                Charset.forName(charset);
507                return;
508            }
509        }
510        throw new UnsupportedCharsetException(charset);
511    }
512
513    /**
514     * Loads the entire stream into memory as a String and returns it.
515     * <p/>
516     * <b>Notice:</b> This implementation appends a <tt>\n</tt> as line terminator at the of the text.
517     * <p/>
518     * Warning, don't use for crazy big streams :)
519     */
520    public static String loadText(InputStream in) throws IOException {
521        StringBuilder builder = new StringBuilder(2048);
522        InputStreamReader isr = new InputStreamReader(in);
523        try {
524            BufferedReader reader = buffered(isr);
525            while (true) {
526                String line = reader.readLine();
527                if (line != null) {
528                    builder.append(line);
529                    builder.append("\n");
530                } else {
531                    break;
532                }
533            }
534            return builder.toString();
535        } finally {
536            close(isr, in);
537        }
538    }
539
540    /**
541     * Loads the entire stream into memory as a String and returns the given line number.
542     * <p/>
543     * Warning, don't use for crazy big streams :)
544     */
545    public static String loadTextLine(InputStream in, int lineNumber) throws IOException {
546        int i = 0;
547        InputStreamReader isr = new InputStreamReader(in);
548        try {
549            BufferedReader reader = buffered(isr);
550            while (true) {
551                String line = reader.readLine();
552                if (line != null) {
553                    i++;
554                    if (i >= lineNumber) {
555                        return line;
556                    }
557                } else {
558                    break;
559                }
560            }
561        } finally {
562            close(isr, in);
563        }
564
565        return null;
566    }
567
568    /**
569     * Appends the text to the file.
570     */
571    public static void appendText(String text, File file) throws IOException {
572        doWriteText(text, file, true);
573    }
574
575    /**
576     * Writes the text to the file.
577     */
578    public static void writeText(String text, File file) throws IOException {
579        doWriteText(text, file, false);
580    }
581
582    @SuppressWarnings("ResultOfMethodCallIgnored")
583    private static void doWriteText(String text, File file, boolean append) throws IOException {
584        if (!file.exists()) {
585            String path = FileUtil.onlyPath(file.getPath());
586            if (path != null) {
587                new File(path).mkdirs();
588            }
589        }
590        writeText(text, new FileOutputStream(file, append));
591    }
592
593    /**
594     * Writes the text to the stream.
595     */
596    public static void writeText(String text, OutputStream os) throws IOException {
597        try {
598            os.write(text.getBytes());
599        } finally {
600            close(os);
601        }
602    }
603
604    /**
605     * Get the charset name from the content type string
606     *
607     * @param  contentType the content type
608     * @return             the charset name, or <tt>UTF-8</tt> if no found
609     */
610    public static String getCharsetNameFromContentType(String contentType) {
611        // try optimized for direct match without using splitting
612        int pos = contentType.indexOf("charset=");
613        if (pos != -1) {
614            // special optimization for utf-8 which is a common charset
615            if (contentType.regionMatches(true, pos + 8, "utf-8", 0, 5)) {
616                return "UTF-8";
617            }
618
619            int end = contentType.indexOf(';', pos);
620            String charset;
621            if (end > pos) {
622                charset = contentType.substring(pos + 8, end);
623            } else {
624                charset = contentType.substring(pos + 8);
625            }
626            return normalizeCharset(charset);
627        }
628
629        String[] values = contentType.split(";");
630        for (String value : values) {
631            value = value.trim();
632            // Perform a case insensitive "startsWith" check that works for different locales
633            String prefix = "charset=";
634            if (value.regionMatches(true, 0, prefix, 0, prefix.length())) {
635                // Take the charset name
636                String charset = value.substring(8);
637                return normalizeCharset(charset);
638            }
639        }
640        // use UTF-8 as default
641        return "UTF-8";
642    }
643
644    /**
645     * This method will take off the quotes and double quotes of the charset
646     */
647    public static String normalizeCharset(String charset) {
648        if (charset != null) {
649            boolean trim = false;
650            String answer = charset.trim();
651            if (answer.startsWith("'") || answer.startsWith("\"")) {
652                answer = answer.substring(1);
653                trim = true;
654            }
655            if (answer.endsWith("'") || answer.endsWith("\"")) {
656                answer = answer.substring(0, answer.length() - 1);
657                trim = true;
658            }
659            return trim ? answer.trim() : answer;
660        } else {
661            return null;
662        }
663    }
664
665    /**
666     * Lookup the OS environment variable in a safe manner by using upper case keys and underscore instead of dash.
667     *
668     * At first lookup attempt is made without considering camelCase keys. The second lookup is converting camelCase to
669     * underscores.
670     *
671     * For example given an ENV variable in either format: - CAMEL_KAMELET_AWS_S3_SOURCE_BUCKETNAMEORARN=myArn -
672     * CAMEL_KAMELET_AWS_S3_SOURCE_BUCKET_NAME_OR_ARN=myArn
673     *
674     * Then the following keys can look up both ENV formats above: - camel.kamelet.awsS3Source.bucketNameOrArn -
675     * camel.kamelet.aws-s3-source.bucketNameOrArn - camel.kamelet.aws-s3-source.bucket-name-or-arn
676     */
677    public static String lookupEnvironmentVariable(String key) {
678        // lookup OS env with upper case key
679        String upperKey = key.toUpperCase();
680        String value = System.getenv(upperKey);
681
682        if (value == null) {
683            value = System.getenv(normalizeEnvironmentVariable(upperKey));
684        }
685        if (value == null) {
686            // camelCase keys should use underscore as separator
687            String caseKey = StringHelper.camelCaseToDash(key);
688            value = System.getenv(normalizeEnvironmentVariable(caseKey));
689        }
690        return value;
691    }
692
693    /**
694     * Convert given key into an OS environment variable. Uses uppercase keys and converts dashes and dots to
695     * underscores.
696     */
697    public static String normalizeEnvironmentVariable(String key) {
698        String upperKey = key.toUpperCase();
699        // some OS do not support dashes in keys, so replace with underscore
700        String normalizedKey = upperKey.replace('-', '_');
701
702        // and replace dots with underscores so keys like my.key are
703        // translated to MY_KEY
704        return normalizedKey.replace('.', '_');
705    }
706
707    /**
708     * Encoding-aware input stream.
709     */
710    public static class EncodingInputStream extends InputStream {
711
712        private final Lock lock = new ReentrantLock();
713        private final Path file;
714        private final BufferedReader reader;
715        private final Charset defaultStreamCharset;
716
717        private ByteBuffer bufferBytes;
718        private final CharBuffer bufferedChars = CharBuffer.allocate(4096);
719
720        public EncodingInputStream(Path file, String charset) throws IOException {
721            this.file = file;
722            reader = toReader(file, charset);
723            defaultStreamCharset = defaultCharset.get();
724        }
725
726        @Override
727        public int read() throws IOException {
728            if (bufferBytes == null || bufferBytes.remaining() <= 0) {
729                BufferCaster.cast(bufferedChars).clear();
730                int len = reader.read(bufferedChars);
731                bufferedChars.flip();
732                if (len == -1) {
733                    return -1;
734                }
735                bufferBytes = defaultStreamCharset.encode(bufferedChars);
736            }
737            return bufferBytes.get() & 0xFF;
738        }
739
740        @Override
741        public void close() throws IOException {
742            reader.close();
743        }
744
745        @Override
746        public void reset() throws IOException {
747            lock.lock();
748            try {
749                reader.reset();
750            } finally {
751                lock.unlock();
752            }
753        }
754
755        public InputStream toOriginalInputStream() throws IOException {
756            return Files.newInputStream(file);
757        }
758    }
759
760    /**
761     * Encoding-aware file reader.
762     */
763    public static class EncodingFileReader extends InputStreamReader {
764
765        private final FileInputStream in;
766
767        /**
768         * @param in      file to read
769         * @param charset character set to use
770         */
771        public EncodingFileReader(FileInputStream in, String charset) throws UnsupportedEncodingException {
772            super(in, charset);
773            this.in = in;
774        }
775
776        /**
777         * @param in      file to read
778         * @param charset character set to use
779         */
780        public EncodingFileReader(FileInputStream in, Charset charset) {
781            super(in, charset);
782            this.in = in;
783        }
784
785        @Override
786        public void close() throws IOException {
787            try {
788                super.close();
789            } finally {
790                in.close();
791            }
792        }
793    }
794
795    /**
796     * Encoding-aware file writer.
797     */
798    public static class EncodingFileWriter extends OutputStreamWriter {
799
800        private final FileOutputStream out;
801
802        /**
803         * @param out     file to write
804         * @param charset character set to use
805         */
806        public EncodingFileWriter(FileOutputStream out, String charset) throws UnsupportedEncodingException {
807            super(out, charset);
808            this.out = out;
809        }
810
811        /**
812         * @param out     file to write
813         * @param charset character set to use
814         */
815        public EncodingFileWriter(FileOutputStream out, Charset charset) {
816            super(out, charset);
817            this.out = out;
818        }
819
820        @Override
821        public void close() throws IOException {
822            try {
823                super.close();
824            } finally {
825                out.close();
826            }
827        }
828    }
829
830    /**
831     * Converts the given {@link File} with the given charset to {@link InputStream} with the JVM default charset
832     *
833     * @param  file    the file to be converted
834     * @param  charset the charset the file is read with
835     * @return         the input stream with the JVM default charset
836     */
837    public static InputStream toInputStream(File file, String charset) throws IOException {
838        return toInputStream(file.toPath(), charset);
839    }
840
841    /**
842     * Converts the given {@link File} with the given charset to {@link InputStream} with the JVM default charset
843     *
844     * @param  file    the file to be converted
845     * @param  charset the charset the file is read with
846     * @return         the input stream with the JVM default charset
847     */
848    public static InputStream toInputStream(Path file, String charset) throws IOException {
849        if (charset != null) {
850            return new EncodingInputStream(file, charset);
851        } else {
852            return buffered(Files.newInputStream(file));
853        }
854    }
855
856    public static BufferedReader toReader(Path file, String charset) throws IOException {
857        return toReader(file, charset != null ? Charset.forName(charset) : null);
858    }
859
860    public static BufferedReader toReader(File file, String charset) throws IOException {
861        return toReader(file, charset != null ? Charset.forName(charset) : null);
862    }
863
864    public static BufferedReader toReader(File file, Charset charset) throws IOException {
865        return toReader(file.toPath(), charset);
866    }
867
868    public static BufferedReader toReader(Path file, Charset charset) throws IOException {
869        if (charset != null) {
870            return Files.newBufferedReader(file, charset);
871        } else {
872            return Files.newBufferedReader(file);
873        }
874    }
875
876    public static BufferedWriter toWriter(FileOutputStream os, String charset) throws IOException {
877        return IOHelper.buffered(new EncodingFileWriter(os, charset));
878    }
879
880    public static BufferedWriter toWriter(FileOutputStream os, Charset charset) {
881        return IOHelper.buffered(new EncodingFileWriter(os, charset));
882    }
883
884    /**
885     * Reads the file under the given {@code path}, strips lines starting with {@code commentPrefix} and optionally also
886     * strips blank lines (the ones for which {@link String#isBlank()} returns {@code true}. Normalizes EOL characters
887     * to {@code '\n'}.
888     *
889     * @param  path            the path of the file to read
890     * @param  commentPrefix   the leading character sequence of comment lines.
891     * @param  stripBlankLines if true {@code true} the lines matching {@link String#isBlank()} will not appear in the
892     *                         result
893     * @return                 the filtered content of the file
894     */
895    public static String stripLineComments(Path path, String commentPrefix, boolean stripBlankLines) {
896        StringBuilder result = new StringBuilder(2048);
897        try (Stream<String> lines = Files.lines(path)) {
898            lines
899                    .filter(l -> !stripBlankLines || !l.isBlank())
900                    .filter(line -> !line.startsWith(commentPrefix))
901                    .forEach(line -> result.append(line).append('\n'));
902        } catch (IOException e) {
903            throw new RuntimeException("Cannot read file: " + path, e);
904        }
905        return result.toString();
906    }
907
908}