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.juneau.commons.utils;
018
019import static org.apache.juneau.commons.utils.Exceptions.*;
020import static org.apache.juneau.commons.utils.ObjectUtils.*;
021import static org.apache.juneau.commons.utils.Shorts.f;
022import static org.apache.juneau.commons.utils.Shorts.eq;
023import static org.apache.juneau.commons.utils.Shorts.ioex;
024import static org.apache.juneau.commons.utils.SystemUtils.*;
025import static org.apache.juneau.commons.utils.ThrowableUtils.*;
026
027import java.io.*;
028import java.nio.charset.*;
029import java.nio.file.*;
030import java.util.*;
031import java.util.concurrent.atomic.*;
032import java.util.function.*;
033import java.util.zip.*;
034
035import org.apache.juneau.commons.io.*;
036import org.apache.juneau.commons.logging.*;
037
038/**
039 * Various I/O related utility methods.
040 */
041@SuppressWarnings({
042   "java:S5164", // Cleanup method provided: cleanupThreadLocals()
043   "null", // Eclipse null analysis does not recognise isNotNull()/isNotEmpty() guards on the optional buffer caches and the saved IOException.
044   "resource" // Wrapper readers/writers (InputStreamReader, OutputStreamWriter, BufferedReader) are built over caller-owned streams and must not close them.
045})
046public class IoUtils {
047
048   /** UTF-8 charset */
049   public static final Charset UTF8 = StandardCharsets.UTF_8;
050
051   /** Reusable empty input stream. */
052   public static final InputStream EMPTY_INPUT_STREAM = new InputStream() {
053      @Override
054      public int read() {
055         return -1;  // end of stream
056      }
057   };
058
059   private static final int BUFF_SIZE = 1024;
060
061   private static final ThreadLocal<byte[]> BYTE_BUFFER_CACHE = (isTrue(env("juneau.disableIoBufferReuse", false)) ? null : new ThreadLocal<>());
062   private static final ThreadLocal<char[]> CHAR_BUFFER_CACHE = (isTrue(env("juneau.disableIoBufferReuse", false)) ? null : new ThreadLocal<>());
063   static final AtomicInteger BYTE_BUFFER_CACHE_HITS = new AtomicInteger();
064
065   static final AtomicInteger BYTE_BUFFER_CACHE_MISSES = new AtomicInteger();
066   static final AtomicInteger CHAR_BUFFER_CACHE_HITS = new AtomicInteger();
067   static final AtomicInteger CHAR_BUFFER_CACHE_MISSES = new AtomicInteger();
068   static {
069      shutdownMessage(() -> "Byte buffer cache:  hits=" + BYTE_BUFFER_CACHE_HITS.get() + ", misses=" + BYTE_BUFFER_CACHE_MISSES);
070      shutdownMessage(() -> "Char buffer cache:  hits=" + CHAR_BUFFER_CACHE_HITS.get() + ", misses=" + CHAR_BUFFER_CACHE_MISSES);
071   }
072
073   /**
074    * Cleans up thread-local buffer caches for the current thread.
075    *
076    * <p>
077    * This method should be called when a thread is finished using the buffer caches to prevent memory leaks
078    * in thread pool environments where threads are reused.
079    */
080   public static void cleanupThreadLocals() {
081      if (BYTE_BUFFER_CACHE != null) {
082         BYTE_BUFFER_CACHE.remove();
083      }
084      if (CHAR_BUFFER_CACHE != null) {
085         CHAR_BUFFER_CACHE.remove();
086      }
087   }
088
089   /** Reusable empty reader. */
090   public static final Reader EMPTY_READER = new Reader() {
091      @Override
092      public void close() throws IOException { /* no-op */ }
093
094      @Override
095      public int read() {
096         return -1;  // end of stream
097      }
098
099      @Override
100      public int read(char[] cbuf, int off, int len) throws IOException {
101         return -1;  // end of stream
102      }
103   };
104
105   /**
106    * Close all specified input streams, output streams, readers, and writers.
107    *
108    * @param o
109    *    The list of all objects to close.
110    *    <jk>null</jk> entries are ignored.
111    * @throws IOException Thrown by underlying stream.
112    */
113   public static void close(Object...o) throws IOException {
114      IOException ex = null;
115      for (var o2 : o) {
116         try {
117            if (o2 instanceof InputStream o3)
118               o3.close();
119            if (o2 instanceof OutputStream o3)
120               o3.close();
121            if (o2 instanceof Reader o3)
122               o3.close();
123            if (o2 instanceof Writer o3)
124               o3.close();
125         } catch (IOException e) {
126            ex = e;
127         }
128      }
129      if (isNotNull(ex))
130         throw ex;
131   }
132
133   /**
134    * Reads and discards the remainder of a reader or input stream, ignoring any exceptions.
135    *
136    * <p>
137    * Reads until end-of-stream using a 256-unit buffer.  Any {@link IOException} stops the drain silently.
138    * No-op if the object is <jk>null</jk> or is neither a {@link Reader} nor an {@link InputStream}.
139    * The stream is not closed.
140    *
141    * @param in The reader or input stream to drain.
142    */
143   public static void drainQuietly(Closeable in) {
144      try {
145         if (in instanceof Reader r) {
146            var buf = new char[256];
147            while (r.read(buf) != -1) { /* discard */ }
148         } else if (in instanceof InputStream is) {
149            var buf = new byte[256];
150            while (is.read(buf) != -1) { /* discard */ }
151         }
152      } catch (IOException ignored) {
153         // best-effort
154      }
155   }
156
157   /**
158    * Close input stream and ignore any exceptions.
159    *
160    * <p>
161    * No-op if input stream is <jk>null</jk>.
162    *
163    * @param is The input stream to close.
164    */
165   public static void closeQuietly(InputStream is) {
166      if (isNotNull(is))
167         safe(is::close);
168   }
169
170   /**
171    * Quietly close all specified input streams, output streams, readers, and writers.
172    *
173    * @param o The list of all objects to quietly close.
174    */
175   public static void closeQuietly(Object...o) {
176      for (var o2 : o) {
177         if (o2 instanceof InputStream o3)
178            closeQuietly(o3);
179         if (o2 instanceof OutputStream o3)
180            closeQuietly(o3);
181         if (o2 instanceof Reader o3)
182            closeQuietly(o3);
183         if (o2 instanceof Writer o3)
184            closeQuietly(o3);
185      }
186   }
187
188   /**
189    * Close output stream and ignore any exceptions.
190    *
191    * <p>
192    * No-op if output stream is <jk>null</jk>.
193    *
194    * @param os The output stream to close.
195    */
196   public static void closeQuietly(OutputStream os) {
197      if (isNotNull(os))
198         runQuietly(os::close);
199   }
200
201   /**
202    * Close reader and ignore any exceptions.
203    *
204    * <p>
205    * No-op if reader is <jk>null</jk>.
206    *
207    * @param r The reader to close.
208    */
209   public static void closeQuietly(Reader r) {
210      if (isNotNull(r))
211         runQuietly(r::close);
212   }
213
214   /**
215    * Close writer and ignore any exceptions.
216    *
217    * <p>
218    * No-op if writer is <jk>null</jk>.
219    *
220    * @param w The writer to close.
221    */
222   public static void closeQuietly(Writer w) {
223      if (isNotNull(w))
224         runQuietly(w::close);
225   }
226
227   /**
228    * Counts the number of bytes in the input stream and then closes the stream.
229    *
230    * @param is The input stream to read from.  Can be <jk>null</jk> (returns <c>0</c>).
231    * @return The number of bytes read.
232    * @throws IOException Thrown by underlying stream.
233    */
234   public static long count(InputStream is) throws IOException {
235      if (is == null)
236         return 0;
237      var c = 0l;
238      var i = 0l;
239      try {
240         while ((i = is.skip(1024)) != 0)
241            c += i;
242      } finally {
243         is.close();
244      }
245      return c;
246   }
247
248   /**
249    * Counts the number of characters in the reader and then closes the reader.
250    *
251    * @param r The reader to read from.  Can be <jk>null</jk> (returns <c>0</c>).
252    * @return The number of characters read.
253    * @throws IOException Thrown by underlying stream.
254    */
255   public static long count(Reader r) throws IOException {
256      if (r == null)
257         return 0;
258      var c = 0l;
259      var i = 0l;
260      try {
261         while ((i = r.skip(1024)) != 0)
262            c += i;
263      } finally {
264         r.close();
265      }
266      return c;
267   }
268
269   /**
270    * Flushes multiple output streams and writers in a single call.
271    *
272    * @param o
273    *    The objects to flush.
274    *    <jk>null</jk> entries are ignored.
275    * @throws IOException Thrown by underlying stream.
276    */
277   public static void flush(Object...o) throws IOException {
278      IOException ex = null;
279      for (var o2 : o) {
280         try {
281            if (o2 instanceof OutputStream o3)
282               o3.flush();
283            if (o2 instanceof Writer o3)
284               o3.flush();
285         } catch (IOException e) {
286            ex = e;
287         }
288      }
289      if (isNotNull(ex))
290         throw ex;
291   }
292
293   /**
294    * Loads a text file from either the file system or classpath.
295    *
296    * @param name The file name.  Must not be <jk>null</jk>.
297    * @param paths The paths to search.
298    * @return The file contents, or <jk>null</jk> if not found.
299    * @throws IOException Thrown by underlying stream.
300    */
301   public static String loadSystemResourceAsString(String name, String...paths) throws IOException {
302      for (var path : paths) {
303         var p = new File(path);
304         if (p.exists()) {
305            var f = new File(p, name);
306            if (f.exists() && f.canRead())
307               return read(f);
308         }
309      }
310      var cl = Thread.currentThread().getContextClassLoader();
311      if (cl == null)
312         cl = ClassLoader.getSystemClassLoader();
313      for (var path : paths) {
314         var n = eq(path, ".") ? name : path + '/' + name;
315         try (var is = cl.getResourceAsStream(n)) {
316            if (isNotNull(is))
317               return read(is);
318         }
319         try (var is = ClassLoader.getSystemResourceAsStream(n)) {
320            if (isNotNull(is))
321               return read(is);
322         }
323      }
324      return null;
325   }
326
327   /**
328    * Pipes the contents of the specified <c>Reader</c> to the specified <c>Writer</c> a line at a time.
329    *
330    * <p>
331    * Writer is flushed after every line.  Typically useful when writing to consoles.
332    *
333    * @param in
334    *    The reader to pipe from.
335    *    <br>Can be <jk>null</jk> (returns <c>0</c>).
336    *    <br>Reader is automatically closed.
337    * @param out
338    *    The file to write the output to.
339    *    <br>Can be <jk>null</jk> (returns <c>0</c>).
340    *    <br>Writer is flushed but not automatically closed.
341    * @return
342    *    The number of characters piped.
343    * @throws IOException Thrown by underlying stream.
344    */
345   public static long pipeLines(Reader in, Writer out) throws IOException {
346      if (in == null || out == null)
347         return 0;
348      var total = 0l;
349      try (var in2 = in) {
350         try (var s = new Scanner(in2)) {
351            while (s.hasNextLine()) {
352               var l = s.nextLine();
353               if (isNotNull(l)) {
354                  out.write(l);
355                  out.write("\n");
356                  out.flush();
357                  total += l.length() + 1;
358               }
359            }
360         }
361      }
362      return total;
363   }
364
365   /**
366    * Reads the specified byte array containing UTF-8 into a string.
367    *
368    * @param in
369    *    The input.
370    *    <br>Can be <jk>null</jk>.
371    * @return The new string, or <jk>null</jk> if the input was null.
372    */
373   public static String read(byte[] in) {
374      return read(in, UTF8);
375   }
376
377   /**
378    * Reads the specified byte array into a string.
379    *
380    * @param in
381    *    The input.
382    *    <br>Can be <jk>null</jk>.
383    * @param charset The character set to use for decoding.  Must not be <jk>null</jk> when the input is non-<jk>null</jk>.
384    * @return The new string, or <jk>null</jk> if the input was null.
385    */
386   public static String read(byte[] in, Charset charset) {
387      if (in == null)
388         return null;
389      return new String(in, charset);
390   }
391
392   /**
393    * Reads the contents of a file into a string.
394    *
395    * <p>
396    * Assumes default character encoding.
397    *
398    * @param in
399    *  The file to read.
400    *  <br>Can be <jk>null</jk>.
401    * @return
402    *  The contents of the reader as a string, or <jk>null</jk> if file does not exist.
403    * @throws IOException If a problem occurred trying to read from the reader.
404    */
405   public static String read(File in) throws IOException {
406      if (in == null || ! in.exists())
407         return null;
408      try (var r = FileReaderBuilder.create(in).build()) {
409         return read(r, in.length());
410      }
411   }
412
413   /**
414    * Reads the contents of an input stream into a string.
415    *
416    * <p>
417    * Assumes UTF-8 encoding.
418    *
419    * @param in
420    *    The input stream.
421    *    <br>Can be <jk>null</jk>.
422    *    <br>Stream is automatically closed.
423    * @return
424    *    The contents of the reader as a string, or <jk>null</jk> if the input stream was <jk>null</jk>.
425    * @throws IOException If a problem occurred trying to read from the input stream.
426    */
427   public static String read(InputStream in) throws IOException {
428      return read(in, UTF8);
429   }
430
431   /**
432    * Reads the contents of an input stream into a string using the specified charset.
433    *
434    * @param in
435    *    The input stream.
436    *    <br>Can be <jk>null</jk>.
437    *    <br>Stream is automatically closed.
438    * @param cs
439    *    The charset of the contents of the input stream.  Must not be <jk>null</jk> when the input stream is non-<jk>null</jk>.
440    * @return
441    *    The contents of the reader as a string or <jk>null</jk> if input stream was <jk>null</jk>.
442    * @throws IOException If a problem occurred trying to read from the input stream.
443    */
444   public static String read(InputStream in, Charset cs) throws IOException {
445      if (in == null)
446         return null;
447      try (var isr = new InputStreamReader(in, cs)) {
448         return read(isr);
449      }
450   }
451
452   /**
453    * Reads the contents of an input stream into a string using the specified charset.
454    *
455    * @param in
456    *    The input stream.
457    *    <br>Can be <jk>null</jk>.
458    *    <br>Stream is automatically closed.
459    * @param cs
460    *    The charset of the contents of the input stream.  Must not be <jk>null</jk> when the input stream is non-<jk>null</jk>.
461    * @param onException Consumer of any {@link IOException I/O exceptions}.  Must not be <jk>null</jk> (invoked on I/O error).
462    * @return
463    *    The contents of the reader as a string or <jk>null</jk> if input stream was <jk>null</jk>.
464    */
465   public static String read(InputStream in, Charset cs, Consumer<IOException> onException) {
466      if (in == null)
467         return null;
468      try (var isr = new InputStreamReader(in, cs)) {
469         return read(isr);
470      } catch (IOException e) {
471         onException.accept(e);
472         return null;
473      }
474   }
475
476   /**
477    * Reads the contents of an input stream into a string.
478    *
479    * <p>
480    * Assumes UTF-8 encoding.
481    *
482    * @param in
483    *    The input stream.
484    *    <br>Can be <jk>null</jk>.
485    *    <br>Stream is automatically closed.
486    * @param onException Consumer of any {@link IOException I/O exceptions}.  Must not be <jk>null</jk> (invoked on I/O error).
487    * @return
488    *    The contents of the reader as a string, or <jk>null</jk> if the input stream was <jk>null</jk>.
489    */
490   public static String read(InputStream in, Consumer<IOException> onException) {
491      return read(in, UTF8, onException);
492   }
493
494   /**
495    * Reads the contents of an input stream into a string, reading up to the specified maximum number of bytes.
496    *
497    * <p>
498    * Assumes UTF-8 encoding.
499    *
500    * @param in
501    *    The input stream.
502    *    <br>Can be <jk>null</jk>.
503    *    <br>Stream is automatically closed.
504    * @param maxBytes
505    *    The maximum number of bytes to read, or <c>-1</c> to read all bytes.
506    * @return
507    *    The contents of the input stream as a string, or <jk>null</jk> if the input stream was <jk>null</jk>.
508    * @throws IOException If a problem occurred trying to read from the input stream.
509    */
510   public static String read(InputStream in, int maxBytes) throws IOException {
511      return read(in, maxBytes, UTF8);
512   }
513
514   /**
515    * Reads the contents of an input stream into a string using the specified charset, reading up to the specified maximum number of bytes.
516    *
517    * @param in
518    *    The input stream.
519    *    <br>Can be <jk>null</jk>.
520    *    <br>Stream is automatically closed.
521    * @param maxBytes
522    *    The maximum number of bytes to read, or <c>-1</c> to read all bytes.
523    * @param cs
524    *    The charset of the contents of the input stream.  Must not be <jk>null</jk> when the input stream is non-<jk>null</jk>.
525    * @return
526    *    The contents of the input stream as a string, or <jk>null</jk> if the input stream was <jk>null</jk>.
527    * @throws IOException If a problem occurred trying to read from the input stream.
528    */
529   public static String read(InputStream in, int maxBytes, Charset cs) throws IOException {
530      if (in == null)
531         return null;
532      try (var in2 = in) {
533         var bytes = readBytes(in2, maxBytes);
534         return new String(bytes, cs);
535      }
536   }
537
538   /**
539    * Reads the contents of an input stream into a string, reading up to the specified maximum number of bytes.
540    *
541    * <p>
542    * Assumes UTF-8 encoding.
543    *
544    * @param in
545    *    The input stream.
546    *    <br>Can be <jk>null</jk>.
547    *    <br>Stream is automatically closed.
548    * @param maxBytes
549    *    The maximum number of bytes to read, or <c>-1</c> to read all bytes.
550    * @return
551    *    The contents of the input stream as a string, or <jk>null</jk> if the input stream was <jk>null</jk>.
552    * @throws IOException If a problem occurred trying to read from the input stream.
553    */
554   public static String read(InputStream in, long maxBytes) throws IOException {
555      if (maxBytes > Integer.MAX_VALUE)
556         maxBytes = Integer.MAX_VALUE;
557      return read(in, (int)maxBytes, UTF8);
558   }
559
560   /**
561    * Pipes the specified object to the specified output stream.
562    *
563    * @param in
564    *    The input byte array.
565    *    <br>Can be <jk>null</jk> or any of the following types:
566    *    <ul>
567    *       <li>{@link Reader}
568    *       <li>{@link InputStream}
569    *       <li>{@link File}
570    *       <li>byte array.
571    *    </ul>
572    * @return The input converted to a string, or <jk>null</jk> if the input was <jk>null</jk>.
573    * @throws IOException If thrown from output stream.
574    * @throws IllegalArgumentException If the input is non-<jk>null</jk> but not one of the supported types.
575    */
576   public static String read(Object in) throws IOException {
577      if (in == null)
578         return null;
579      if (in instanceof Reader in2)
580         return read(in2);
581      if (in instanceof InputStream in2)
582         return read(in2);
583      if (in instanceof File in2)
584         return read(in2);
585      if (in instanceof byte[] in2)
586         return read(in2);
587      throw iaex("Invalid type passed to read:  %s", ClassUtils.className(in));
588   }
589
590   /**
591    * Reads the contents of a path into a string.
592    *
593    * <p>
594    * Assumes default character encoding.
595    *
596    * @param in
597    *  The path to read.
598    *  <br>Can be <jk>null</jk>.
599    * @return
600    *  The contents of the reader as a string, or <jk>null</jk> if path does not exist.
601    * @throws IOException If a problem occurred trying to read from the reader.
602    * @since 9.1.0
603    */
604   public static String read(Path in) throws IOException {
605      if (in == null || ! Files.exists(in)) {
606         return null;
607      }
608      try (var r = PathReaderBuilder.create(in).build()) {
609         return read(r, Files.size(in));
610      }
611   }
612
613   /**
614    * Reads the contents of a reader into a string.
615    *
616    * @param in
617    *    The input reader.
618    *    <br>Can be <jk>null</jk>.
619    *    <br>Stream is automatically closed.
620    * @return
621    *    The contents of the reader as a string, or <jk>null</jk> if the reader was <jk>null</jk>.
622    * @throws IOException If a problem occurred trying to read from the reader.
623    */
624   public static String read(Reader in) throws IOException {
625      try (var in2 = in) {
626         return read(in, -1);
627      }
628   }
629
630   /**
631    * Reads the contents of a reader into a string.
632    *
633    * @param in
634    *    The input reader.
635    *    <br>Can be <jk>null</jk>.
636    *    <br>Stream is automatically closed.
637    * @param onException Consumer of any {@link IOException I/O exceptions}.  Must not be <jk>null</jk> (invoked on I/O error).
638    * @return
639    *    The contents of the reader as a string, or <jk>null</jk> if the reader was <jk>null</jk>.
640    */
641   public static String read(Reader in, Consumer<IOException> onException) {
642      try (var in2 = in) {
643         return read(in, -1);
644      } catch (IOException e) {
645         onException.accept(e);
646         return null;
647      }
648   }
649
650   /**
651    * Reads the specified input into a {@link String} until the end of the input is reached.
652    *
653    * @param in
654    *    The input reader.
655    *    <br>Can be <jk>null</jk>.
656    *    <br>String is automatically closed.
657    * @param expectedLength
658    *    Specify a positive number if the length of the input is known, or <c>-1</c> if unknown.
659    * @return
660    *    The contents of the reader as a string, or <jk>null</jk> if the reader was <jk>null</jk>.
661    * @throws IOException If a problem occurred trying to read from the reader.
662    */
663   public static String read(Reader in, long expectedLength) throws IOException {
664      if (in == null)
665         return null;
666      try (var in2 = in) {
667         var sb = new StringBuilder(buffSize(expectedLength)); // Assume they're ASCII characters.
668         var buf = charBuffer((int)expectedLength);
669         var i = 0;
670         while ((i = in2.read(buf)) != -1)
671            sb.append(buf, 0, i);
672         return sb.toString();
673      }
674   }
675
676   /**
677    * Read the specified file into a byte array.
678    *
679    * @param in
680    *    The file to read into a byte array.
681    *    <br>Can be <jk>null</jk> (returns an empty array).
682    * @return The contents of the file as a byte array.  Never <jk>null</jk>.
683    * @throws IOException Thrown by underlying stream.
684    */
685   public static byte[] readBytes(File in) throws IOException {
686      return readBytes(in, -1);
687   }
688
689   /**
690    * Read the specified file into a byte array.
691    *
692    * @param in
693    *    The file to read into a byte array.
694    *    <br>Can be <jk>null</jk> (returns an empty array).
695    * @param maxBytes
696    *    The maximum number of bytes to read, or <jk>-1</jk> to read all bytes.
697    * @return The contents of the file as a byte array.  Never <jk>null</jk>.
698    * @throws IOException Thrown by underlying stream.
699    */
700   public static byte[] readBytes(File in, int maxBytes) throws IOException {
701      if (in == null || ! (in.exists() && in.canRead()))
702         return new byte[0];
703      try (var is = new FileInputStream(in)) {
704         return readBytes(is, maxBytes);
705      }
706   }
707
708   /**
709    * Reads the specified input stream into the specified byte array.
710    *
711    * @param in
712    *    The input stream to read.
713    *    <br>Can be <jk>null</jk> (returns an empty array).
714    *    <br>Stream is automatically closed.
715    * @return A byte array containing the contents.  Never <jk>null</jk>.
716    * @throws IOException Thrown by underlying stream.
717    */
718   public static byte[] readBytes(InputStream in) throws IOException {
719      try (var in2 = in) {
720         return readBytes(in2, -1);
721      }
722   }
723
724   /**
725    * Reads the specified input stream into the specified byte array.
726    *
727    * @param in
728    *    The input stream to read.
729    *    <br>Can be <jk>null</jk> (returns an empty array).
730    *    <br>Stream is not automatically closed.
731    * @param maxBytes
732    *    The maximum number of bytes or <c>-1</c> to read the entire stream.
733    * @return A byte array containing the contents.  Never <jk>null</jk>.
734    * @throws IOException Thrown by underlying stream.
735    */
736   public static byte[] readBytes(InputStream in, int maxBytes) throws IOException {
737      if (in == null)
738         return new byte[0];
739      var buff = new ByteArrayOutputStream(buffSize(maxBytes));
740      var nRead = 0;
741      var b = byteBuffer(maxBytes);
742      while ((nRead = in.read(b, 0, b.length)) != -1)
743         buff.write(b, 0, nRead);
744      buff.flush();
745      return buff.toByteArray();
746   }
747
748   /**
749    * Reads the specified input stream into the specified byte array.
750    *
751    * @param in
752    *    The input stream to read.
753    *    <br>Can be <jk>null</jk> (returns an empty array).
754    *    <br>Stream is automatically closed.
755    * @return A byte array containing the contents.  Never <jk>null</jk>.
756    * @throws IOException Thrown by underlying stream.
757    */
758   public static byte[] readBytes(Reader in) throws IOException {
759      if (in == null)
760         return new byte[0];
761      try (var in2 = in) {
762         return read(in2, -1).getBytes();
763      }
764   }
765
766   /**
767    * Inflates (decompresses) the specified compressed bytes with a hard ceiling on the total output size and an
768    * effective output-to-input ratio guard.
769    *
770    * <p>
771    * Building on the bounded-copy family in this class (such as {@link #readBytes(InputStream,int)} and
772    * {@link #pipe(InputStream,OutputStream,long)}), this method decompresses untrusted input without letting a
773    * small compressed payload expand into an unbounded allocation.  Two independent limits are enforced
774    * <i>during</i> inflation, so a violating stream is aborted before its full output is ever materialized:
775    * <ul>
776    *    <li><b>Absolute cap.</b> If the running output size would exceed <jv>maxOutputBytes</jv>, an
777    *       {@link IOException} is thrown.
778    *    <li><b>Ratio guard.</b> If the running output size would exceed <jv>maxRatio</jv> times the compressed
779    *       input length, an {@link IOException} is thrown &mdash; this catches a highly-compressible payload even
780    *       when the absolute cap is generous.
781    * </ul>
782    *
783    * @param compressed
784    *    The compressed input bytes.
785    *    <br>Can be <jk>null</jk> (returns an empty array).
786    * @param nowrap
787    *    Pass <jk>true</jk> for a raw (headerless) DEFLATE stream, or <jk>false</jk> for a zlib-wrapped stream.
788    *    <br>Must match the format used to compress the input.
789    * @param maxOutputBytes
790    *    The maximum number of decompressed bytes to allow.
791    *    <br>Values <c>&lt;= 0</c> disable the absolute cap (mirroring the <c>maxLength</c> convention used
792    *    elsewhere); the ratio guard, if enabled, still applies.
793    * @param maxRatio
794    *    The maximum allowed ratio of output size to compressed input size.
795    *    <br>Values <c>&lt;= 0</c> disable the ratio guard.
796    * @return The decompressed bytes.  Never <jk>null</jk>.
797    * @throws IOException
798    *    If the input is not valid for the selected format, or if either the absolute cap or the ratio guard is
799    *    exceeded.
800    * @since 10.0.0
801    */
802   public static byte[] inflate(byte[] compressed, boolean nowrap, long maxOutputBytes, int maxRatio) throws IOException {
803      if (compressed == null || compressed.length == 0)
804         return new byte[0];
805      var cap = maxOutputBytes <= 0 ? Long.MAX_VALUE : maxOutputBytes;
806      var ratioLimit = maxRatio <= 0 ? Long.MAX_VALUE : (long)compressed.length * maxRatio;
807      var inflater = new Inflater(nowrap);
808      // Inflater isn't AutoCloseable at this module's source/target level, so route end() through
809      // try-with-resources via a Closeable method reference (inflate() already declares throws IOException).
810      try (Closeable end = inflater::end) {
811         inflater.setInput(compressed);
812         return inflateAll(inflater, cap, ratioLimit, maxRatio);
813      }
814   }
815
816   private static byte[] inflateAll(Inflater inflater, long cap, long ratioLimit, int maxRatio) throws IOException {
817      var out = new ByteArrayOutputStream(BUFF_SIZE);
818      var buf = new byte[BUFF_SIZE];
819      var total = 0L;
820      while (true) {
821         var n = inflateChunk(inflater, buf);
822         if (n > 0) {
823            total += n;
824            checkInflateLimits(total, cap, ratioLimit, maxRatio);
825            out.write(buf, 0, n);
826         } else if (inflater.finished() || inflater.needsDictionary() || inflater.needsInput()) {
827            break;
828         }
829      }
830      return out.toByteArray();
831   }
832
833   private static int inflateChunk(Inflater inflater, byte[] buf) throws IOException {
834      try {
835         return inflater.inflate(buf);
836      } catch (DataFormatException e) {
837         throw ioex(e, "Invalid compressed data");
838      }
839   }
840
841   private static void checkInflateLimits(long total, long cap, long ratioLimit, int maxRatio) throws IOException {
842      if (total > cap)
843         throw ioex("Inflated output exceeds maximum allowed %s bytes", cap);
844      if (total > ratioLimit)
845         throw ioex("Inflated output-to-input ratio exceeds maximum allowed %s", maxRatio);
846   }
847
848   /**
849    * Wraps the specified reader in a buffered reader.
850    *
851    * @param r The reader being wrapped.  Can be <jk>null</jk> (returns <jk>null</jk>).
852    * @return
853    *    The reader wrapped in a {@link BufferedReader}, or the original {@link Reader} if it's already a buffered
854    *    reader, or <jk>null</jk> if the argument was <jk>null</jk>.
855    */
856   public static Reader toBufferedReader(Reader r) {
857      if (r == null || r instanceof BufferedReader || r instanceof StringReader)
858         return r;
859      return new BufferedReader(r);
860   }
861
862   private static int buffSize(long max) {
863      return (max > 0 && max < BUFF_SIZE) ? (int)max : BUFF_SIZE;
864   }
865
866   private static byte[] byteBuffer(int maxBytes) {
867      if (isNotNull(BYTE_BUFFER_CACHE)) {
868         var x = BYTE_BUFFER_CACHE.get();
869         if (x == null) {
870            x = new byte[BUFF_SIZE];
871            BYTE_BUFFER_CACHE.set(x);
872            BYTE_BUFFER_CACHE_MISSES.incrementAndGet();
873         } else {
874            BYTE_BUFFER_CACHE_HITS.incrementAndGet();
875         }
876         return x;
877      }
878      return new byte[buffSize(maxBytes)];
879   }
880
881   private static char[] charBuffer(int maxChars) {
882      if (isNotNull(CHAR_BUFFER_CACHE)) {
883         var x = CHAR_BUFFER_CACHE.get();
884         if (x == null) {
885            x = new char[BUFF_SIZE];
886            CHAR_BUFFER_CACHE.set(x);
887            CHAR_BUFFER_CACHE_MISSES.incrementAndGet();
888         } else {
889            CHAR_BUFFER_CACHE_HITS.incrementAndGet();
890         }
891         return x;
892      }
893      return new char[buffSize(maxChars)];
894   }
895
896   //-----------------------------------------------------------------------------------------------------------------
897   // Canonical-named methods (pipe overloads delegate to these)
898   //-----------------------------------------------------------------------------------------------------------------
899
900   /** Pipes bytes to output stream.  <br>Returns <c>0</c> if either <jv>in</jv> or <jv>out</jv> is <jk>null</jk>. */
901   public static long pipe(byte[] in, OutputStream out, int maxBytes) throws IOException {
902      if (in == null || out == null)
903         return 0;
904      var length = (maxBytes < 0 || maxBytes > in.length) ? in.length : maxBytes;
905      out.write(in, 0, length);
906      return length;
907   }
908
909   /** Pipes input stream to output stream.  <br>Returns <c>0</c> if either <jv>in</jv> or <jv>out</jv> is <jk>null</jk>. */
910   public static long pipe(InputStream in, OutputStream out) throws IOException {
911      try (var in2 = in) {
912         return pipe(in, out, -1L);
913      }
914   }
915
916   /** Pipes input stream to output stream with exception handler.  <br>Returns <c>0</c> if either <jv>in</jv> or <jv>out</jv> is <jk>null</jk>.  <br><jv>onException</jv> must not be <jk>null</jk> (invoked on I/O error). */
917   public static long pipe(InputStream in, OutputStream out, Consumer<IOException> onException) {
918      try {
919         try (var in2 = in) {
920            return pipe(in, out, -1L);
921         }
922      } catch (IOException e) {
923         onException.accept(e);
924         return -1;
925      }
926   }
927
928   /** Pipes input stream to output stream with max bytes.  <br>Returns <c>0</c> if either <jv>in</jv> or <jv>out</jv> is <jk>null</jk>. */
929   public static long pipe(InputStream in, OutputStream out, long maxBytes) throws IOException {
930      if (in == null || out == null)
931         return 0;
932      var buffer = byteBuffer((int)maxBytes);
933      int readLen;
934      var total = 0l;
935      if (maxBytes < 0) {
936         while ((readLen = in.read(buffer)) != -1) {
937            out.write(buffer, 0, readLen);
938            total += readLen;
939         }
940      } else {
941         var remaining = maxBytes;
942         while (remaining > 0) {
943            readLen = in.read(buffer, 0, buffSize(remaining));
944            if (readLen == -1)
945               break;
946            out.write(buffer, 0, readLen);
947            total += readLen;
948            remaining -= readLen;
949         }
950      }
951      out.flush();
952      return total;
953   }
954
955   /** Pipes input stream to writer.  <br>Returns <c>0</c> if either <jv>in</jv> or <jv>out</jv> is <jk>null</jk>. */
956   public static long pipe(InputStream in, Writer out) throws IOException {
957      if (in == null || out == null)
958         return 0;
959      return pipe(new InputStreamReader(in, UTF8), out);
960   }
961
962   /** Pipes input stream to writer with exception handler.  <br>Returns <c>0</c> if either <jv>in</jv> or <jv>out</jv> is <jk>null</jk>.  <br><jv>onException</jv> must not be <jk>null</jk> (invoked on I/O error). */
963   public static long pipe(InputStream in, Writer out, Consumer<IOException> onException) {
964      try {
965         if (in == null || out == null)
966            return 0;
967         return pipe(new InputStreamReader(in, UTF8), out);
968      } catch (IOException e) {
969         onException.accept(e);
970         return -2;
971      }
972   }
973
974   /** Pipes reader to file.  <br>Returns <c>0</c> if either <jv>in</jv> or <jv>out</jv> is <jk>null</jk>. */
975   public static long pipe(Reader in, File out) throws IOException {
976      if (out == null || in == null)
977         return 0;
978      try (var w = FileWriterBuilder.create(out).buffered().build()) {
979         return pipe(in, w);
980      }
981   }
982
983   /** Pipes reader to output stream.  <br>Returns <c>0</c> if either <jv>in</jv> or <jv>out</jv> is <jk>null</jk>. */
984   public static long pipe(Reader in, OutputStream out) throws IOException {
985      if (in == null || out == null)
986         return 0;
987      var total = 0l;
988      try (var in2 = in) {
989         var osw = new OutputStreamWriter(out, UTF8);
990         var i = 0;
991         var b = charBuffer(-1);
992         while ((i = in.read(b)) > 0) {
993            total += i;
994            osw.write(b, 0, i);
995         }
996         osw.flush();
997      }
998      return total;
999   }
1000
1001   /** Pipes reader to output stream with exception handler.  <br>Returns <c>0</c> if either <jv>in</jv> or <jv>out</jv> is <jk>null</jk>.  <br><jv>onException</jv> must not be <jk>null</jk> (invoked on I/O error). */
1002   public static long pipe(Reader in, OutputStream out, Consumer<IOException> onException) {
1003      try {
1004         return pipe(in, out);
1005      } catch (IOException e) {
1006         onException.accept(e);
1007         return -1;
1008      }
1009   }
1010
1011   /** Pipes reader to writer.  <br>Returns <c>0</c> if either <jv>in</jv> or <jv>out</jv> is <jk>null</jk>. */
1012   public static long pipe(Reader in, Writer out) throws IOException {
1013      if (out == null || in == null)
1014         return 0;
1015      var total = 0l;
1016      try (var in2 = in) {
1017         var buffer = charBuffer(-1);
1018         int readLen;
1019         while ((readLen = in.read(buffer)) != -1) {
1020            out.write(buffer, 0, readLen);
1021            total += readLen;
1022         }
1023      }
1024      out.flush();
1025      return total;
1026   }
1027
1028   /** Pipes reader to writer with exception handler.  <br>Returns <c>0</c> if either <jv>in</jv> or <jv>out</jv> is <jk>null</jk>.  <br><jv>onException</jv> must not be <jk>null</jk> (invoked on I/O error). */
1029   public static long pipe(Reader in, Writer out, Consumer<IOException> onException) {
1030      try {
1031         return pipe(in, out);
1032      } catch (IOException e) {
1033         onException.accept(e);
1034         return -1;
1035      }
1036   }
1037
1038   /**
1039    * Prints all the specified lines to the log with line numbers.
1040    *
1041    * @param lines The lines to print.  Must not be <jk>null</jk> (individual <jk>null</jk> entries render as <js>"null"</js>).
1042    */
1043   public static final void printLines(String[] lines) {
1044      RichLogger log = RichLogger.getLogger(IoUtils.class);
1045      for (var i = 0; i < lines.length; i++)
1046         log.info(f("%4s:%s", i + 1, lines[i]));
1047   }
1048
1049   /**
1050    * Constructor.
1051    */
1052   protected IoUtils() {}
1053}