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}