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