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