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        return copy(input, output, bufferSize, flushOnEachWrite, -1);
153    }
154
155    public static int copy(
156            final InputStream input, final OutputStream output, int bufferSize, boolean flushOnEachWrite,
157            long maxSize)
158            throws IOException {
159
160        if (input instanceof ByteArrayInputStream) {
161            // optimized for byte array as we only need the max size it can be
162            input.mark(0);
163            input.reset();
164            bufferSize = input.available();
165        } else {
166            int avail = input.available();
167            if (avail > bufferSize) {
168                bufferSize = avail;
169            }
170        }
171
172        if (bufferSize > 262144) {
173            // upper cap to avoid buffers too big
174            bufferSize = 262144;
175        }
176
177        if (LOG.isTraceEnabled()) {
178            LOG.trace("Copying InputStream: {} -> OutputStream: {} with buffer: {} and flush on each write {}", input, output,
179                    bufferSize, flushOnEachWrite);
180        }
181
182        int total = 0;
183        final byte[] buffer = new byte[bufferSize];
184        int n = input.read(buffer);
185
186        boolean hasData;
187        if (ZERO_BYTE_EOL_ENABLED) {
188            // workaround issue on some application servers which can return 0
189            // (instead of -1)
190            // as first byte to indicate end of stream (CAMEL-11672)
191            hasData = n > 0;
192        } else {
193            hasData = n > -1;
194        }
195        if (hasData) {
196            while (-1 != n) {
197                output.write(buffer, 0, n);
198                if (flushOnEachWrite) {
199                    output.flush();
200                }
201                total += n;
202                if (maxSize > 0 && total > maxSize) {
203                    throw new IOException("The InputStream entry being copied exceeds the maximum allowed size");
204                }
205                n = input.read(buffer);
206            }
207        }
208        if (!flushOnEachWrite) {
209            // flush at end, if we didn't do it during the writing
210            output.flush();
211        }
212        return total;
213    }
214
215    public static void copyAndCloseInput(InputStream input, OutputStream output) throws IOException {
216        copyAndCloseInput(input, output, DEFAULT_BUFFER_SIZE);
217    }
218
219    public static void copyAndCloseInput(InputStream input, OutputStream output, int bufferSize) throws IOException {
220        copy(input, output, bufferSize);
221        close(input, null, LOG);
222    }
223
224    public static int copy(final Reader input, final Writer output, int bufferSize) throws IOException {
225        final char[] buffer = new char[bufferSize];
226        int n = input.read(buffer);
227        int total = 0;
228        while (-1 != n) {
229            output.write(buffer, 0, n);
230            total += n;
231            n = input.read(buffer);
232        }
233        output.flush();
234        return total;
235    }
236
237    public static void transfer(ReadableByteChannel input, WritableByteChannel output) throws IOException {
238        ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
239        while (input.read(buffer) >= 0) {
240            buffer.flip();
241            while (buffer.hasRemaining()) {
242                output.write(buffer);
243            }
244            buffer.clear();
245        }
246    }
247
248    /**
249     * Forces any updates to this channel's file to be written to the storage device that contains it.
250     *
251     * @param channel the file channel
252     * @param name    the name of the resource
253     * @param log     the log to use when reporting warnings, will use this class's own {@link Logger} if
254     *                <tt>log == null</tt>
255     */
256    public static void force(FileChannel channel, String name, Logger log) {
257        try {
258            if (channel != null) {
259                channel.force(true);
260            }
261        } catch (Exception e) {
262            if (log == null) {
263                // then fallback to use the own Logger
264                log = LOG;
265            }
266            if (name != null) {
267                log.warn("Cannot force FileChannel: " + name + ". Reason: " + e.getMessage(), e);
268            } else {
269                log.warn("Cannot force FileChannel. Reason: {}", e.getMessage(), e);
270            }
271        }
272    }
273
274    /**
275     * Forces any updates to a FileOutputStream be written to the storage device that contains it.
276     *
277     * @param os   the file output stream
278     * @param name the name of the resource
279     * @param log  the log to use when reporting warnings, will use this class's own {@link Logger} if
280     *             <tt>log == null</tt>
281     */
282    public static void force(FileOutputStream os, String name, Logger log) {
283        try {
284            if (os != null) {
285                os.getFD().sync();
286            }
287        } catch (Exception e) {
288            if (log == null) {
289                // then fallback to use the own Logger
290                log = LOG;
291            }
292            if (name != null) {
293                log.warn("Cannot sync FileDescriptor: " + name + ". Reason: " + e.getMessage(), e);
294            } else {
295                log.warn("Cannot sync FileDescriptor. Reason: {}", e.getMessage(), e);
296            }
297        }
298    }
299
300    /**
301     * Closes the given writer, logging any closing exceptions to the given log. An associated FileOutputStream can
302     * optionally be forced to disk.
303     *
304     * @param writer the writer to close
305     * @param os     an underlying FileOutputStream that will to be forced to disk according to the force parameter
306     * @param name   the name of the resource
307     * @param log    the log to use when reporting warnings, will use this class's own {@link Logger} if
308     *               <tt>log == null</tt>
309     * @param force  forces the FileOutputStream to disk
310     */
311    public static void close(Writer writer, FileOutputStream os, String name, Logger log, boolean force) {
312        if (writer != null && force) {
313            // flush the writer prior to syncing the FD
314            try {
315                writer.flush();
316            } catch (Exception e) {
317                if (log == null) {
318                    // then fallback to use the own Logger
319                    log = LOG;
320                }
321                if (name != null) {
322                    log.warn("Cannot flush Writer: " + name + ". Reason: " + e.getMessage(), e);
323                } else {
324                    log.warn("Cannot flush Writer. Reason: {}", e.getMessage(), e);
325                }
326            }
327            force(os, name, log);
328        }
329        close(writer, name, log);
330    }
331
332    /**
333     * Closes the given resource if it is available, logging any closing exceptions to the given log.
334     *
335     * @param closeable the object to close
336     * @param name      the name of the resource
337     * @param log       the log to use when reporting closure warnings, will use this class's own {@link Logger} if
338     *                  <tt>log == null</tt>
339     */
340    public static void close(Closeable closeable, String name, Logger log) {
341        if (closeable != null) {
342            try {
343                closeable.close();
344            } catch (IOException e) {
345                if (log == null) {
346                    // then fallback to use the own Logger
347                    log = LOG;
348                }
349                if (name != null) {
350                    log.warn("Cannot close: " + name + ". Reason: " + e.getMessage(), e);
351                } else {
352                    log.warn("Cannot close. Reason: {}", e.getMessage(), e);
353                }
354            }
355        }
356    }
357
358    /**
359     * Closes the given resource if it is available and don't catch the exception
360     *
361     * @param  closeable   the object to close
362     * @throws IOException
363     */
364    public static void closeWithException(Closeable closeable) throws IOException {
365        if (closeable != null) {
366            closeable.close();
367        }
368    }
369
370    /**
371     * Closes the given channel if it is available, logging any closing exceptions to the given log. The file's channel
372     * can optionally be forced 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 own {@link Logger} if
377     *                <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 terminator at the of the text.
443     * <p/>
444     * Warning, don't use for crazy big streams :)
445     */
446    public static String loadText(InputStream in) throws IOException {
447        StringBuilder builder = new StringBuilder();
448        InputStreamReader isr = new InputStreamReader(in);
449        try {
450            BufferedReader reader = buffered(isr);
451            while (true) {
452                String line = reader.readLine();
453                if (line != null) {
454                    builder.append(line);
455                    builder.append("\n");
456                } else {
457                    break;
458                }
459            }
460            return builder.toString();
461        } finally {
462            close(isr, in);
463        }
464    }
465
466    /**
467     * Get the charset name from the content type string
468     *
469     * @param  contentType
470     * @return             the charset name, or <tt>UTF-8</tt> if no found
471     */
472    public static String getCharsetNameFromContentType(String contentType) {
473        String[] values = contentType.split(";");
474        String charset = "";
475
476        for (String value : values) {
477            value = value.trim();
478            // Perform a case insensitive "startsWith" check that works for different locales
479            String prefix = "charset=";
480            if (value.regionMatches(true, 0, prefix, 0, prefix.length())) {
481                // Take the charset name
482                charset = value.substring(8);
483            }
484        }
485        if ("".equals(charset)) {
486            charset = "UTF-8";
487        }
488        return normalizeCharset(charset);
489
490    }
491
492    /**
493     * This method will take off the quotes and double quotes of the charset
494     */
495    public static String normalizeCharset(String charset) {
496        if (charset != null) {
497            String answer = charset.trim();
498            if (answer.startsWith("'") || answer.startsWith("\"")) {
499                answer = answer.substring(1);
500            }
501            if (answer.endsWith("'") || answer.endsWith("\"")) {
502                answer = answer.substring(0, answer.length() - 1);
503            }
504            return answer.trim();
505        } else {
506            return null;
507        }
508    }
509
510    /**
511     * Lookup the OS environment variable in a safe manner by 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,
590                                                                      UnsupportedEncodingException {
591            super(in, charset);
592            this.in = in;
593        }
594
595        @Override
596        public void close() throws IOException {
597            try {
598                super.close();
599            } finally {
600                in.close();
601            }
602        }
603    }
604
605    /**
606     * Encoding-aware file writer.
607     */
608    public static class EncodingFileWriter extends OutputStreamWriter {
609
610        private final FileOutputStream out;
611
612        /**
613         * @param out     file to write
614         * @param charset character set to use
615         */
616        public EncodingFileWriter(FileOutputStream out, String charset) throws FileNotFoundException,
617                                                                        UnsupportedEncodingException {
618            super(out, charset);
619            this.out = out;
620        }
621
622        @Override
623        public void close() throws IOException {
624            try {
625                super.close();
626            } finally {
627                out.close();
628            }
629        }
630    }
631
632    /**
633     * Converts the given {@link File} with the given charset to {@link InputStream} with the JVM default charset
634     *
635     * @param  file    the file to be converted
636     * @param  charset the charset the file is read with
637     * @return         the input stream with the JVM default charset
638     */
639    public static InputStream toInputStream(File file, String charset) throws IOException {
640        if (charset != null) {
641            return new EncodingInputStream(file, charset);
642        } else {
643            return buffered(new FileInputStream(file));
644        }
645    }
646
647    public static BufferedReader toReader(File file, String charset) throws IOException {
648        FileInputStream in = new FileInputStream(file);
649        return IOHelper.buffered(new EncodingFileReader(in, charset));
650    }
651
652    public static BufferedWriter toWriter(FileOutputStream os, String charset) throws IOException {
653        return IOHelper.buffered(new EncodingFileWriter(os, charset));
654    }
655}