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