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