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