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 = "true".equalsIgnoreCase(System.getProperty("camel.zeroByteEOLEnabled", "true"));
064
065    private IOHelper() {
066        // Utility Class
067    }
068
069    /**
070     * Wraps the passed <code>in</code> into a {@link BufferedInputStream}
071     * object and returns that. If the passed <code>in</code> is already an
072     * instance of {@link BufferedInputStream} returns the same passed
073     * <code>in</code> 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
077     *         {@link BufferedInputStream} object as wrapper
078     */
079    public static BufferedInputStream buffered(InputStream in) {
080        ObjectHelper.notNull(in, "in");
081        return (in instanceof BufferedInputStream) ? (BufferedInputStream)in : new BufferedInputStream(in);
082    }
083
084    /**
085     * Wraps the passed <code>out</code> into a {@link BufferedOutputStream}
086     * object and returns that. If the passed <code>out</code> is already an
087     * instance of {@link BufferedOutputStream} returns the same passed
088     * <code>out</code> reference as is (avoiding double wrapping).
089     *
090     * @param out the wrapee to be used for the buffering support
091     * @return the passed <code>out</code> decorated through a
092     *         {@link BufferedOutputStream} object as wrapper
093     */
094    public static BufferedOutputStream buffered(OutputStream out) {
095        ObjectHelper.notNull(out, "out");
096        return (out instanceof BufferedOutputStream) ? (BufferedOutputStream)out : new BufferedOutputStream(out);
097    }
098
099    /**
100     * Wraps the passed <code>reader</code> into a {@link BufferedReader} object
101     * and returns that. If the passed <code>reader</code> is already an
102     * instance of {@link BufferedReader} returns the same passed
103     * <code>reader</code> reference as is (avoiding double wrapping).
104     *
105     * @param reader the wrapee to be used for the buffering support
106     * @return the passed <code>reader</code> decorated through a
107     *         {@link BufferedReader} object as wrapper
108     */
109    public static BufferedReader buffered(Reader reader) {
110        ObjectHelper.notNull(reader, "reader");
111        return (reader instanceof BufferedReader) ? (BufferedReader)reader : new BufferedReader(reader);
112    }
113
114    /**
115     * Wraps the passed <code>writer</code> into a {@link BufferedWriter} object
116     * and returns that. If the passed <code>writer</code> is already an
117     * instance of {@link BufferedWriter} returns the same passed
118     * <code>writer</code> reference as is (avoiding double wrapping).
119     *
120     * @param writer the wrapee to be used for the buffering support
121     * @return the passed <code>writer</code> decorated through a
122     *         {@link BufferedWriter} object as wrapper
123     */
124    public static BufferedWriter buffered(Writer writer) {
125        ObjectHelper.notNull(writer, "writer");
126        return (writer instanceof BufferedWriter) ? (BufferedWriter)writer : new BufferedWriter(writer);
127    }
128
129    public static String toString(Reader reader) throws IOException {
130        return toString(buffered(reader));
131    }
132
133    public static String toString(BufferedReader reader) throws IOException {
134        StringBuilder sb = new StringBuilder(1024);
135        char[] buf = new char[1024];
136        try {
137            int len;
138            // read until we reach then end which is the -1 marker
139            while ((len = reader.read(buf)) != -1) {
140                sb.append(buf, 0, len);
141            }
142        } finally {
143            IOHelper.close(reader, "reader", LOG);
144        }
145
146        return sb.toString();
147    }
148
149    public static int copy(InputStream input, OutputStream output) throws IOException {
150        return copy(input, output, DEFAULT_BUFFER_SIZE);
151    }
152
153    public static int copy(final InputStream input, final OutputStream output, int bufferSize) throws IOException {
154        return copy(input, output, bufferSize, false);
155    }
156
157    public static int copy(final InputStream input, final OutputStream output, int bufferSize, boolean flushOnEachWrite) throws IOException {
158        if (input instanceof ByteArrayInputStream) {
159            // optimized for byte array as we only need the max size it can be
160            input.mark(0);
161            input.reset();
162            bufferSize = input.available();
163        } else {
164            int avail = input.available();
165            if (avail > bufferSize) {
166                bufferSize = avail;
167            }
168        }
169
170        if (bufferSize > 262144) {
171            // upper cap to avoid buffers too big
172            bufferSize = 262144;
173        }
174
175        if (LOG.isTraceEnabled()) {
176            LOG.trace("Copying InputStream: {} -> OutputStream: {} with buffer: {} and flush on each write {}", input, output, bufferSize, flushOnEachWrite);
177        }
178
179        int total = 0;
180        final byte[] buffer = new byte[bufferSize];
181        int n = input.read(buffer);
182
183        boolean hasData;
184        if (ZERO_BYTE_EOL_ENABLED) {
185            // workaround issue on some application servers which can return 0
186            // (instead of -1)
187            // as first byte to indicate end of stream (CAMEL-11672)
188            hasData = n > 0;
189        } else {
190            hasData = n > -1;
191        }
192        if (hasData) {
193            while (-1 != n) {
194                output.write(buffer, 0, n);
195                if (flushOnEachWrite) {
196                    output.flush();
197                }
198                total += n;
199                n = input.read(buffer);
200            }
201        }
202        if (!flushOnEachWrite) {
203            // flush at end, if we didn't do it during the writing
204            output.flush();
205        }
206        return total;
207    }
208
209    public static void copyAndCloseInput(InputStream input, OutputStream output) throws IOException {
210        copyAndCloseInput(input, output, DEFAULT_BUFFER_SIZE);
211    }
212
213    public static void copyAndCloseInput(InputStream input, OutputStream output, int bufferSize) throws IOException {
214        copy(input, output, bufferSize);
215        close(input, null, LOG);
216    }
217
218    public static int copy(final Reader input, final Writer output, int bufferSize) throws IOException {
219        final char[] buffer = new char[bufferSize];
220        int n = input.read(buffer);
221        int total = 0;
222        while (-1 != n) {
223            output.write(buffer, 0, n);
224            total += n;
225            n = input.read(buffer);
226        }
227        output.flush();
228        return total;
229    }
230
231    public static void transfer(ReadableByteChannel input, WritableByteChannel output) throws IOException {
232        ByteBuffer buffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
233        while (input.read(buffer) >= 0) {
234            buffer.flip();
235            while (buffer.hasRemaining()) {
236                output.write(buffer);
237            }
238            buffer.clear();
239        }
240    }
241
242    /**
243     * Forces any updates to this channel's file to be written to the storage
244     * device that contains it.
245     *
246     * @param channel the file channel
247     * @param name the name of the resource
248     * @param log the log to use when reporting warnings, will use this class's
249     *            own {@link Logger} if <tt>log == null</tt>
250     */
251    public static void force(FileChannel channel, String name, Logger log) {
252        try {
253            if (channel != null) {
254                channel.force(true);
255            }
256        } catch (Exception e) {
257            if (log == null) {
258                // then fallback to use the own Logger
259                log = LOG;
260            }
261            if (name != null) {
262                log.warn("Cannot force FileChannel: " + name + ". Reason: " + e.getMessage(), e);
263            } else {
264                log.warn("Cannot force FileChannel. Reason: {}", e.getMessage(), e);
265            }
266        }
267    }
268
269    /**
270     * Forces any updates to a FileOutputStream be written to the storage device
271     * 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
276     *            own {@link Logger} if <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.
298     * An associated FileOutputStream can 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
302     *            according to the force parameter
303     * @param name the name of the resource
304     * @param log the log to use when reporting warnings, will use this class's
305     *            own {@link Logger} if <tt>log == null</tt>
306     * @param force forces the FileOutputStream to disk
307     */
308    public static void close(Writer writer, FileOutputStream os, String name, Logger log, boolean force) {
309        if (writer != null && force) {
310            // flush the writer prior to syncing the FD
311            try {
312                writer.flush();
313            } catch (Exception e) {
314                if (log == null) {
315                    // then fallback to use the own Logger
316                    log = LOG;
317                }
318                if (name != null) {
319                    log.warn("Cannot flush Writer: " + name + ". Reason: " + e.getMessage(), e);
320                } else {
321                    log.warn("Cannot flush Writer. Reason: {}", e.getMessage(), e);
322                }
323            }
324            force(os, name, log);
325        }
326        close(writer, name, log);
327    }
328
329    /**
330     * Closes the given resource if it is available, logging any closing
331     * exceptions to the given log.
332     *
333     * @param closeable the object to close
334     * @param name the name of the resource
335     * @param log the log to use when reporting closure warnings, will use this
336     *            class's own {@link Logger} if <tt>log == null</tt>
337     */
338    public static void close(Closeable closeable, String name, Logger log) {
339        if (closeable != null) {
340            try {
341                closeable.close();
342            } catch (IOException e) {
343                if (log == null) {
344                    // then fallback to use the own Logger
345                    log = LOG;
346                }
347                if (name != null) {
348                    log.warn("Cannot close: " + name + ". Reason: " + e.getMessage(), e);
349                } else {
350                    log.warn("Cannot close. Reason: {}", e.getMessage(), e);
351                }
352            }
353        }
354    }
355
356    /**
357     * Closes the given resource if it is available and don't catch the
358     * exception
359     *
360     * @param closeable the object to close
361     * @throws IOException
362     */
363    public static void closeWithException(Closeable closeable) throws IOException {
364        if (closeable != null) {
365            closeable.close();
366        }
367    }
368
369    /**
370     * Closes the given channel if it is available, logging any closing
371     * exceptions to the given log. The file's channel can optionally be forced
372     * 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
377     *            own {@link Logger} if <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
443     * terminator at the of the text.
444     * <p/>
445     * Warning, don't use for crazy big streams :)
446     */
447    public static String loadText(InputStream in) throws IOException {
448        StringBuilder builder = new StringBuilder();
449        InputStreamReader isr = new InputStreamReader(in);
450        try {
451            BufferedReader reader = buffered(isr);
452            while (true) {
453                String line = reader.readLine();
454                if (line != null) {
455                    builder.append(line);
456                    builder.append("\n");
457                } else {
458                    break;
459                }
460            }
461            return builder.toString();
462        } finally {
463            close(isr, in);
464        }
465    }
466
467    /**
468     * Get the charset name from the content type string
469     *
470     * @param contentType
471     * @return the charset name, or <tt>UTF-8</tt> if no found
472     */
473    public static String getCharsetNameFromContentType(String contentType) {
474        String[] values = contentType.split(";");
475        String charset = "";
476
477        for (String value : values) {
478            value = value.trim();
479            if (value.toLowerCase().startsWith("charset=")) {
480                // Take the charset name
481                charset = value.substring(8);
482            }
483        }
484        if ("".equals(charset)) {
485            charset = "UTF-8";
486        }
487        return normalizeCharset(charset);
488
489    }
490
491    /**
492     * This method will take off the quotes and double quotes of the charset
493     */
494    public static String normalizeCharset(String charset) {
495        if (charset != null) {
496            String answer = charset.trim();
497            if (answer.startsWith("'") || answer.startsWith("\"")) {
498                answer = answer.substring(1);
499            }
500            if (answer.endsWith("'") || answer.endsWith("\"")) {
501                answer = answer.substring(0, answer.length() - 1);
502            }
503            return answer.trim();
504        } else {
505            return null;
506        }
507    }
508
509    /**
510     * Lookup the OS environment variable in a safe manner by
511     * 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, UnsupportedEncodingException {
590            super(in, charset);
591            this.in = in;
592        }
593
594        @Override
595        public void close() throws IOException {
596            try {
597                super.close();
598            } finally {
599                in.close();
600            }
601        }
602    }
603
604    /**
605     * Encoding-aware file writer.
606     */
607    public static class EncodingFileWriter extends OutputStreamWriter {
608
609        private final FileOutputStream out;
610
611        /**
612         * @param out file to write
613         * @param charset character set to use
614         */
615        public EncodingFileWriter(FileOutputStream out, String charset) throws FileNotFoundException, UnsupportedEncodingException {
616            super(out, charset);
617            this.out = out;
618        }
619
620        @Override
621        public void close() throws IOException {
622            try {
623                super.close();
624            } finally {
625                out.close();
626            }
627        }
628    }
629
630    /**
631     * Converts the given {@link File} with the given charset to {@link InputStream} with the JVM default charset
632     *
633     * @param file the file to be converted
634     * @param charset the charset the file is read with
635     * @return the input stream with the JVM default charset
636     */
637    public static InputStream toInputStream(File file, String charset) throws IOException {
638        if (charset != null) {
639            return new EncodingInputStream(file, charset);
640        } else {
641            return buffered(new FileInputStream(file));
642        }
643    }
644
645    public static BufferedReader toReader(File file, String charset) throws IOException {
646        FileInputStream in = new FileInputStream(file);
647        return IOHelper.buffered(new EncodingFileReader(in, charset));
648    }
649
650    public static BufferedWriter toWriter(FileOutputStream os, String charset) throws IOException {
651        return IOHelper.buffered(new EncodingFileWriter(os, charset));
652    }
653}