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