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