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