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