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