blob: 565b54c5c60ccca7062cf454e32bae6a98193099 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.util;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* IO helper class.
*/
public final class IOHelper {
public static Supplier<Charset> defaultCharset = Charset::defaultCharset;
public static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
private static final Logger LOG = LoggerFactory.getLogger(IOHelper.class);
private static final Charset UTF8_CHARSET = Charset.forName("UTF-8");
// allows to turn on backwards compatible to turn off regarding the first
// read byte with value zero (0b0) as EOL.
// See more at CAMEL-11672
private static final boolean ZERO_BYTE_EOL_ENABLED = "true".equalsIgnoreCase(System.getProperty("camel.zeroByteEOLEnabled", "true"));
private IOHelper() {
// Utility Class
}
/**
* Use this function instead of new String(byte[]) to avoid surprises from
* non-standard default encodings.
*/
public static String newStringFromBytes(byte[] bytes) {
try {
return new String(bytes, UTF8_CHARSET.name());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Impossible failure: Charset.forName(\"UTF-8\") returns invalid name.", e);
}
}
/**
* Use this function instead of new String(byte[], int, int) to avoid
* surprises from non-standard default encodings.
*/
public static String newStringFromBytes(byte[] bytes, int start, int length) {
try {
return new String(bytes, start, length, UTF8_CHARSET.name());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException("Impossible failure: Charset.forName(\"UTF-8\") returns invalid name.", e);
}
}
/**
* Wraps the passed <code>in</code> into a {@link BufferedInputStream}
* object and returns that. If the passed <code>in</code> is already an
* instance of {@link BufferedInputStream} returns the same passed
* <code>in</code> reference as is (avoiding double wrapping).
*
* @param in the wrapee to be used for the buffering support
* @return the passed <code>in</code> decorated through a
* {@link BufferedInputStream} object as wrapper
*/
public static BufferedInputStream buffered(InputStream in) {
ObjectHelper.notNull(in, "in");
return (in instanceof BufferedInputStream) ? (BufferedInputStream)in : new BufferedInputStream(in);
}
/**
* Wraps the passed <code>out</code> into a {@link BufferedOutputStream}
* object and returns that. If the passed <code>out</code> is already an
* instance of {@link BufferedOutputStream} returns the same passed
* <code>out</code> reference as is (avoiding double wrapping).
*
* @param out the wrapee to be used for the buffering support
* @return the passed <code>out</code> decorated through a
* {@link BufferedOutputStream} object as wrapper
*/
public static BufferedOutputStream buffered(OutputStream out) {
ObjectHelper.notNull(out, "out");
return (out instanceof BufferedOutputStream) ? (BufferedOutputStream)out : new BufferedOutputStream(out);
}
/**
* Wraps the passed <code>reader</code> into a {@link BufferedReader} object
* and returns that. If the passed <code>reader</code> is already an
* instance of {@link BufferedReader} returns the same passed
* <code>reader</code> reference as is (avoiding double wrapping).
*
* @param reader the wrapee to be used for the buffering support
* @return the passed <code>reader</code> decorated through a
* {@link BufferedReader} object as wrapper
*/
public static BufferedReader buffered(Reader reader) {
ObjectHelper.notNull(reader, "reader");
return (reader instanceof BufferedReader) ? (BufferedReader)reader : new BufferedReader(reader);
}
/**
* Wraps the passed <code>writer</code> into a {@link BufferedWriter} object
* and returns that. If the passed <code>writer</code> is already an
* instance of {@link BufferedWriter} returns the same passed
* <code>writer</code> reference as is (avoiding double wrapping).
*
* @param writer the wrapee to be used for the buffering support
* @return the passed <code>writer</code> decorated through a
* {@link BufferedWriter} object as wrapper
*/
public static BufferedWriter buffered(Writer writer) {
ObjectHelper.notNull(writer, "writer");
return (writer instanceof BufferedWriter) ? (BufferedWriter)writer : new BufferedWriter(writer);
}
public static String toString(Reader reader) throws IOException {
return toString(buffered(reader));
}
public static String toString(BufferedReader reader) throws IOException {
StringBuilder sb = new StringBuilder(1024);
char[] buf = new char[1024];
try {
int len;
// read until we reach then end which is the -1 marker
while ((len = reader.read(buf)) != -1) {
sb.append(buf, 0, len);
}
} finally {
IOHelper.close(reader, "reader", LOG);
}
return sb.toString();
}
public static int copy(InputStream input, OutputStream output) throws IOException {
return copy(input, output, DEFAULT_BUFFER_SIZE);
}
public static int copy(final InputStream input, final OutputStream output, int bufferSize) throws IOException {
return copy(input, output, bufferSize, false);
}
public static int copy(final InputStream input, final OutputStream output, int bufferSize, boolean flushOnEachWrite) throws IOException {
if (input instanceof ByteArrayInputStream) {
// optimized for byte array as we only need the max size it can be
input.mark(0);
input.reset();
bufferSize = input.available();
} else {
int avail = input.available();
if (avail > bufferSize) {
bufferSize = avail;
}
}
if (bufferSize > 262144) {
// upper cap to avoid buffers too big
bufferSize = 262144;
}
if (LOG.isTraceEnabled()) {
LOG.trace("Copying InputStream: {} -> OutputStream: {} with buffer: {} and flush on each write {}", input, output, bufferSize, flushOnEachWrite);
}
int total = 0;
final byte[] buffer = new byte[bufferSize];
int n = input.read(buffer);
boolean hasData;
if (ZERO_BYTE_EOL_ENABLED) {
// workaround issue on some application servers which can return 0
// (instead of -1)
// as first byte to indicate end of stream (CAMEL-11672)
hasData = n > 0;
} else {
hasData = n > -1;
}
if (hasData) {
while (-1 != n) {
output.write(buffer, 0, n);
if (flushOnEachWrite) {
output.flush();
}
total += n;
n = input.read(buffer);
}
}
if (!flushOnEachWrite) {
// flush at end, if we didn't do it during the writing
output.flush();
}
return total;
}
public static void copyAndCloseInput(InputStream input, OutputStream output) throws IOException {
copyAndCloseInput(input, output, DEFAULT_BUFFER_SIZE);
}
public static void copyAndCloseInput(InputStream input, OutputStream output, int bufferSize) throws IOException {
copy(input, output, bufferSize);
close(input, null, LOG);
}
public static int copy(final Reader input, final Writer output, int bufferSize) throws IOException {
final char[] buffer = new char[bufferSize];
int n = input.read(buffer);
int total = 0;
while (-1 != n) {
output.write(buffer, 0, n);
total += n;
n = input.read(buffer);
}
output.flush();
return total;
}
/**
* Forces any updates to this channel's file to be written to the storage
* device that contains it.
*
* @param channel the file channel
* @param name the name of the resource
* @param log the log to use when reporting warnings, will use this class's
* own {@link Logger} if <tt>log == null</tt>
*/
public static void force(FileChannel channel, String name, Logger log) {
try {
if (channel != null) {
channel.force(true);
}
} catch (Exception e) {
if (log == null) {
// then fallback to use the own Logger
log = LOG;
}
if (name != null) {
log.warn("Cannot force FileChannel: " + name + ". Reason: " + e.getMessage(), e);
} else {
log.warn("Cannot force FileChannel. Reason: {}", e.getMessage(), e);
}
}
}
/**
* Forces any updates to a FileOutputStream be written to the storage device
* that contains it.
*
* @param os the file output stream
* @param name the name of the resource
* @param log the log to use when reporting warnings, will use this class's
* own {@link Logger} if <tt>log == null</tt>
*/
public static void force(FileOutputStream os, String name, Logger log) {
try {
if (os != null) {
os.getFD().sync();
}
} catch (Exception e) {
if (log == null) {
// then fallback to use the own Logger
log = LOG;
}
if (name != null) {
log.warn("Cannot sync FileDescriptor: " + name + ". Reason: " + e.getMessage(), e);
} else {
log.warn("Cannot sync FileDescriptor. Reason: {}", e.getMessage(), e);
}
}
}
/**
* Closes the given writer, logging any closing exceptions to the given log.
* An associated FileOutputStream can optionally be forced to disk.
*
* @param writer the writer to close
* @param os an underlying FileOutputStream that will to be forced to disk
* according to the force parameter
* @param name the name of the resource
* @param log the log to use when reporting warnings, will use this class's
* own {@link Logger} if <tt>log == null</tt>
* @param force forces the FileOutputStream to disk
*/
public static void close(Writer writer, FileOutputStream os, String name, Logger log, boolean force) {
if (writer != null && force) {
// flush the writer prior to syncing the FD
try {
writer.flush();
} catch (Exception e) {
if (log == null) {
// then fallback to use the own Logger
log = LOG;
}
if (name != null) {
log.warn("Cannot flush Writer: " + name + ". Reason: " + e.getMessage(), e);
} else {
log.warn("Cannot flush Writer. Reason: {}", e.getMessage(), e);
}
}
force(os, name, log);
}
close(writer, name, log);
}
/**
* Closes the given resource if it is available, logging any closing
* exceptions to the given log.
*
* @param closeable the object to close
* @param name the name of the resource
* @param log the log to use when reporting closure warnings, will use this
* class's own {@link Logger} if <tt>log == null</tt>
*/
public static void close(Closeable closeable, String name, Logger log) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
if (log == null) {
// then fallback to use the own Logger
log = LOG;
}
if (name != null) {
log.warn("Cannot close: " + name + ". Reason: " + e.getMessage(), e);
} else {
log.warn("Cannot close. Reason: {}", e.getMessage(), e);
}
}
}
}
/**
* Closes the given resource if it is available and don't catch the
* exception
*
* @param closeable the object to close
* @throws IOException
*/
public static void closeWithException(Closeable closeable) throws IOException {
if (closeable != null) {
closeable.close();
}
}
/**
* Closes the given channel if it is available, logging any closing
* exceptions to the given log. The file's channel can optionally be forced
* to disk.
*
* @param channel the file channel
* @param name the name of the resource
* @param log the log to use when reporting warnings, will use this class's
* own {@link Logger} if <tt>log == null</tt>
* @param force forces the file channel to disk
*/
public static void close(FileChannel channel, String name, Logger log, boolean force) {
if (force) {
force(channel, name, log);
}
close(channel, name, log);
}
/**
* Closes the given resource if it is available.
*
* @param closeable the object to close
* @param name the name of the resource
*/
public static void close(Closeable closeable, String name) {
close(closeable, name, LOG);
}
/**
* Closes the given resource if it is available.
*
* @param closeable the object to close
*/
public static void close(Closeable closeable) {
close(closeable, null, LOG);
}
/**
* Closes the given resources if they are available.
*
* @param closeables the objects to close
*/
public static void close(Closeable... closeables) {
for (Closeable closeable : closeables) {
close(closeable);
}
}
public static void closeIterator(Object it) throws IOException {
if (it instanceof Closeable) {
IOHelper.closeWithException((Closeable)it);
}
if (it instanceof java.util.Scanner) {
IOException ioException = ((java.util.Scanner)it).ioException();
if (ioException != null) {
throw ioException;
}
}
}
public static void validateCharset(String charset) throws UnsupportedCharsetException {
if (charset != null) {
if (Charset.isSupported(charset)) {
Charset.forName(charset);
return;
}
}
throw new UnsupportedCharsetException(charset);
}
/**
* Loads the entire stream into memory as a String and returns it.
* <p/>
* <b>Notice:</b> This implementation appends a <tt>\n</tt> as line
* terminator at the of the text.
* <p/>
* Warning, don't use for crazy big streams :)
*/
public static String loadText(InputStream in) throws IOException {
StringBuilder builder = new StringBuilder();
InputStreamReader isr = new InputStreamReader(in);
try {
BufferedReader reader = buffered(isr);
while (true) {
String line = reader.readLine();
if (line != null) {
builder.append(line);
builder.append("\n");
} else {
break;
}
}
return builder.toString();
} finally {
close(isr, in);
}
}
/**
* Get the charset name from the content type string
*
* @param contentType
* @return the charset name, or <tt>UTF-8</tt> if no found
*/
public static String getCharsetNameFromContentType(String contentType) {
String[] values = contentType.split(";");
String charset = "";
for (String value : values) {
value = value.trim();
if (value.toLowerCase().startsWith("charset=")) {
// Take the charset name
charset = value.substring(8);
}
}
if ("".equals(charset)) {
charset = "UTF-8";
}
return normalizeCharset(charset);
}
/**
* This method will take off the quotes and double quotes of the charset
*/
public static String normalizeCharset(String charset) {
if (charset != null) {
String answer = charset.trim();
if (answer.startsWith("'") || answer.startsWith("\"")) {
answer = answer.substring(1);
}
if (answer.endsWith("'") || answer.endsWith("\"")) {
answer = answer.substring(0, answer.length() - 1);
}
return answer.trim();
} else {
return null;
}
}
/**
* Lookup the OS environment variable in a safe manner by
* using upper case keys and underscore instead of dash.
*/
public static String lookupEnvironmentVariable(String key) {
// lookup OS env with upper case key
String upperKey = key.toUpperCase();
String value = System.getenv(upperKey);
// some OS do not support dashes in keys, so replace with underscore
if (value == null) {
String noDashKey = upperKey.replace('-', '_');
value = System.getenv(noDashKey);
}
return value;
}
/**
* Encoding-aware input stream.
*/
public static class EncodingInputStream extends InputStream {
private final File file;
private final BufferedReader reader;
private final Charset defaultStreamCharset;
private ByteBuffer bufferBytes;
private CharBuffer bufferedChars = CharBuffer.allocate(4096);
public EncodingInputStream(File file, String charset) throws IOException {
this.file = file;
reader = toReader(file, charset);
defaultStreamCharset = defaultCharset.get();
}
@Override
public int read() throws IOException {
if (bufferBytes == null || bufferBytes.remaining() <= 0) {
bufferedChars.clear();
int len = reader.read(bufferedChars);
bufferedChars.flip();
if (len == -1) {
return -1;
}
bufferBytes = defaultStreamCharset.encode(bufferedChars);
}
return bufferBytes.get();
}
@Override
public void close() throws IOException {
reader.close();
}
@Override
public void reset() throws IOException {
reader.reset();
}
public InputStream toOriginalInputStream() throws FileNotFoundException {
return new FileInputStream(file);
}
}
/**
* Encoding-aware file reader.
*/
public static class EncodingFileReader extends InputStreamReader {
private final FileInputStream in;
/**
* @param in file to read
* @param charset character set to use
*/
public EncodingFileReader(FileInputStream in, String charset) throws FileNotFoundException, UnsupportedEncodingException {
super(in, charset);
this.in = in;
}
@Override
public void close() throws IOException {
try {
super.close();
} finally {
in.close();
}
}
}
/**
* Encoding-aware file writer.
*/
public static class EncodingFileWriter extends OutputStreamWriter {
private final FileOutputStream out;
/**
* @param out file to write
* @param charset character set to use
*/
public EncodingFileWriter(FileOutputStream out, String charset) throws FileNotFoundException, UnsupportedEncodingException {
super(out, charset);
this.out = out;
}
@Override
public void close() throws IOException {
try {
super.close();
} finally {
out.close();
}
}
}
/**
* Converts the given {@link File} with the given charset to {@link InputStream} with the JVM default charset
*
* @param file the file to be converted
* @param charset the charset the file is read with
* @return the input stream with the JVM default charset
*/
public static InputStream toInputStream(File file, String charset) throws IOException {
if (charset != null) {
return new EncodingInputStream(file, charset);
} else {
return buffered(new FileInputStream(file));
}
}
public static BufferedReader toReader(File file, String charset) throws IOException {
FileInputStream in = new FileInputStream(file);
return IOHelper.buffered(new EncodingFileReader(in, charset));
}
public static BufferedWriter toWriter(FileOutputStream os, String charset) throws IOException {
return IOHelper.buffered(new EncodingFileWriter(os, charset));
}
}