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