From e4513af88e11dd106fbabe58c54bd3a25f06fa57 Mon Sep 17 00:00:00 2001 From: ag2s20150909 Date: Sun, 18 Apr 2021 20:18:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/me/ag2s/epublib/epub/EpubWriter.java | 2 +- .../java/me/ag2s/epublib/util/IOUtil.java | 797 +++++++++++++++++- .../util/commons/io/ByteOrderMark.java | 1 + .../epublib/util/commons/io/IOConsumer.java | 59 ++ .../util/commons/io/ProxyInputStream.java | 86 +- 5 files changed, 877 insertions(+), 68 deletions(-) create mode 100644 epublib/src/main/java/me/ag2s/epublib/util/commons/io/IOConsumer.java diff --git a/epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java b/epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java index 11d716b68..422ad75b3 100644 --- a/epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java +++ b/epublib/src/main/java/me/ag2s/epublib/epub/EpubWriter.java @@ -102,7 +102,7 @@ public class EpubWriter { try { resultStream.putNextEntry(new ZipEntry("OEBPS/" + resource.getHref())); InputStream inputStream = resource.getInputStream(); - IOUtil.copy(inputStream, resultStream); + IOUtil.copy(inputStream, resultStream,IOUtil.DEFAULT_BUFFER_SIZE); inputStream.close(); } catch (Exception e) { Log.e(TAG,e.getMessage(), e); diff --git a/epublib/src/main/java/me/ag2s/epublib/util/IOUtil.java b/epublib/src/main/java/me/ag2s/epublib/util/IOUtil.java index e75d91f70..1c605846c 100644 --- a/epublib/src/main/java/me/ag2s/epublib/util/IOUtil.java +++ b/epublib/src/main/java/me/ag2s/epublib/util/IOUtil.java @@ -1,12 +1,24 @@ package me.ag2s.epublib.util; import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; +import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringWriter; import java.io.Writer; +import java.net.HttpURLConnection; +import java.net.URLConnection; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.charset.Charset; + +import me.ag2s.epublib.util.commons.io.IOConsumer; /** * Most of the functions herein are re-implementations of the ones in @@ -23,8 +35,22 @@ public class IOUtil { */ public static final int EOF = -1; - public static final int IO_COPY_BUFFER_SIZE = 1024 * 8; - public static final int DEFAULT_BUFFER_SIZE = 8192; + + public static final int DEFAULT_BUFFER_SIZE = 1024*8; + private static final byte[] SKIP_BYTE_BUFFER = new byte[DEFAULT_BUFFER_SIZE]; + + // Allocated in the relevant skip method if necessary. + /* + * These buffers are static and are shared between threads. + * This is possible because the buffers are write-only - the contents are never read. + * + * N.B. there is no need to synchronize when creating these because: + * - we don't care if the buffer is created multiple times (the data is ignored) + * - we always use the same size buffer, so if it it is recreated it will still be OK + * (if the buffer size were variable, we would need to synch. to ensure some other thread + * did not create a smaller one) + */ + private static char[] SKIP_CHAR_BUFFER; /** * Gets the contents of the Reader as a byte[], with the given character encoding. @@ -51,7 +77,7 @@ public class IOUtil { */ public static byte[] toByteArray(InputStream in) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); - copy(in, result); + copy(in, result,DEFAULT_BUFFER_SIZE); result.flush(); return result.toByteArray(); } @@ -79,7 +105,7 @@ public class IOUtil { result = new ByteArrayOutputStream(); } - copy(in, result); + copy(in, result,DEFAULT_BUFFER_SIZE); result.flush(); return result.toByteArray(); } catch (OutOfMemoryError error) { @@ -112,45 +138,695 @@ public class IOUtil { } } +// /** - * Copies the contents of the InputStream to the OutputStream. + * Copies bytes from an InputStream to an OutputStream using an internal buffer of the + * given size. + *

+ * This method buffers the input internally, so there is no need to use a BufferedInputStream. + *

* - * @param in f - * @param out f - * @return the nr of bytes read, or -1 if the amount > Integer.MAX_VALUE - * @throws IOException f + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param bufferSize the bufferSize used to copy from the input to the output + * @return the number of bytes copied. or {@code 0} if {@code input is null}. + * @throws NullPointerException if the output is null + * @throws IOException if an I/O error occurs + * @since 2.5 + */ + public static long copy(final InputStream input, final OutputStream output, final int bufferSize) + throws IOException { + return copyLarge(input, output, new byte[bufferSize]); + } + + /** + * Copies bytes from an InputStream to chars on a + * Writer using the default character encoding of the platform. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + * @deprecated 2.5 use {@link #copy(InputStream, Writer, Charset)} instead + */ + @Deprecated + public static void copy(final InputStream input, final Writer output) + throws IOException { + copy(input, output, Charset.defaultCharset()); + } + + /** + * Copies bytes from an InputStream to chars on a + * Writer using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @param inputCharset the charset to use for the input stream, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static void copy(final InputStream input, final Writer output, final Charset inputCharset) + throws IOException { + final InputStreamReader in = new InputStreamReader(input, inputCharset.name()); + copy(in, output); + } + + /** + * Copies bytes from an InputStream to chars on a + * Writer using the specified character encoding. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ * Character encoding names can be found at + * IANA. + *

+ * This method uses {@link InputStreamReader}. + * + * @param input the InputStream to read from + * @param output the Writer to write to + * @param inputCharsetName the name of the requested charset for the InputStream, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io + * .UnsupportedEncodingException} in version 2.2 if the + * encoding is not supported. + * @since 1.1 */ - public static int copy(InputStream in, OutputStream out) + public static void copy(final InputStream input, final Writer output, final String inputCharsetName) throws IOException { - byte[] buffer = new byte[IO_COPY_BUFFER_SIZE]; - int readSize; - int result = 0; - while ((readSize = in.read(buffer)) >= 0) { - out.write(buffer, 0, readSize); - result = calcNewNrReadSize(readSize, result); + copy(input, output,Charset.forName(inputCharsetName)); + } + + /** + * Copies chars from a Reader to a Appendable. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Large streams (over 2GB) will return a chars copied value of + * -1 after the copy has completed since the correct + * number of chars cannot be returned as an int. For large streams + * use the copyLarge(Reader, Writer) method. + * + * @param input the Reader to read from + * @param output the Appendable to write to + * @return the number of characters copied, or -1 if > Integer.MAX_VALUE + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.7 + */ + public static long copy(final Reader input, final Appendable output) throws IOException { + return copy(input, output, CharBuffer.allocate(DEFAULT_BUFFER_SIZE)); + } + + /** + * Copies chars from a Reader to an Appendable. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedReader. + *

+ * + * @param input the Reader to read from + * @param output the Appendable to write to + * @param buffer the buffer to be used for the copy + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.7 + */ + public static long copy(final Reader input, final Appendable output, final CharBuffer buffer) throws IOException { + long count = 0; + int n; + while (EOF != (n = input.read(buffer))) { + buffer.flip(); + output.append(buffer, 0, n); + count += n; } + return count; + } + + /** + * Copies chars from a Reader to bytes on an + * OutputStream using the default character encoding of the + * platform, and calling flush. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

+ * This method uses {@link OutputStreamWriter}. + * + * @param input the Reader to read from + * @param output the OutputStream to write to + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + * @deprecated 2.5 use {@link #copy(Reader, OutputStream, Charset)} instead + */ + @Deprecated + public static void copy(final Reader input, final OutputStream output) + throws IOException { + copy(input, output, Charset.defaultCharset()); + } + + /** + * Copies chars from a Reader to bytes on an + * OutputStream using the specified character encoding, and + * calling flush. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ *

+ * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

+ *

+ * This method uses {@link OutputStreamWriter}. + *

+ * + * @param input the Reader to read from + * @param output the OutputStream to write to + * @param outputCharset the charset to use for the OutputStream, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.3 + */ + public static void copy(final Reader input, final OutputStream output, final Charset outputCharset) + throws IOException { + final OutputStreamWriter out = new OutputStreamWriter(output, outputCharset.name()); + copy(input, out); + // XXX Unless anyone is planning on rewriting OutputStreamWriter, + // we have to flush here. out.flush(); - return result; } /** - * Copies the contents of the Reader to the Writer. + * Copies chars from a Reader to bytes on an + * OutputStream using the specified character encoding, and + * calling flush. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Character encoding names can be found at + * IANA. + *

+ * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + *

+ * This method uses {@link OutputStreamWriter}. * - * @param in f - * @param out f - * @return the nr of characters read, or -1 if the amount > Integer.MAX_VALUE - * @throws IOException f + * @param input the Reader to read from + * @param output the OutputStream to write to + * @param outputCharsetName the name of the requested charset for the OutputStream, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link java.io + * .UnsupportedEncodingException} in version 2.2 if the + * encoding is not supported. + * @since 1.1 */ - public static int copy(Reader in, Writer out) throws IOException { - char[] buffer = new char[IO_COPY_BUFFER_SIZE]; - int readSize; - int result = 0; - while ((readSize = in.read(buffer)) >= 0) { - out.write(buffer, 0, readSize); - result = calcNewNrReadSize(readSize, result); + public static void copy(final Reader input, final OutputStream output, final String outputCharsetName) + throws IOException { + copy(input, output, Charset.forName(outputCharsetName)); + } + + /** + * Copies chars from a Reader to a Writer. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * Large streams (over 2GB) will return a chars copied value of + * -1 after the copy has completed since the correct + * number of chars cannot be returned as an int. For large streams + * use the copyLarge(Reader, Writer) method. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @return the number of characters copied, or -1 if > Integer.MAX_VALUE + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.1 + */ + public static int copy(final Reader input, final Writer output) throws IOException { + final long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copies bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + *

+ * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @return the number of bytes copied. or {@code 0} if {@code input is null}. + * @throws NullPointerException if the output is null + * @throws IOException if an I/O error occurs + * @since 1.3 + */ + public static long copyLarge(final InputStream input, final OutputStream output) + throws IOException { + return copy(input, output, DEFAULT_BUFFER_SIZE); + } + + /** + * Copies bytes from a large (over 2GB) InputStream to an + * OutputStream. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedInputStream. + *

+ * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param buffer the buffer to use for the copy + * @return the number of bytes copied. or {@code 0} if {@code input is null}. + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final InputStream input, final OutputStream output, final byte[] buffer) + throws IOException { + long count = 0; + if (input != null) { + int n; + while (EOF != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + } + return count; + } + + /** + * Copies some or all bytes from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input bytes. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedInputStream. + *

+ *

+ * Note that the implementation uses {@link #skip(InputStream, long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of characters are skipped. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param inputOffset : number of bytes to skip from input before copying + * -ve values are ignored + * @param length : number of bytes to copy. -ve means all + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset, + final long length) throws IOException { + return copyLarge(input, output, inputOffset, length, new byte[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copies some or all bytes from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input bytes. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedInputStream. + *

+ *

+ * Note that the implementation uses {@link #skip(InputStream, long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of characters are skipped. + *

+ * + * @param input the InputStream to read from + * @param output the OutputStream to write to + * @param inputOffset : number of bytes to skip from input before copying + * -ve values are ignored + * @param length : number of bytes to copy. -ve means all + * @param buffer the buffer to use for the copy + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final InputStream input, final OutputStream output, + final long inputOffset, final long length, final byte[] buffer) throws IOException { + if (inputOffset > 0) { + skipFully(input, inputOffset); + } + if (length == 0) { + return 0; + } + final int bufferLength = buffer.length; + int bytesToRead = bufferLength; + if (length > 0 && length < bufferLength) { + bytesToRead = (int) length; + } + int read; + long totalRead = 0; + while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) { + output.write(buffer, 0, read); + totalRead += read; + if (length > 0) { // only adjust length if not reading to the end + // Note the cast must work because buffer.length is an integer + bytesToRead = (int) Math.min(length - totalRead, bufferLength); + } + } + return totalRead; + } + + /** + * Copies chars from a large (over 2GB) Reader to a Writer. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 1.3 + */ + public static long copyLarge(final Reader input, final Writer output) throws IOException { + return copyLarge(input, output, new char[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copies chars from a large (over 2GB) Reader to a Writer. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedReader. + *

+ * + * @param input the Reader to read from + * @param output the Writer to write to + * @param buffer the buffer to be used for the copy + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final Reader input, final Writer output, final char[] buffer) throws IOException { + long count = 0; + int n; + while (EOF != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Copies some or all chars from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input chars. + *

+ * This method buffers the input internally, so there is no need to use a + * BufferedReader. + *

+ * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. + * + * @param input the Reader to read from + * @param output the Writer to write to + * @param inputOffset : number of chars to skip from input before copying + * -ve values are ignored + * @param length : number of chars to copy. -ve means all + * @return the number of chars copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length) + throws IOException { + return copyLarge(input, output, inputOffset, length, new char[DEFAULT_BUFFER_SIZE]); + } + + /** + * Copies some or all chars from a large (over 2GB) InputStream to an + * OutputStream, optionally skipping input chars. + *

+ * This method uses the provided buffer, so there is no need to use a + * BufferedReader. + *

+ * + * @param input the Reader to read from + * @param output the Writer to write to + * @param inputOffset : number of chars to skip from input before copying + * -ve values are ignored + * @param length : number of chars to copy. -ve means all + * @param buffer the buffer to be used for the copy + * @return the number of chars copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since 2.2 + */ + public static long copyLarge(final Reader input, final Writer output, final long inputOffset, final long length, + final char[] buffer) + throws IOException { + if (inputOffset > 0) { + skipFully(input, inputOffset); + } + if (length == 0) { + return 0; + } + int bytesToRead = buffer.length; + if (length > 0 && length < buffer.length) { + bytesToRead = (int) length; + } + int read; + long totalRead = 0; + while (bytesToRead > 0 && EOF != (read = input.read(buffer, 0, bytesToRead))) { + output.write(buffer, 0, read); + totalRead += read; + if (length > 0) { // only adjust length if not reading to the end + // Note the cast must work because buffer.length is an integer + bytesToRead = (int) Math.min(length - totalRead, buffer.length); + } + } + return totalRead; + } + + /** + * Skips bytes from an input byte stream. + * This implementation guarantees that it will read as many bytes + * as possible before giving up; this may not always be the case for + * skip() implementations in subclasses of {@link InputStream}. + *

+ * Note that the implementation uses {@link InputStream#read(byte[], int, int)} rather + * than delegating to {@link InputStream#skip(long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of bytes are skipped. + *

+ * + * @param input byte stream to skip + * @param toSkip number of bytes to skip. + * @return number of bytes actually skipped. + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @see InputStream#skip(long) + * @see IO-203 - Add skipFully() method for InputStreams + * @since 2.0 + */ + public static long skip(final InputStream input, final long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); + } + /* + * N.B. no need to synchronize access to SKIP_BYTE_BUFFER: - we don't care if the buffer is created multiple + * times (the data is ignored) - we always use the same size buffer, so if it it is recreated it will still be + * OK (if the buffer size were variable, we would need to synch. to ensure some other thread did not create a + * smaller one) + */ + long remain = toSkip; + while (remain > 0) { + // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip() + final long n = input.read(SKIP_BYTE_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length)); + if (n < 0) { // EOF + break; + } + remain -= n; + } + return toSkip - remain; + } + + /** + * Skips bytes from a ReadableByteChannel. + * This implementation guarantees that it will read as many bytes + * as possible before giving up. + * + * @param input ReadableByteChannel to skip + * @param toSkip number of bytes to skip. + * @return number of bytes actually skipped. + * @throws IOException if there is a problem reading the ReadableByteChannel + * @throws IllegalArgumentException if toSkip is negative + * @since 2.5 + */ + public static long skip(final ReadableByteChannel input, final long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); + } + final ByteBuffer skipByteBuffer = ByteBuffer.allocate((int) Math.min(toSkip, SKIP_BYTE_BUFFER.length)); + long remain = toSkip; + while (remain > 0) { + skipByteBuffer.position(0); + skipByteBuffer.limit((int) Math.min(remain, SKIP_BYTE_BUFFER.length)); + final int n = input.read(skipByteBuffer); + if (n == EOF) { + break; + } + remain -= n; + } + return toSkip - remain; + } + + /** + * Skips characters from an input character stream. + * This implementation guarantees that it will read as many characters + * as possible before giving up; this may not always be the case for + * skip() implementations in subclasses of {@link Reader}. + *

+ * Note that the implementation uses {@link Reader#read(char[], int, int)} rather + * than delegating to {@link Reader#skip(long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of characters are skipped. + *

+ * + * @param input character stream to skip + * @param toSkip number of characters to skip. + * @return number of characters actually skipped. + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @see Reader#skip(long) + * @see IO-203 - Add skipFully() method for InputStreams + * @since 2.0 + */ + public static long skip(final Reader input, final long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); + } + /* + * N.B. no need to synchronize this because: - we don't care if the buffer is created multiple times (the data + * is ignored) - we always use the same size buffer, so if it it is recreated it will still be OK (if the buffer + * size were variable, we would need to synch. to ensure some other thread did not create a smaller one) + */ + if (SKIP_CHAR_BUFFER == null) { + SKIP_CHAR_BUFFER = new char[SKIP_BYTE_BUFFER.length]; + } + long remain = toSkip; + while (remain > 0) { + // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip() + final long n = input.read(SKIP_CHAR_BUFFER, 0, (int) Math.min(remain, SKIP_BYTE_BUFFER.length)); + if (n < 0) { // EOF + break; + } + remain -= n; + } + return toSkip - remain; + } + + /** + * Skips the requested number of bytes or fail if there are not enough left. + *

+ * This allows for the possibility that {@link InputStream#skip(long)} may + * not skip as many bytes as requested (most likely because of reaching EOF). + *

+ * Note that the implementation uses {@link #skip(InputStream, long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of characters are skipped. + *

+ * + * @param input stream to skip + * @param toSkip the number of bytes to skip + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @throws EOFException if the number of bytes skipped was incorrect + * @see InputStream#skip(long) + * @since 2.0 + */ + public static void skipFully(final InputStream input, final long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip); + } + final long skipped = skip(input, toSkip); + if (skipped != toSkip) { + throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped); + } + } + + /** + * Skips the requested number of bytes or fail if there are not enough left. + * + * @param input ReadableByteChannel to skip + * @param toSkip the number of bytes to skip + * @throws IOException if there is a problem reading the ReadableByteChannel + * @throws IllegalArgumentException if toSkip is negative + * @throws EOFException if the number of bytes skipped was incorrect + * @since 2.5 + */ + public static void skipFully(final ReadableByteChannel input, final long toSkip) throws IOException { + if (toSkip < 0) { + throw new IllegalArgumentException("Bytes to skip must not be negative: " + toSkip); + } + final long skipped = skip(input, toSkip); + if (skipped != toSkip) { + throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped); + } + } + + /** + * Skips the requested number of characters or fail if there are not enough left. + *

+ * This allows for the possibility that {@link Reader#skip(long)} may + * not skip as many characters as requested (most likely because of reaching EOF). + *

+ * Note that the implementation uses {@link #skip(Reader, long)}. + * This means that the method may be considerably less efficient than using the actual skip implementation, + * this is done to guarantee that the correct number of characters are skipped. + *

+ * + * @param input stream to skip + * @param toSkip the number of characters to skip + * @throws IOException if there is a problem reading the file + * @throws IllegalArgumentException if toSkip is negative + * @throws EOFException if the number of characters skipped was incorrect + * @see Reader#skip(long) + * @since 2.0 + */ + public static void skipFully(final Reader input, final long toSkip) throws IOException { + final long skipped = skip(input, toSkip); + if (skipped != toSkip) { + throw new EOFException("Chars to skip: " + toSkip + " actual: " + skipped); } - out.flush(); - return result; } /** * Returns the length of the given array in a null-safe manner. @@ -195,6 +871,65 @@ public class IOUtil { public static int length(final Object[] array) { return array == null ? 0 : array.length; } + /** + * Closes the given {@link Closeable} as a null-safe operation. + * + * @param closeable The resource to close, may be null. + * @throws IOException if an I/O error occurs. + * @since 2.7 + */ + public static void close(final Closeable closeable) throws IOException { + if (closeable != null) { + closeable.close(); + } + } + + /** + * Closes the given {@link Closeable} as a null-safe operation. + * + * @param closeables The resource(s) to close, may be null. + * @throws IOException if an I/O error occurs. + * @since 2.8.0 + */ + public static void close(final Closeable... closeables) throws IOException { + if (closeables != null) { + for (final Closeable closeable : closeables) { + close(closeable); + } + } + } + + /** + * Closes the given {@link Closeable} as a null-safe operation. + * + * @param closeable The resource to close, may be null. + * @param consumer Consume the IOException thrown by {@link Closeable#close()}. + * @throws IOException if an I/O error occurs. + * @since 2.7 + */ + public static void close(final Closeable closeable, final IOConsumer consumer) throws IOException { + if (closeable != null) { + try { + closeable.close(); + } catch (final IOException e) { + if (consumer != null) { + consumer.accept(e); + } + } + } + } + + /** + * Closes a URLConnection. + * + * @param conn the connection to close. + * @since 2.4 + */ + public static void close(final URLConnection conn) { + if (conn instanceof HttpURLConnection) { + ((HttpURLConnection) conn).disconnect(); + } + } @SuppressWarnings("unused") public static String Stream2String(InputStream inputStream) { diff --git a/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ByteOrderMark.java b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ByteOrderMark.java index 369ec1978..3fdb6b8af 100644 --- a/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ByteOrderMark.java +++ b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ByteOrderMark.java @@ -59,6 +59,7 @@ public class ByteOrderMark implements Serializable { * @see Byte Order Mark (BOM) FAQ * @since 2.5 */ + @SuppressWarnings("unused") public static final char UTF_BOM = '\uFEFF'; private final String charsetName; diff --git a/epublib/src/main/java/me/ag2s/epublib/util/commons/io/IOConsumer.java b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/IOConsumer.java new file mode 100644 index 000000000..b4e024f71 --- /dev/null +++ b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/IOConsumer.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.ag2s.epublib.util.commons.io; + +import java.io.IOException; +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Like {@link Consumer} but throws {@link IOException}. + * + * @param the type of the input to the operations. + * @since 2.7 + */ +@FunctionalInterface +public interface IOConsumer { + + /** + * Performs this operation on the given argument. + * + * @param t the input argument + * @throws IOException if an I/O error occurs. + */ + void accept(T t) throws IOException; + + /** + * Returns a composed {@code IoConsumer} that performs, in sequence, this operation followed by the {@code after} + * operation. If performing either operation throws an exception, it is relayed to the caller of the composed + * operation. If performing this operation throws an exception, the {@code after} operation will not be performed. + * + * @param after the operation to perform after this operation + * @return a composed {@code Consumer} that performs in sequence this operation followed by the {@code after} + * operation + * @throws NullPointerException if {@code after} is null + */ + @SuppressWarnings("unused") + default IOConsumer andThen(final IOConsumer after) { + Objects.requireNonNull(after); + return (final T t) -> { + accept(t); + after.accept(t); + }; + } +} diff --git a/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ProxyInputStream.java b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ProxyInputStream.java index 25e5fd7d0..3fe2415ab 100644 --- a/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ProxyInputStream.java +++ b/epublib/src/main/java/me/ag2s/epublib/util/commons/io/ProxyInputStream.java @@ -17,10 +17,16 @@ package me.ag2s.epublib.util.commons.io; * limitations under the License. */ + import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; +import me.ag2s.epublib.util.IOUtil; + +import static me.ag2s.epublib.util.IOUtil.EOF; + + /** * A Proxy stream which acts as expected, that is it passes the method * calls on to the proxied stream and doesn't change which methods are @@ -29,27 +35,27 @@ import java.io.InputStream; * It is an alternative base class to FilterInputStream * to increase reusability, because FilterInputStream changes the * methods being called, such as read(byte[]) to read(byte[], int, int). + *

*

* See the protected methods for ways in which a subclass can easily decorate * a stream with custom pre-, post- or error processing functionality. - * - * @author Stephen Colebourne - * @version $Id: ProxyInputStream.java 934041 2010-04-14 17:37:24Z jukka $ + *

*/ public abstract class ProxyInputStream extends FilterInputStream { /** * Constructs a new ProxyInputStream. * - * @param proxy the InputStream to delegate to + * @param proxy the InputStream to delegate to */ - public ProxyInputStream(InputStream proxy) { + public ProxyInputStream(final InputStream proxy) { super(proxy); // the proxy is stored in a protected superclass variable named 'in' } /** * Invokes the delegate's read() method. + * * @return the byte read or -1 if the end of stream * @throws IOException if an I/O error occurs */ @@ -57,36 +63,38 @@ public abstract class ProxyInputStream extends FilterInputStream { public int read() throws IOException { try { beforeRead(1); - int b = in.read(); - afterRead(b != -1 ? 1 : -1); + final int b = in.read(); + afterRead(b != EOF ? 1 : EOF); return b; - } catch (IOException e) { + } catch (final IOException e) { handleIOException(e); - return -1; + return EOF; } } /** * Invokes the delegate's read(byte[]) method. + * * @param bts the buffer to read the bytes into - * @return the number of bytes read or -1 if the end of stream + * @return the number of bytes read or EOF if the end of stream * @throws IOException if an I/O error occurs */ @Override - public int read(byte[] bts) throws IOException { + public int read(final byte[] bts) throws IOException { try { - beforeRead(bts != null ? bts.length : 0); - int n = in.read(bts); + beforeRead(IOUtil.length(bts)); + final int n = in.read(bts); afterRead(n); return n; - } catch (IOException e) { + } catch (final IOException e) { handleIOException(e); - return -1; + return EOF; } } /** * Invokes the delegate's read(byte[], int, int) method. + * * @param bts the buffer to read the bytes into * @param off The start offset * @param len The number of bytes to read @@ -94,29 +102,30 @@ public abstract class ProxyInputStream extends FilterInputStream { * @throws IOException if an I/O error occurs */ @Override - public int read(byte[] bts, int off, int len) throws IOException { + public int read(final byte[] bts, final int off, final int len) throws IOException { try { beforeRead(len); - int n = in.read(bts, off, len); + final int n = in.read(bts, off, len); afterRead(n); return n; - } catch (IOException e) { + } catch (final IOException e) { handleIOException(e); - return -1; + return EOF; } } /** * Invokes the delegate's skip(long) method. + * * @param ln the number of bytes to skip * @return the actual number of bytes skipped * @throws IOException if an I/O error occurs */ @Override - public long skip(long ln) throws IOException { + public long skip(final long ln) throws IOException { try { return in.skip(ln); - } catch (IOException e) { + } catch (final IOException e) { handleIOException(e); return 0; } @@ -124,6 +133,7 @@ public abstract class ProxyInputStream extends FilterInputStream { /** * Invokes the delegate's available() method. + * * @return the number of available bytes * @throws IOException if an I/O error occurs */ @@ -131,7 +141,7 @@ public abstract class ProxyInputStream extends FilterInputStream { public int available() throws IOException { try { return super.available(); - } catch (IOException e) { + } catch (final IOException e) { handleIOException(e); return 0; } @@ -139,41 +149,41 @@ public abstract class ProxyInputStream extends FilterInputStream { /** * Invokes the delegate's close() method. + * * @throws IOException if an I/O error occurs */ @Override public void close() throws IOException { - try { - in.close(); - } catch (IOException e) { - handleIOException(e); - } + IOUtil.close(in, this::handleIOException); } /** * Invokes the delegate's mark(int) method. + * * @param readlimit read ahead limit */ @Override - public synchronized void mark(int readlimit) { + public synchronized void mark(final int readlimit) { in.mark(readlimit); } /** * Invokes the delegate's reset() method. + * * @throws IOException if an I/O error occurs */ @Override public synchronized void reset() throws IOException { try { in.reset(); - } catch (IOException e) { + } catch (final IOException e) { handleIOException(e); } } /** * Invokes the delegate's markSupported() method. + * * @return true if mark is supported, otherwise false */ @Override @@ -195,11 +205,13 @@ public abstract class ProxyInputStream extends FilterInputStream { * {@link #reset()}. You need to explicitly override those methods if * you want to add pre-processing steps also to them. * - * @since Commons IO 2.0 * @param n number of bytes that the caller asked to be read + * @since 2.0 */ @SuppressWarnings("unused") - protected void beforeRead(int n) { + + protected void beforeRead(final int n) { + // no-op } /** @@ -215,23 +227,25 @@ public abstract class ProxyInputStream extends FilterInputStream { * {@link #reset()}. You need to explicitly override those methods if * you want to add post-processing steps also to them. * - * @since Commons IO 2.0 * @param n number of bytes read, or -1 if the end of stream was reached + * @since 2.0 */ @SuppressWarnings("unused") - protected void afterRead(int n) { + protected void afterRead(final int n) { + // no-op } /** * Handle any IOExceptions thrown. *

* This method provides a point to implement custom exception - * handling. The default behaviour is to re-throw the exception. + * handling. The default behavior is to re-throw the exception. + * * @param e The IOException thrown * @throws IOException if an I/O error occurs - * @since Commons IO 2.0 + * @since 2.0 */ - protected void handleIOException(IOException e) throws IOException { + protected void handleIOException(final IOException e) throws IOException { throw e; }