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.debug("Cannot force FileChannel: {}. Reason: {}", name, e.getMessage(), e);
346            } else {
347                log.debug("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.debug("Cannot sync FileDescriptor: {}. Reason: {}", name, e.getMessage(), e);
372            } else {
373                log.debug("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.debug("Cannot flush Writer: {}. Reason: {}", name, e.getMessage(), e);
401                } else {
402                    log.debug("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.debug("Cannot close: {}. Reason: {}", name, e.getMessage(), e);
429                } else {
430                    log.debug("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     */
441    public static void closeWithException(Closeable closeable) throws IOException {
442        if (closeable != null) {
443            closeable.close();
444        }
445    }
446
447    /**
448     * Closes the given channel if it is available, logging any closing exceptions to the given log. The file's channel
449     * can optionally be forced to disk.
450     *
451     * @param channel the file channel
452     * @param name    the name of the resource
453     * @param log     the log to use when reporting warnings, will use this class's own {@link Logger} if
454     *                <tt>log == null</tt>
455     * @param force   forces the file channel to disk
456     */
457    public static void close(FileChannel channel, String name, Logger log, boolean force) {
458        if (force) {
459            force(channel, name, log);
460        }
461        close(channel, name, log);
462    }
463
464    /**
465     * Closes the given resource if it is available.
466     *
467     * @param closeable the object to close
468     * @param name      the name of the resource
469     */
470    public static void close(Closeable closeable, String name) {
471        close(closeable, name, LOG);
472    }
473
474    /**
475     * Closes the given resource if it is available.
476     *
477     * @param closeable the object to close
478     */
479    public static void close(Closeable closeable) {
480        close(closeable, null, LOG);
481    }
482
483    /**
484     * Closes the given resources if they are available.
485     *
486     * @param closeables the objects to close
487     */
488    public static void close(Closeable... closeables) {
489        for (Closeable closeable : closeables) {
490            close(closeable);
491        }
492    }
493
494    public static void closeIterator(Object it) throws IOException {
495        if (it instanceof Closeable closeable) {
496            IOHelper.closeWithException(closeable);
497        }
498        if (it instanceof java.util.Scanner scanner) {
499            IOException ioException = scanner.ioException();
500            if (ioException != null) {
501                throw ioException;
502            }
503        }
504    }
505
506    public static void validateCharset(String charset) throws UnsupportedCharsetException {
507        if (charset != null) {
508            if (Charset.isSupported(charset)) {
509                Charset.forName(charset);
510                return;
511            }
512        }
513        throw new UnsupportedCharsetException(charset);
514    }
515
516    /**
517     * Loads the entire stream into memory as a String and returns it.
518     * <p/>
519     * <b>Notice:</b> This implementation appends a <tt>\n</tt> as line terminator at the of the text.
520     * <p/>
521     * Warning, don't use for crazy big streams :)
522     */
523    public static String loadText(InputStream in) throws IOException {
524        StringBuilder builder = new StringBuilder(2048);
525        InputStreamReader isr = new InputStreamReader(in);
526        try {
527            BufferedReader reader = buffered(isr);
528            while (true) {
529                String line = reader.readLine();
530                if (line != null) {
531                    builder.append(line);
532                    builder.append("\n");
533                } else {
534                    break;
535                }
536            }
537            return builder.toString();
538        } finally {
539            close(isr, in);
540        }
541    }
542
543    /**
544     * Loads the entire stream into memory as a String and returns the given line number.
545     * <p/>
546     * Warning, don't use for crazy big streams :)
547     */
548    public static String loadTextLine(InputStream in, int lineNumber) throws IOException {
549        int i = 0;
550        InputStreamReader isr = new InputStreamReader(in);
551        try {
552            BufferedReader reader = buffered(isr);
553            while (true) {
554                String line = reader.readLine();
555                if (line != null) {
556                    i++;
557                    if (i >= lineNumber) {
558                        return line;
559                    }
560                } else {
561                    break;
562                }
563            }
564        } finally {
565            close(isr, in);
566        }
567
568        return null;
569    }
570
571    /**
572     * Appends the text to the file.
573     */
574    public static void appendText(String text, File file) throws IOException {
575        doWriteText(text, file, true);
576    }
577
578    /**
579     * Writes the text to the file.
580     */
581    public static void writeText(String text, File file) throws IOException {
582        doWriteText(text, file, false);
583    }
584
585    @SuppressWarnings("ResultOfMethodCallIgnored")
586    private static void doWriteText(String text, File file, boolean append) throws IOException {
587        if (!file.exists()) {
588            String path = FileUtil.onlyPath(file.getPath());
589            if (path != null) {
590                new File(path).mkdirs();
591            }
592        }
593        writeText(text, new FileOutputStream(file, append));
594    }
595
596    /**
597     * Writes the text to the stream.
598     */
599    public static void writeText(String text, OutputStream os) throws IOException {
600        try {
601            os.write(text.getBytes());
602        } finally {
603            close(os);
604        }
605    }
606
607    /**
608     * Get the charset name from the content type string
609     *
610     * @param  contentType the content type
611     * @return             the charset name, or <tt>UTF-8</tt> if no found
612     */
613    public static String getCharsetNameFromContentType(String contentType) {
614        // try optimized for direct match without using splitting
615        int pos = contentType.indexOf("charset=");
616        if (pos != -1) {
617            // special optimization for utf-8 which is a common charset
618            if (contentType.regionMatches(true, pos + 8, "utf-8", 0, 5)) {
619                return "UTF-8";
620            }
621
622            int end = contentType.indexOf(';', pos);
623            String charset;
624            if (end > pos) {
625                charset = contentType.substring(pos + 8, end);
626            } else {
627                charset = contentType.substring(pos + 8);
628            }
629            return normalizeCharset(charset);
630        }
631
632        String[] values = contentType.split(";");
633        for (String value : values) {
634            value = value.trim();
635            // Perform a case insensitive "startsWith" check that works for different locales
636            String prefix = "charset=";
637            if (value.regionMatches(true, 0, prefix, 0, prefix.length())) {
638                // Take the charset name
639                String charset = value.substring(8);
640                return normalizeCharset(charset);
641            }
642        }
643        // use UTF-8 as default
644        return "UTF-8";
645    }
646
647    /**
648     * This method will take off the quotes and double quotes of the charset
649     */
650    public static String normalizeCharset(String charset) {
651        if (charset != null) {
652            boolean trim = false;
653            String answer = charset.trim();
654            if (answer.startsWith("'") || answer.startsWith("\"")) {
655                answer = answer.substring(1);
656                trim = true;
657            }
658            if (answer.endsWith("'") || answer.endsWith("\"")) {
659                answer = answer.substring(0, answer.length() - 1);
660                trim = true;
661            }
662            return trim ? answer.trim() : answer;
663        } else {
664            return null;
665        }
666    }
667
668    /**
669     * Lookup the OS environment variable in a safe manner by using upper case keys and underscore instead of dash.
670     *
671     * At first lookup attempt is made without considering camelCase keys. The second lookup is converting camelCase to
672     * underscores.
673     *
674     * For example given an ENV variable in either format: - CAMEL_KAMELET_AWS_S3_SOURCE_BUCKETNAMEORARN=myArn -
675     * CAMEL_KAMELET_AWS_S3_SOURCE_BUCKET_NAME_OR_ARN=myArn
676     *
677     * Then the following keys can look up both ENV formats above: - camel.kamelet.awsS3Source.bucketNameOrArn -
678     * camel.kamelet.aws-s3-source.bucketNameOrArn - camel.kamelet.aws-s3-source.bucket-name-or-arn
679     */
680    public static String lookupEnvironmentVariable(String key) {
681        // lookup OS env with upper case key
682        String upperKey = key.toUpperCase();
683        String value = System.getenv(upperKey);
684
685        if (value == null) {
686            value = System.getenv(normalizeEnvironmentVariable(upperKey));
687        }
688        if (value == null) {
689            // camelCase keys should use underscore as separator
690            String caseKey = StringHelper.camelCaseToDash(key);
691            value = System.getenv(normalizeEnvironmentVariable(caseKey));
692        }
693        return value;
694    }
695
696    /**
697     * Convert given key into an OS environment variable. Uses uppercase keys and converts dashes and dots to
698     * underscores.
699     */
700    public static String normalizeEnvironmentVariable(String key) {
701        String upperKey = key.toUpperCase();
702        // some OS do not support dashes in keys, so replace with underscore
703        String normalizedKey = upperKey.replace('-', '_');
704
705        // and replace dots with underscores so keys like my.key are
706        // translated to MY_KEY
707        return normalizedKey.replace('.', '_');
708    }
709
710    /**
711     * Encoding-aware input stream.
712     */
713    public static class EncodingInputStream extends InputStream {
714
715        private final Lock lock = new ReentrantLock();
716        private final Path file;
717        private final BufferedReader reader;
718        private final Charset defaultStreamCharset;
719
720        private ByteBuffer bufferBytes;
721        private final CharBuffer bufferedChars = CharBuffer.allocate(4096);
722
723        public EncodingInputStream(Path file, String charset) throws IOException {
724            this.file = file;
725            reader = toReader(file, charset);
726            defaultStreamCharset = defaultCharset.get();
727        }
728
729        @Override
730        public int read() throws IOException {
731            if (bufferBytes == null || bufferBytes.remaining() <= 0) {
732                BufferCaster.cast(bufferedChars).clear();
733                int len = reader.read(bufferedChars);
734                bufferedChars.flip();
735                if (len == -1) {
736                    return -1;
737                }
738                bufferBytes = defaultStreamCharset.encode(bufferedChars);
739            }
740            return bufferBytes.get() & 0xFF;
741        }
742
743        @Override
744        public void close() throws IOException {
745            reader.close();
746        }
747
748        @Override
749        public void reset() throws IOException {
750            lock.lock();
751            try {
752                reader.reset();
753            } finally {
754                lock.unlock();
755            }
756        }
757
758        public InputStream toOriginalInputStream() throws IOException {
759            return Files.newInputStream(file);
760        }
761    }
762
763    /**
764     * Encoding-aware file reader.
765     */
766    public static class EncodingFileReader extends InputStreamReader {
767
768        private final FileInputStream in;
769
770        /**
771         * @param in      file to read
772         * @param charset character set to use
773         */
774        public EncodingFileReader(FileInputStream in, String charset) throws UnsupportedEncodingException {
775            super(in, charset);
776            this.in = in;
777        }
778
779        /**
780         * @param in      file to read
781         * @param charset character set to use
782         */
783        public EncodingFileReader(FileInputStream in, Charset charset) {
784            super(in, charset);
785            this.in = in;
786        }
787
788        @Override
789        public void close() throws IOException {
790            try {
791                super.close();
792            } finally {
793                in.close();
794            }
795        }
796    }
797
798    /**
799     * Encoding-aware file writer.
800     */
801    public static class EncodingFileWriter extends OutputStreamWriter {
802
803        private final FileOutputStream out;
804
805        /**
806         * @param out     file to write
807         * @param charset character set to use
808         */
809        public EncodingFileWriter(FileOutputStream out, String charset) throws UnsupportedEncodingException {
810            super(out, charset);
811            this.out = out;
812        }
813
814        /**
815         * @param out     file to write
816         * @param charset character set to use
817         */
818        public EncodingFileWriter(FileOutputStream out, Charset charset) {
819            super(out, charset);
820            this.out = out;
821        }
822
823        @Override
824        public void close() throws IOException {
825            try {
826                super.close();
827            } finally {
828                out.close();
829            }
830        }
831    }
832
833    /**
834     * Converts the given {@link File} with the given charset to {@link InputStream} with the JVM default charset
835     *
836     * @param  file    the file to be converted
837     * @param  charset the charset the file is read with
838     * @return         the input stream with the JVM default charset
839     */
840    public static InputStream toInputStream(File file, String charset) throws IOException {
841        return toInputStream(file.toPath(), charset);
842    }
843
844    /**
845     * Converts the given {@link File} with the given charset to {@link InputStream} with the JVM default charset
846     *
847     * @param  file    the file to be converted
848     * @param  charset the charset the file is read with
849     * @return         the input stream with the JVM default charset
850     */
851    public static InputStream toInputStream(Path file, String charset) throws IOException {
852        if (charset != null) {
853            return new EncodingInputStream(file, charset);
854        } else {
855            return buffered(Files.newInputStream(file));
856        }
857    }
858
859    public static BufferedReader toReader(Path file, String charset) throws IOException {
860        return toReader(file, charset != null ? Charset.forName(charset) : null);
861    }
862
863    public static BufferedReader toReader(File file, String charset) throws IOException {
864        return toReader(file, charset != null ? Charset.forName(charset) : null);
865    }
866
867    public static BufferedReader toReader(File file, Charset charset) throws IOException {
868        return toReader(file.toPath(), charset);
869    }
870
871    public static BufferedReader toReader(Path file, Charset charset) throws IOException {
872        if (charset != null) {
873            return Files.newBufferedReader(file, charset);
874        } else {
875            return Files.newBufferedReader(file);
876        }
877    }
878
879    public static BufferedWriter toWriter(FileOutputStream os, String charset) throws IOException {
880        return IOHelper.buffered(new EncodingFileWriter(os, charset));
881    }
882
883    public static BufferedWriter toWriter(FileOutputStream os, Charset charset) {
884        return IOHelper.buffered(new EncodingFileWriter(os, charset));
885    }
886
887    /**
888     * Reads the file under the given {@code path}, strips lines starting with {@code commentPrefix} and optionally also
889     * strips blank lines (the ones for which {@link String#isBlank()} returns {@code true}. Normalizes EOL characters
890     * to {@code '\n'}.
891     *
892     * @param  path            the path of the file to read
893     * @param  commentPrefix   the leading character sequence of comment lines.
894     * @param  stripBlankLines if true {@code true} the lines matching {@link String#isBlank()} will not appear in the
895     *                         result
896     * @return                 the filtered content of the file
897     */
898    public static String stripLineComments(Path path, String commentPrefix, boolean stripBlankLines) {
899        StringBuilder result = new StringBuilder(2048);
900        try (Stream<String> lines = Files.lines(path)) {
901            lines
902                    .filter(l -> !stripBlankLines || !l.isBlank())
903                    .filter(line -> !line.startsWith(commentPrefix))
904                    .forEach(line -> result.append(line).append('\n'));
905        } catch (IOException e) {
906            throw new RuntimeException("Cannot read file: " + path, e);
907        }
908        return result.toString();
909    }
910
911}