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