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