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 = "true".equalsIgnoreCase(System.getProperty("camel.zeroByteEOLEnabled", "true")); 064 065 private IOHelper() { 066 // Utility Class 067 } 068 069 /** 070 * Wraps the passed <code>in</code> into a {@link BufferedInputStream} 071 * object and returns that. If the passed <code>in</code> is already an 072 * instance of {@link BufferedInputStream} returns the same passed 073 * <code>in</code> 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 077 * {@link BufferedInputStream} object as wrapper 078 */ 079 public static BufferedInputStream buffered(InputStream in) { 080 ObjectHelper.notNull(in, "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} 086 * object and returns that. If the passed <code>out</code> is already an 087 * instance of {@link BufferedOutputStream} returns the same passed 088 * <code>out</code> reference as is (avoiding double wrapping). 089 * 090 * @param out the wrapee to be used for the buffering support 091 * @return the passed <code>out</code> decorated through a 092 * {@link BufferedOutputStream} object as wrapper 093 */ 094 public static BufferedOutputStream buffered(OutputStream out) { 095 ObjectHelper.notNull(out, "out"); 096 return (out instanceof BufferedOutputStream) ? (BufferedOutputStream)out : new BufferedOutputStream(out); 097 } 098 099 /** 100 * Wraps the passed <code>reader</code> into a {@link BufferedReader} object 101 * and returns that. If the passed <code>reader</code> is already an 102 * instance of {@link BufferedReader} returns the same passed 103 * <code>reader</code> reference as is (avoiding double wrapping). 104 * 105 * @param reader the wrapee to be used for the buffering support 106 * @return the passed <code>reader</code> decorated through a 107 * {@link BufferedReader} object as wrapper 108 */ 109 public static BufferedReader buffered(Reader reader) { 110 ObjectHelper.notNull(reader, "reader"); 111 return (reader instanceof BufferedReader) ? (BufferedReader)reader : new BufferedReader(reader); 112 } 113 114 /** 115 * Wraps the passed <code>writer</code> into a {@link BufferedWriter} object 116 * and returns that. If the passed <code>writer</code> is already an 117 * instance of {@link BufferedWriter} returns the same passed 118 * <code>writer</code> reference as is (avoiding double wrapping). 119 * 120 * @param writer the wrapee to be used for the buffering support 121 * @return the passed <code>writer</code> decorated through a 122 * {@link BufferedWriter} object as wrapper 123 */ 124 public static BufferedWriter buffered(Writer writer) { 125 ObjectHelper.notNull(writer, "writer"); 126 return (writer instanceof BufferedWriter) ? (BufferedWriter)writer : new BufferedWriter(writer); 127 } 128 129 public static String toString(Reader reader) throws IOException { 130 return toString(buffered(reader)); 131 } 132 133 public static String toString(BufferedReader reader) throws IOException { 134 StringBuilder sb = new StringBuilder(1024); 135 char[] buf = new char[1024]; 136 try { 137 int len; 138 // read until we reach then end which is the -1 marker 139 while ((len = reader.read(buf)) != -1) { 140 sb.append(buf, 0, len); 141 } 142 } finally { 143 IOHelper.close(reader, "reader", LOG); 144 } 145 146 return sb.toString(); 147 } 148 149 public static int copy(InputStream input, OutputStream output) throws IOException { 150 return copy(input, output, DEFAULT_BUFFER_SIZE); 151 } 152 153 public static int copy(final InputStream input, final OutputStream output, int bufferSize) throws IOException { 154 return copy(input, output, bufferSize, false); 155 } 156 157 public static int copy(final InputStream input, final OutputStream output, int bufferSize, boolean flushOnEachWrite) throws IOException { 158 if (input instanceof ByteArrayInputStream) { 159 // optimized for byte array as we only need the max size it can be 160 input.mark(0); 161 input.reset(); 162 bufferSize = input.available(); 163 } else { 164 int avail = input.available(); 165 if (avail > bufferSize) { 166 bufferSize = avail; 167 } 168 } 169 170 if (bufferSize > 262144) { 171 // upper cap to avoid buffers too big 172 bufferSize = 262144; 173 } 174 175 if (LOG.isTraceEnabled()) { 176 LOG.trace("Copying InputStream: {} -> OutputStream: {} with buffer: {} and flush on each write {}", input, output, bufferSize, flushOnEachWrite); 177 } 178 179 int total = 0; 180 final byte[] buffer = new byte[bufferSize]; 181 int n = input.read(buffer); 182 183 boolean hasData; 184 if (ZERO_BYTE_EOL_ENABLED) { 185 // workaround issue on some application servers which can return 0 186 // (instead of -1) 187 // as first byte to indicate end of stream (CAMEL-11672) 188 hasData = n > 0; 189 } else { 190 hasData = n > -1; 191 } 192 if (hasData) { 193 while (-1 != n) { 194 output.write(buffer, 0, n); 195 if (flushOnEachWrite) { 196 output.flush(); 197 } 198 total += n; 199 n = input.read(buffer); 200 } 201 } 202 if (!flushOnEachWrite) { 203 // flush at end, if we didn't do it during the writing 204 output.flush(); 205 } 206 return total; 207 } 208 209 public static void copyAndCloseInput(InputStream input, OutputStream output) throws IOException { 210 copyAndCloseInput(input, output, DEFAULT_BUFFER_SIZE); 211 } 212 213 public static void copyAndCloseInput(InputStream input, OutputStream output, int bufferSize) throws IOException { 214 copy(input, output, bufferSize); 215 close(input, null, LOG); 216 } 217 218 public static int copy(final Reader input, final Writer output, int bufferSize) throws IOException { 219 final char[] buffer = new char[bufferSize]; 220 int n = input.read(buffer); 221 int total = 0; 222 while (-1 != n) { 223 output.write(buffer, 0, n); 224 total += n; 225 n = input.read(buffer); 226 } 227 output.flush(); 228 return total; 229 } 230 231 public static void transfer(ReadableByteChannel input, WritableByteChannel output) throws IOException { 232 ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE); 233 while (input.read(buffer) >= 0) { 234 buffer.flip(); 235 while (buffer.hasRemaining()) { 236 output.write(buffer); 237 } 238 buffer.clear(); 239 } 240 } 241 242 /** 243 * Forces any updates to this channel's file to be written to the storage 244 * device that contains it. 245 * 246 * @param channel the file channel 247 * @param name the name of the resource 248 * @param log the log to use when reporting warnings, will use this class's 249 * own {@link Logger} if <tt>log == null</tt> 250 */ 251 public static void force(FileChannel channel, String name, Logger log) { 252 try { 253 if (channel != null) { 254 channel.force(true); 255 } 256 } catch (Exception e) { 257 if (log == null) { 258 // then fallback to use the own Logger 259 log = LOG; 260 } 261 if (name != null) { 262 log.warn("Cannot force FileChannel: " + name + ". Reason: " + e.getMessage(), e); 263 } else { 264 log.warn("Cannot force FileChannel. Reason: {}", e.getMessage(), e); 265 } 266 } 267 } 268 269 /** 270 * Forces any updates to a FileOutputStream be written to the storage device 271 * 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 276 * own {@link Logger} if <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. 298 * An associated FileOutputStream can 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 302 * according to the force parameter 303 * @param name the name of the resource 304 * @param log the log to use when reporting warnings, will use this class's 305 * own {@link Logger} if <tt>log == null</tt> 306 * @param force forces the FileOutputStream to disk 307 */ 308 public static void close(Writer writer, FileOutputStream os, String name, Logger log, boolean force) { 309 if (writer != null && force) { 310 // flush the writer prior to syncing the FD 311 try { 312 writer.flush(); 313 } catch (Exception e) { 314 if (log == null) { 315 // then fallback to use the own Logger 316 log = LOG; 317 } 318 if (name != null) { 319 log.warn("Cannot flush Writer: " + name + ". Reason: " + e.getMessage(), e); 320 } else { 321 log.warn("Cannot flush Writer. Reason: {}", e.getMessage(), e); 322 } 323 } 324 force(os, name, log); 325 } 326 close(writer, name, log); 327 } 328 329 /** 330 * Closes the given resource if it is available, logging any closing 331 * exceptions to the given log. 332 * 333 * @param closeable the object to close 334 * @param name the name of the resource 335 * @param log the log to use when reporting closure warnings, will use this 336 * class's own {@link Logger} if <tt>log == null</tt> 337 */ 338 public static void close(Closeable closeable, String name, Logger log) { 339 if (closeable != null) { 340 try { 341 closeable.close(); 342 } catch (IOException e) { 343 if (log == null) { 344 // then fallback to use the own Logger 345 log = LOG; 346 } 347 if (name != null) { 348 log.warn("Cannot close: " + name + ". Reason: " + e.getMessage(), e); 349 } else { 350 log.warn("Cannot close. Reason: {}", e.getMessage(), e); 351 } 352 } 353 } 354 } 355 356 /** 357 * Closes the given resource if it is available and don't catch the 358 * exception 359 * 360 * @param closeable the object to close 361 * @throws IOException 362 */ 363 public static void closeWithException(Closeable closeable) throws IOException { 364 if (closeable != null) { 365 closeable.close(); 366 } 367 } 368 369 /** 370 * Closes the given channel if it is available, logging any closing 371 * exceptions to the given log. The file's channel can optionally be forced 372 * 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 377 * own {@link Logger} if <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 443 * terminator at the of the text. 444 * <p/> 445 * Warning, don't use for crazy big streams :) 446 */ 447 public static String loadText(InputStream in) throws IOException { 448 StringBuilder builder = new StringBuilder(); 449 InputStreamReader isr = new InputStreamReader(in); 450 try { 451 BufferedReader reader = buffered(isr); 452 while (true) { 453 String line = reader.readLine(); 454 if (line != null) { 455 builder.append(line); 456 builder.append("\n"); 457 } else { 458 break; 459 } 460 } 461 return builder.toString(); 462 } finally { 463 close(isr, in); 464 } 465 } 466 467 /** 468 * Get the charset name from the content type string 469 * 470 * @param contentType 471 * @return the charset name, or <tt>UTF-8</tt> if no found 472 */ 473 public static String getCharsetNameFromContentType(String contentType) { 474 String[] values = contentType.split(";"); 475 String charset = ""; 476 477 for (String value : values) { 478 value = value.trim(); 479 if (value.toLowerCase().startsWith("charset=")) { 480 // Take the charset name 481 charset = value.substring(8); 482 } 483 } 484 if ("".equals(charset)) { 485 charset = "UTF-8"; 486 } 487 return normalizeCharset(charset); 488 489 } 490 491 /** 492 * This method will take off the quotes and double quotes of the charset 493 */ 494 public static String normalizeCharset(String charset) { 495 if (charset != null) { 496 String answer = charset.trim(); 497 if (answer.startsWith("'") || answer.startsWith("\"")) { 498 answer = answer.substring(1); 499 } 500 if (answer.endsWith("'") || answer.endsWith("\"")) { 501 answer = answer.substring(0, answer.length() - 1); 502 } 503 return answer.trim(); 504 } else { 505 return null; 506 } 507 } 508 509 /** 510 * Lookup the OS environment variable in a safe manner by 511 * 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, UnsupportedEncodingException { 590 super(in, charset); 591 this.in = in; 592 } 593 594 @Override 595 public void close() throws IOException { 596 try { 597 super.close(); 598 } finally { 599 in.close(); 600 } 601 } 602 } 603 604 /** 605 * Encoding-aware file writer. 606 */ 607 public static class EncodingFileWriter extends OutputStreamWriter { 608 609 private final FileOutputStream out; 610 611 /** 612 * @param out file to write 613 * @param charset character set to use 614 */ 615 public EncodingFileWriter(FileOutputStream out, String charset) throws FileNotFoundException, UnsupportedEncodingException { 616 super(out, charset); 617 this.out = out; 618 } 619 620 @Override 621 public void close() throws IOException { 622 try { 623 super.close(); 624 } finally { 625 out.close(); 626 } 627 } 628 } 629 630 /** 631 * Converts the given {@link File} with the given charset to {@link InputStream} with the JVM default charset 632 * 633 * @param file the file to be converted 634 * @param charset the charset the file is read with 635 * @return the input stream with the JVM default charset 636 */ 637 public static InputStream toInputStream(File file, String charset) throws IOException { 638 if (charset != null) { 639 return new EncodingInputStream(file, charset); 640 } else { 641 return buffered(new FileInputStream(file)); 642 } 643 } 644 645 public static BufferedReader toReader(File file, String charset) throws IOException { 646 FileInputStream in = new FileInputStream(file); 647 return IOHelper.buffered(new EncodingFileReader(in, charset)); 648 } 649 650 public static BufferedWriter toWriter(FileOutputStream os, String charset) throws IOException { 651 return IOHelper.buffered(new EncodingFileWriter(os, charset)); 652 } 653}