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