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}