# HG changeset patch # User prappo # Date 1418733816 0 # Node ID b35a5a7f6dcc7dfdbbadea9cc2ea0937521db3d6 # Parent cd22a60e9459fa047b554102ccdfe10235c856cc 8066867: Add InputStream transferTo to transfer content to an OutputStream Reviewed-by: chegar Contributed-by: Alan Bateman , Chris Hegarty , Patrick Reinhart , Pavel Rappo diff -r cd22a60e9459 -r b35a5a7f6dcc jdk/src/java.base/share/classes/java/io/InputStream.java --- a/jdk/src/java.base/share/classes/java/io/InputStream.java Tue Dec 16 04:58:34 2014 +0000 +++ b/jdk/src/java.base/share/classes/java/io/InputStream.java Tue Dec 16 12:43:36 2014 +0000 @@ -25,6 +25,8 @@ package java.io; +import java.util.Objects; + /** * This abstract class is the superclass of all classes representing * an input stream of bytes. @@ -48,6 +50,8 @@ // use when skipping. private static final int MAX_SKIP_BUFFER_SIZE = 2048; + private static final int TRANSFER_BUFFER_SIZE = 8192; + /** * Reads the next byte of data from the input stream. The value byte is * returned as an int in the range 0 to @@ -364,4 +368,40 @@ return false; } + /** + * Reads all bytes from this input stream and writes the bytes to the + * given output stream in the order that they are read. On return, this + * input stream will be at end of stream. This method does not close either + * stream. + *

+ * This method may block indefinitely reading from the input stream, or + * writing to the output stream. The behavior for the case where the input + * and/or output stream is asynchronously closed, or the thread + * interrupted during the transfer, is highly input and output stream + * specific, and therefore not specified. + *

+ * If an I/O error occurs reading from the input stream or writing to the + * output stream, then it may do so after some bytes have been read or + * written. Consequently the input stream may not be at end of stream and + * one, or both, streams may be in an inconsistent state. It is strongly + * recommended that both streams be promptly closed if an I/O error occurs. + * + * @param out the output stream, non-null + * @return the number of bytes transferred + * @throws IOException if an I/O error occurs when reading or writing + * @throws NullPointerException if {@code out} is {@code null} + * + * @since 1.9 + */ + public long transferTo(OutputStream out) throws IOException { + Objects.requireNonNull(out, "out"); + long transferred = 0; + byte[] buffer = new byte[TRANSFER_BUFFER_SIZE]; + int read; + while ((read = this.read(buffer, 0, TRANSFER_BUFFER_SIZE)) >= 0) { + out.write(buffer, 0, read); + transferred += read; + } + return transferred; + } } diff -r cd22a60e9459 -r b35a5a7f6dcc jdk/test/java/io/InputStream/TransferTo.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/jdk/test/java/io/InputStream/TransferTo.java Tue Dec 16 12:43:36 2014 +0000 @@ -0,0 +1,326 @@ +/* + * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +import java.io.ByteArrayOutputStream; +import java.io.FilterInputStream; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.Random; + +import static java.lang.String.format; + +/* + * @test + * @bug 8066867 + * @summary tests whether java.io.InputStream.transferTo conforms to its + * contract defined in the javadoc + */ +public class TransferTo { + + public static void main(String[] args) throws IOException { + ifOutIsNullThenNpeIsThrown(); + ifExceptionInInputNeitherStreamIsClosed(); + ifExceptionInOutputNeitherStreamIsClosed(); + onReturnNeitherStreamIsClosed(); + onReturnInputIsAtEnd(); + contents(); + } + + private static void ifOutIsNullThenNpeIsThrown() throws IOException { + try (InputStream in = input()) { + assertThrowsNPE(() -> in.transferTo(null), "out"); + } + + try (InputStream in = input((byte) 1)) { + assertThrowsNPE(() -> in.transferTo(null), "out"); + } + + try (InputStream in = input((byte) 1, (byte) 2)) { + assertThrowsNPE(() -> in.transferTo(null), "out"); + } + + InputStream in = null; + try { + InputStream fin = in = new ThrowingInputStream(); + // null check should precede everything else: + // InputStream shouldn't be touched if OutputStream is null + assertThrowsNPE(() -> fin.transferTo(null), "out"); + } finally { + if (in != null) + try { + in.close(); + } catch (IOException ignored) { } + } + } + + private static void ifExceptionInInputNeitherStreamIsClosed() + throws IOException { + transferToThenCheckIfAnyClosed(input(0, new byte[]{1, 2, 3}), output()); + transferToThenCheckIfAnyClosed(input(1, new byte[]{1, 2, 3}), output()); + transferToThenCheckIfAnyClosed(input(2, new byte[]{1, 2, 3}), output()); + } + + private static void ifExceptionInOutputNeitherStreamIsClosed() + throws IOException { + transferToThenCheckIfAnyClosed(input(new byte[]{1, 2, 3}), output(0)); + transferToThenCheckIfAnyClosed(input(new byte[]{1, 2, 3}), output(1)); + transferToThenCheckIfAnyClosed(input(new byte[]{1, 2, 3}), output(2)); + } + + private static void transferToThenCheckIfAnyClosed(InputStream input, + OutputStream output) + throws IOException { + try (CloseLoggingInputStream in = new CloseLoggingInputStream(input); + CloseLoggingOutputStream out = + new CloseLoggingOutputStream(output)) { + boolean thrown = false; + try { + in.transferTo(out); + } catch (IOException ignored) { + thrown = true; + } + if (!thrown) + throw new AssertionError(); + + if (in.wasClosed() || out.wasClosed()) { + throw new AssertionError(); + } + } + } + + private static void onReturnNeitherStreamIsClosed() + throws IOException { + try (CloseLoggingInputStream in = + new CloseLoggingInputStream(input(new byte[]{1, 2, 3})); + CloseLoggingOutputStream out = + new CloseLoggingOutputStream(output())) { + + in.transferTo(out); + + if (in.wasClosed() || out.wasClosed()) { + throw new AssertionError(); + } + } + } + + private static void onReturnInputIsAtEnd() throws IOException { + try (InputStream in = input(new byte[]{1, 2, 3}); + OutputStream out = output()) { + + in.transferTo(out); + + if (in.read() != -1) { + throw new AssertionError(); + } + } + } + + private static void contents() throws IOException { + checkTransferredContents(new byte[0]); + checkTransferredContents(createRandomBytes(1024, 4096)); + // to span through several batches + checkTransferredContents(createRandomBytes(16384, 16384)); + } + + private static void checkTransferredContents(byte[] bytes) + throws IOException { + try (InputStream in = input(bytes); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + in.transferTo(out); + + byte[] outBytes = out.toByteArray(); + if (!Arrays.equals(bytes, outBytes)) { + throw new AssertionError( + format("bytes.length=%s, outBytes.length=%s", + bytes.length, outBytes.length)); + } + } + } + + private static byte[] createRandomBytes(int min, int maxRandomAdditive) { + Random rnd = new Random(); + byte[] bytes = new byte[min + rnd.nextInt(maxRandomAdditive)]; + rnd.nextBytes(bytes); + return bytes; + } + + private static OutputStream output() { + return output(-1); + } + + private static OutputStream output(int exceptionPosition) { + return new OutputStream() { + + int pos; + + @Override + public void write(int b) throws IOException { + if (pos++ == exceptionPosition) + throw new IOException(); + } + }; + } + + private static InputStream input(byte... bytes) { + return input(-1, bytes); + } + + private static InputStream input(int exceptionPosition, byte... bytes) { + return new InputStream() { + + int pos; + + @Override + public int read() throws IOException { + if (pos == exceptionPosition) { + // because of the pesky IOException swallowing in + // java.io.InputStream.read(byte[], int, int) + // pos++; + throw new IOException(); + } + + if (pos >= bytes.length) + return -1; + return bytes[pos++] & 0xff; + } + }; + } + + private static class ThrowingInputStream extends InputStream { + + boolean closed; + + @Override + public int read(byte[] b) throws IOException { + throw new IOException(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + throw new IOException(); + } + + @Override + public long skip(long n) throws IOException { + throw new IOException(); + } + + @Override + public int available() throws IOException { + throw new IOException(); + } + + @Override + public void close() throws IOException { + if (!closed) { + closed = true; + throw new IOException(); + } + } + + @Override + public void reset() throws IOException { + throw new IOException(); + } + + @Override + public int read() throws IOException { + throw new IOException(); + } + } + + private static class CloseLoggingInputStream extends FilterInputStream { + + boolean closed; + + CloseLoggingInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + closed = true; + super.close(); + } + + boolean wasClosed() { + return closed; + } + } + + private static class CloseLoggingOutputStream extends FilterOutputStream { + + boolean closed; + + CloseLoggingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void close() throws IOException { + closed = true; + super.close(); + } + + boolean wasClosed() { + return closed; + } + } + + public interface Thrower { + public void run() throws Throwable; + } + + public static void assertThrowsNPE(Thrower thrower, String message) { + assertThrows(thrower, NullPointerException.class, message); + } + + public static void assertThrows(Thrower thrower, + Class throwable, + String message) { + Throwable thrown; + try { + thrower.run(); + thrown = null; + } catch (Throwable caught) { + thrown = caught; + } + + if (!throwable.isInstance(thrown)) { + String caught = thrown == null ? + "nothing" : thrown.getClass().getCanonicalName(); + throw new AssertionError( + format("Expected to catch %s, but caught %s", + throwable, caught), thrown); + } + + if (thrown != null && !message.equals(thrown.getMessage())) { + throw new AssertionError( + format("Expected exception message to be '%s', but it's '%s'", + message, thrown.getMessage())); + } + } +}