From 049ee8d7c8523ffcab76d4b83618426f3487c2d1 Mon Sep 17 00:00:00 2001 From: Samridh Srinath Date: Thu, 25 Oct 2018 22:20:58 -0700 Subject: [PATCH 1/9] 857 Support multipart files using InputStream from source file --- README.md | 1 + .../body/multipart/InputStreamPart.java | 49 ++++++++++ .../body/multipart/MultipartUtils.java | 3 + .../part/InputStreamMultipartPart.java | 92 +++++++++++++++++++ .../body/InputStreamPartLargeFileTest.java | 83 +++++++++++++++++ .../body/multipart/MultipartBodyTest.java | 13 ++- .../body/multipart/MultipartUploadTest.java | 37 ++++++++ 7 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java create mode 100644 client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java create mode 100644 client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java diff --git a/README.md b/README.md index 487cf8ffc5..eed94ccf15 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Use the `addBodyPart` method to add a multipart part to the request. This part can be of type: * `ByteArrayPart` * `FilePart` +* `InputStreamPart` * `StringPart` ### Dealing with Responses diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java new file mode 100644 index 0000000000..aa7b4a3556 --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java @@ -0,0 +1,49 @@ +package org.asynchttpclient.request.body.multipart; + +import java.io.InputStream; +import java.nio.charset.Charset; + +import static org.asynchttpclient.util.Assertions.assertNotNull; + +public class InputStreamPart extends FileLikePart { + + private final InputStream inputStream; + private final long contentLength; + + public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName) { + this(name, inputStream, contentLength, fileName, null); + } + + public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType) { + this(name, inputStream, contentLength, fileName, contentType, null); + } + + public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType, Charset charset) { + this(name, inputStream, contentLength, fileName, contentType, charset, null); + } + + public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType, Charset charset, + String contentId) { + this(name, inputStream, contentLength, fileName, contentType, charset, contentId, null); + } + + public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType, Charset charset, + String contentId, String transferEncoding) { + super(name, + contentType, + charset, + fileName, + contentId, + transferEncoding); + this.inputStream = assertNotNull(inputStream, "inputStream"); + this.contentLength = contentLength; + } + + public InputStream getInputStream() { + return inputStream; + } + + public long getContentLength() { + return contentLength; + } +} diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java index 94bcb295d5..78e2d130a4 100644 --- a/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java @@ -75,6 +75,9 @@ public static List> generateMultipartParts(List { + + private long position = 0L; + private ByteBuffer buffer; + private ReadableByteChannel channel; + + public InputStreamMultipartPart(InputStreamPart part, byte[] boundary) { + super(part, boundary); + } + + private ByteBuffer getBuffer() { + if (buffer == null) { + buffer = ByteBuffer.allocateDirect(BodyChunkedInput.DEFAULT_CHUNK_SIZE); + } + return buffer; + } + + private ReadableByteChannel getChannel() { + if (channel == null) { + channel = Channels.newChannel(part.getInputStream()); + } + return channel; + } + + @Override + protected long getContentLength() { + return part.getContentLength(); + } + + @Override + protected long transferContentTo(ByteBuf target) throws IOException { + InputStream inputStream = part.getInputStream(); + int transferred = target.writeBytes(inputStream, target.writableBytes()); + if (transferred > 0) { + position += transferred; + } + if (position == getContentLength() || transferred < 0) { + state = MultipartState.POST_CONTENT; + inputStream.close(); + } + return transferred; + } + + @Override + protected long transferContentTo(WritableByteChannel target) throws IOException { + ReadableByteChannel channel = getChannel(); + ByteBuffer buffer = getBuffer(); + + int transferred = 0; + int read = channel.read(buffer); + + if (read > 0) { + buffer.flip(); + while (buffer.hasRemaining()) { + transferred += target.write(buffer); + } + buffer.compact(); + position += transferred; + } + if (position == getContentLength() || read < 0) { + state = MultipartState.POST_CONTENT; + if (channel.isOpen()) { + channel.close(); + } + } + + return transferred; + } + + @Override + public void close() { + super.close(); + closeSilently(part.getInputStream()); + closeSilently(channel); + } + +} diff --git a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java new file mode 100644 index 0000000000..64bed4e0e1 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010-2012 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.request.body; + +import org.asynchttpclient.AbstractBasicTest; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.Response; +import org.asynchttpclient.request.body.multipart.FilePart; +import org.asynchttpclient.request.body.multipart.InputStreamPart; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.testng.annotations.Test; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.asynchttpclient.Dsl.asyncHttpClient; +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.test.TestUtils.LARGE_IMAGE_FILE; +import static org.asynchttpclient.test.TestUtils.createTempFile; +import static org.testng.Assert.assertEquals; + +public class InputStreamPartLargeFileTest extends AbstractBasicTest { + + @Override + public AbstractHandler configureHandler() throws Exception { + return new AbstractHandler() { + + public void handle(String target, Request baseRequest, HttpServletRequest req, HttpServletResponse resp) throws IOException { + + ServletInputStream in = req.getInputStream(); + byte[] b = new byte[8192]; + + int count; + int total = 0; + while ((count = in.read(b)) != -1) { + b = new byte[8192]; + total += count; + } + resp.setStatus(200); + resp.addHeader("X-TRANSFERRED", String.valueOf(total)); + resp.getOutputStream().flush(); + resp.getOutputStream().close(); + + baseRequest.setHandled(true); + } + }; + } + + @Test + public void testPutImageFile() throws Exception { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); + Response response = client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.length(), LARGE_IMAGE_FILE.getName(), "application/octet-stream", UTF_8)).execute().get(); + assertEquals(response.getStatusCode(), 200); + } + } + + @Test + public void testPutLargeTextFile() throws Exception { + File file = createTempFile(1024 * 1024); + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + Response response = client.preparePut(getTargetUrl()) + .addBodyPart(new InputStreamPart("test", inputStream, file.length(), file.getName(), "application/octet-stream", UTF_8)).execute().get(); + assertEquals(response.getStatusCode(), 200); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java index 0b6c5fe6f2..2d8ed91178 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java @@ -19,8 +19,7 @@ import org.asynchttpclient.request.body.Body.BodyState; import org.testng.annotations.Test; -import java.io.File; -import java.io.IOException; +import java.io.*; import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; @@ -63,7 +62,15 @@ private static File getTestfile() throws URISyntaxException { } private static MultipartBody buildMultipart() { - return MultipartUtils.newMultipartBody(PARTS, EmptyHttpHeaders.INSTANCE); + List parts = new ArrayList<>(PARTS); + try { + File testFile = getTestfile(); + InputStream inputStream = new BufferedInputStream(new FileInputStream(testFile)); + parts.add(new InputStreamPart("isPart", inputStream, testFile.length(), testFile.getName())); + } catch (URISyntaxException | FileNotFoundException e) { + throw new ExceptionInInitializerError(e); + } + return MultipartUtils.newMultipartBody(parts, EmptyHttpHeaders.INSTANCE); } private static long transferWithCopy(MultipartBody multipartBody, int bufferSize) throws IOException { diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java index 77584ecdf3..567b190fc8 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java @@ -77,21 +77,33 @@ public void testSendingSmallFilesAndByteArray() throws Exception { File testResource1File = getClasspathFile(testResource1); File testResource2File = getClasspathFile(testResource2); File testResource3File = getClasspathFile(testResource3); + InputStream inputStreamFile1 = new BufferedInputStream(new FileInputStream(testResource1File)); + InputStream inputStreamFile2 = new BufferedInputStream(new FileInputStream(testResource2File)); + InputStream inputStreamFile3 = new BufferedInputStream(new FileInputStream(testResource3File)); List testFiles = new ArrayList<>(); testFiles.add(testResource1File); testFiles.add(testResource2File); testFiles.add(testResource3File); + testFiles.add(testResource3File); + testFiles.add(testResource2File); + testFiles.add(testResource1File); List expected = new ArrayList<>(); expected.add(expectedContents); expected.add(expectedContents2); expected.add(expectedContents3); + expected.add(expectedContents3); + expected.add(expectedContents2); + expected.add(expectedContents); List gzipped = new ArrayList<>(); gzipped.add(false); gzipped.add(true); gzipped.add(false); + gzipped.add(false); + gzipped.add(true); + gzipped.add(false); File tmpFile = File.createTempFile("textbytearray", ".txt"); try (OutputStream os = Files.newOutputStream(tmpFile.toPath())) { @@ -109,8 +121,11 @@ public void testSendingSmallFilesAndByteArray() throws Exception { .addBodyPart(new StringPart("Name", "Dominic")) .addBodyPart(new FilePart("file3", testResource3File, "text/plain", UTF_8)) .addBodyPart(new StringPart("Age", "3")).addBodyPart(new StringPart("Height", "shrimplike")) + .addBodyPart(new InputStreamPart("inputStream3", inputStreamFile3, testResource3File.length(), testResource3File.getName(), "text/plain", UTF_8)) + .addBodyPart(new InputStreamPart("inputStream2", inputStreamFile2, testResource2File.length(), testResource2File.getName(), "application/x-gzip", null)) .addBodyPart(new StringPart("Hair", "ridiculous")).addBodyPart(new ByteArrayPart("file4", expectedContents.getBytes(UTF_8), "text/plain", UTF_8, "bytearray.txt")) + .addBodyPart(new InputStreamPart("inputStream1", inputStreamFile1, testResource1File.length(), testResource1File.getName(), "text/plain", UTF_8)) .build(); Response res = c.executeRequest(r).get(); @@ -142,6 +157,28 @@ public void sendEmptyFileZeroCopy() throws Exception { sendEmptyFile0(false); } + private void sendEmptyFileInputStream0(boolean disableZeroCopy) throws Exception { + File file = getClasspathFile("empty.txt"); + try (AsyncHttpClient c = asyncHttpClient(config().setDisableZeroCopy(disableZeroCopy))) { + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + Request r = post("http://localhost" + ":" + port1 + "/upload") + .addBodyPart(new InputStreamPart("file", inputStream, file.length(), file.getName(), "text/plain", UTF_8)).build(); + + Response res = c.executeRequest(r).get(); + assertEquals(res.getStatusCode(), 200); + } + } + + @Test + public void sendEmptyFileInputStream() throws Exception { + sendEmptyFileInputStream0(true); + } + + @Test + public void sendEmptyFileInputStreamZeroCopy() throws Exception { + sendEmptyFileInputStream0(false); + } + /** * Test that the files were sent, based on the response from the servlet */ From 1a6c52223733b6f1f4d19c573e3585278c925d94 Mon Sep 17 00:00:00 2001 From: Samridh Srinath Date: Fri, 26 Oct 2018 10:33:05 -0700 Subject: [PATCH 2/9] Added explicit constructor for -1 contentLength - Added tests for all combinations of known/unknown contentLength and enabled/disabled zeroCopy for `InputStreamPart` - Fixed how `length()` is computed for `MultipartPart` - returns -1 if contentLength < 0 - Added condition in `NettyBodyBody` to use `BodyChunkedBody` for "zeroCopy" of `RandomAccessBody` if contentLength is -1 --- .../netty/request/body/NettyBodyBody.java | 8 ++- .../body/multipart/InputStreamPart.java | 22 +++++--- .../body/multipart/part/MultipartPart.java | 4 ++ .../body/InputStreamPartLargeFileTest.java | 5 +- .../body/multipart/MultipartBodyTest.java | 2 +- .../body/multipart/MultipartUploadTest.java | 55 ++++++++++++++++--- 6 files changed, 73 insertions(+), 23 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java index 728e2ec896..abfb6aba78 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java @@ -54,7 +54,13 @@ public void write(final Channel channel, NettyResponseFuture future) { Object msg; if (body instanceof RandomAccessBody && !ChannelManager.isSslHandlerConfigured(channel.pipeline()) && !config.isDisableZeroCopy()) { - msg = new BodyFileRegion((RandomAccessBody) body); + long contentLength = getContentLength(); + if (contentLength < 0) { + // contentLength unknown in advance, use chunked input + msg = new BodyChunkedInput(body); + } else { + msg = new BodyFileRegion((RandomAccessBody) body); + } } else { msg = new BodyChunkedInput(body); diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java index aa7b4a3556..28c51cf119 100644 --- a/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java @@ -10,24 +10,28 @@ public class InputStreamPart extends FileLikePart { private final InputStream inputStream; private final long contentLength; - public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName) { - this(name, inputStream, contentLength, fileName, null); + public InputStreamPart(String name, InputStream inputStream, String fileName) { + this(name, inputStream, fileName, -1); } - public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType) { - this(name, inputStream, contentLength, fileName, contentType, null); + public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength) { + this(name, inputStream, fileName, contentLength, null); } - public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType, Charset charset) { - this(name, inputStream, contentLength, fileName, contentType, charset, null); + public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType) { + this(name, inputStream, fileName, contentLength, contentType, null); } - public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType, Charset charset, + public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType, Charset charset) { + this(name, inputStream, fileName, contentLength, contentType, charset, null); + } + + public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType, Charset charset, String contentId) { - this(name, inputStream, contentLength, fileName, contentType, charset, contentId, null); + this(name, inputStream, fileName, contentLength, contentType, charset, contentId, null); } - public InputStreamPart(String name, InputStream inputStream, long contentLength, String fileName, String contentType, Charset charset, + public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType, Charset charset, String contentId, String transferEncoding) { super(name, contentType, diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java index 38041338e8..b8c8622680 100644 --- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java @@ -106,6 +106,10 @@ public abstract class MultipartPart implements Closeable { } public long length() { + long contentLength = getContentLength(); + if (contentLength < 0) { + return contentLength; + } return preContentLength + postContentLength + getContentLength(); } diff --git a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java index 64bed4e0e1..ae9f6ddc60 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java @@ -15,7 +15,6 @@ import org.asynchttpclient.AbstractBasicTest; import org.asynchttpclient.AsyncHttpClient; import org.asynchttpclient.Response; -import org.asynchttpclient.request.body.multipart.FilePart; import org.asynchttpclient.request.body.multipart.InputStreamPart; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.AbstractHandler; @@ -64,7 +63,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest req, H public void testPutImageFile() throws Exception { try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); - Response response = client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.length(), LARGE_IMAGE_FILE.getName(), "application/octet-stream", UTF_8)).execute().get(); + Response response = client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), LARGE_IMAGE_FILE.length(), "application/octet-stream", UTF_8)).execute().get(); assertEquals(response.getStatusCode(), 200); } } @@ -76,7 +75,7 @@ public void testPutLargeTextFile() throws Exception { try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { Response response = client.preparePut(getTargetUrl()) - .addBodyPart(new InputStreamPart("test", inputStream, file.length(), file.getName(), "application/octet-stream", UTF_8)).execute().get(); + .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), file.length(), "application/octet-stream", UTF_8)).execute().get(); assertEquals(response.getStatusCode(), 200); } } diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java index 2d8ed91178..fc54d396ac 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java @@ -66,7 +66,7 @@ private static MultipartBody buildMultipart() { try { File testFile = getTestfile(); InputStream inputStream = new BufferedInputStream(new FileInputStream(testFile)); - parts.add(new InputStreamPart("isPart", inputStream, testFile.length(), testFile.getName())); + parts.add(new InputStreamPart("isPart", inputStream, testFile.getName(), testFile.length())); } catch (URISyntaxException | FileNotFoundException e) { throw new ExceptionInInitializerError(e); } diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java index 567b190fc8..879a40a9d7 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java @@ -121,11 +121,11 @@ public void testSendingSmallFilesAndByteArray() throws Exception { .addBodyPart(new StringPart("Name", "Dominic")) .addBodyPart(new FilePart("file3", testResource3File, "text/plain", UTF_8)) .addBodyPart(new StringPart("Age", "3")).addBodyPart(new StringPart("Height", "shrimplike")) - .addBodyPart(new InputStreamPart("inputStream3", inputStreamFile3, testResource3File.length(), testResource3File.getName(), "text/plain", UTF_8)) - .addBodyPart(new InputStreamPart("inputStream2", inputStreamFile2, testResource2File.length(), testResource2File.getName(), "application/x-gzip", null)) + .addBodyPart(new InputStreamPart("inputStream3", inputStreamFile3, testResource3File.getName(), testResource3File.length(), "text/plain", UTF_8)) + .addBodyPart(new InputStreamPart("inputStream2", inputStreamFile2, testResource2File.getName(), testResource2File.length(), "application/x-gzip", null)) .addBodyPart(new StringPart("Hair", "ridiculous")).addBodyPart(new ByteArrayPart("file4", expectedContents.getBytes(UTF_8), "text/plain", UTF_8, "bytearray.txt")) - .addBodyPart(new InputStreamPart("inputStream1", inputStreamFile1, testResource1File.length(), testResource1File.getName(), "text/plain", UTF_8)) + .addBodyPart(new InputStreamPart("inputStream1", inputStreamFile1, testResource1File.getName(), testResource1File.length(), "text/plain", UTF_8)) .build(); Response res = c.executeRequest(r).get(); @@ -157,12 +157,12 @@ public void sendEmptyFileZeroCopy() throws Exception { sendEmptyFile0(false); } - private void sendEmptyFileInputStream0(boolean disableZeroCopy) throws Exception { + private void sendEmptyFileInputStream(boolean disableZeroCopy) throws Exception { File file = getClasspathFile("empty.txt"); try (AsyncHttpClient c = asyncHttpClient(config().setDisableZeroCopy(disableZeroCopy))) { InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); Request r = post("http://localhost" + ":" + port1 + "/upload") - .addBodyPart(new InputStreamPart("file", inputStream, file.length(), file.getName(), "text/plain", UTF_8)).build(); + .addBodyPart(new InputStreamPart("file", inputStream, file.getName(), file.length(), "text/plain", UTF_8)).build(); Response res = c.executeRequest(r).get(); assertEquals(res.getStatusCode(), 200); @@ -170,13 +170,50 @@ private void sendEmptyFileInputStream0(boolean disableZeroCopy) throws Exception } @Test - public void sendEmptyFileInputStream() throws Exception { - sendEmptyFileInputStream0(true); + public void testSendEmptyFileInputStream() throws Exception { + sendEmptyFileInputStream(true); } @Test - public void sendEmptyFileInputStreamZeroCopy() throws Exception { - sendEmptyFileInputStream0(false); + public void testSendEmptyFileInputStreamZeroCopy() throws Exception { + sendEmptyFileInputStream(false); + } + + private void sendFileInputStream(boolean useContentLength, boolean disableZeroCopy) throws Exception { + File file = getClasspathFile("textfile.txt"); + try (AsyncHttpClient c = asyncHttpClient(config().setDisableZeroCopy(disableZeroCopy))) { + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + InputStreamPart part; + if (useContentLength) { + part = new InputStreamPart("file", inputStream, file.getName(), file.length()); + } else { + part = new InputStreamPart("file", inputStream, file.getName()); + } + Request r = post("http://localhost" + ":" + port1 + "/upload").addBodyPart(part).build(); + + Response res = c.executeRequest(r).get(); + assertEquals(res.getStatusCode(), 200); + } + } + + @Test + public void testSendFileInputStreamUnknownContentLength() throws Exception { + sendFileInputStream(false, true); + } + + @Test + public void testSendFileInputStreamZeroCopyUnknownContentLength() throws Exception { + sendFileInputStream(false, false); + } + + @Test + public void testSendFileInputStreamKnownContentLength() throws Exception { + sendFileInputStream(true, true); + } + + @Test + public void testSendFileInputStreamZeroCopyKnownContentLength() throws Exception { + sendFileInputStream(true, false); } /** From 806670f693ac4fe4b98e4fe8fbf357432342f990 Mon Sep 17 00:00:00 2001 From: Samridh Srinath Date: Fri, 26 Oct 2018 11:09:26 -0700 Subject: [PATCH 3/9] Add unknown-content-length coverage for large file uploads using InputStreamPart --- .../body/InputStreamPartLargeFileTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java index ae9f6ddc60..19992b89aa 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java @@ -68,6 +68,15 @@ public void testPutImageFile() throws Exception { } } + @Test + public void testPutImageFileUnknownSize() throws Exception { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); + Response response = client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); + assertEquals(response.getStatusCode(), 200); + } + } + @Test public void testPutLargeTextFile() throws Exception { File file = createTempFile(1024 * 1024); @@ -79,4 +88,16 @@ public void testPutLargeTextFile() throws Exception { assertEquals(response.getStatusCode(), 200); } } + + @Test + public void testPutLargeTextFileUnknownSize() throws Exception { + File file = createTempFile(1024 * 1024); + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + Response response = client.preparePut(getTargetUrl()) + .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); + assertEquals(response.getStatusCode(), 200); + } + } } From 7ab6f45df0550217fcf34546530c22a16a5dbcb6 Mon Sep 17 00:00:00 2001 From: Samridh Srinath Date: Fri, 26 Oct 2018 15:10:42 -0700 Subject: [PATCH 4/9] Remove zero-copy implementation for InputStreamPart - Fix tests, add additional tests to cover various combinations --- .../netty/request/body/NettyBodyBody.java | 8 +-- .../part/InputStreamMultipartPart.java | 44 +-------------- .../body/InputStreamPartLargeFileTest.java | 54 ++++++++++++++++--- .../body/multipart/MultipartBodyTest.java | 31 +++++++++++ .../body/multipart/MultipartUploadTest.java | 51 +++++++++++++----- 5 files changed, 118 insertions(+), 70 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java index abfb6aba78..728e2ec896 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java @@ -54,13 +54,7 @@ public void write(final Channel channel, NettyResponseFuture future) { Object msg; if (body instanceof RandomAccessBody && !ChannelManager.isSslHandlerConfigured(channel.pipeline()) && !config.isDisableZeroCopy()) { - long contentLength = getContentLength(); - if (contentLength < 0) { - // contentLength unknown in advance, use chunked input - msg = new BodyChunkedInput(body); - } else { - msg = new BodyFileRegion((RandomAccessBody) body); - } + msg = new BodyFileRegion((RandomAccessBody) body); } else { msg = new BodyChunkedInput(body); diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java index 7340dc6e71..52e35f234b 100644 --- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java @@ -1,14 +1,10 @@ package org.asynchttpclient.request.body.multipart.part; import io.netty.buffer.ByteBuf; -import org.asynchttpclient.netty.request.body.BodyChunkedInput; import org.asynchttpclient.request.body.multipart.InputStreamPart; import java.io.IOException; import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import static org.asynchttpclient.util.MiscUtils.closeSilently; @@ -16,27 +12,11 @@ public class InputStreamMultipartPart extends FileLikeMultipartPart { private long position = 0L; - private ByteBuffer buffer; - private ReadableByteChannel channel; public InputStreamMultipartPart(InputStreamPart part, byte[] boundary) { super(part, boundary); } - private ByteBuffer getBuffer() { - if (buffer == null) { - buffer = ByteBuffer.allocateDirect(BodyChunkedInput.DEFAULT_CHUNK_SIZE); - } - return buffer; - } - - private ReadableByteChannel getChannel() { - if (channel == null) { - channel = Channels.newChannel(part.getInputStream()); - } - return channel; - } - @Override protected long getContentLength() { return part.getContentLength(); @@ -58,35 +38,13 @@ protected long transferContentTo(ByteBuf target) throws IOException { @Override protected long transferContentTo(WritableByteChannel target) throws IOException { - ReadableByteChannel channel = getChannel(); - ByteBuffer buffer = getBuffer(); - - int transferred = 0; - int read = channel.read(buffer); - - if (read > 0) { - buffer.flip(); - while (buffer.hasRemaining()) { - transferred += target.write(buffer); - } - buffer.compact(); - position += transferred; - } - if (position == getContentLength() || read < 0) { - state = MultipartState.POST_CONTENT; - if (channel.isOpen()) { - channel.close(); - } - } - - return transferred; + throw new UnsupportedOperationException("InputStreamPart does not support zero-copy transfers"); } @Override public void close() { super.close(); closeSilently(part.getInputStream()); - closeSilently(channel); } } diff --git a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java index 19992b89aa..320b0289f5 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java @@ -24,6 +24,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; +import java.util.concurrent.ExecutionException; import static java.nio.charset.StandardCharsets.UTF_8; import static org.asynchttpclient.Dsl.asyncHttpClient; @@ -59,30 +60,71 @@ public void handle(String target, Request baseRequest, HttpServletRequest req, H }; } + @Test(expectedExceptions = ExecutionException.class) + public void testPutImageFileThrowsExecutionException() throws Exception { + // Should throw ExecutionException when zero-copy is enabled + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); + client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), LARGE_IMAGE_FILE.length(), "application/octet-stream", UTF_8)).execute().get(); + } + } + + @Test(expectedExceptions = ExecutionException.class) + public void testPutImageFileUnknownSizeThrowsExecutionException() throws Exception { + // Should throw ExecutionException when zero-copy is enabled + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); + client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); + } + } + @Test public void testPutImageFile() throws Exception { - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000).setDisableZeroCopy(true))) { InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); - Response response = client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), LARGE_IMAGE_FILE.length(), "application/octet-stream", UTF_8)).execute().get(); - assertEquals(response.getStatusCode(), 200); + client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), LARGE_IMAGE_FILE.length(), "application/octet-stream", UTF_8)).execute().get(); } } @Test public void testPutImageFileUnknownSize() throws Exception { - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000).setDisableZeroCopy(true))) { InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); Response response = client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); assertEquals(response.getStatusCode(), 200); } } + @Test(expectedExceptions = ExecutionException.class) + public void testPutLargeTextFileThrowsExecutionException() throws Exception { + File file = createTempFile(1024 * 1024); + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + Response response = client.preparePut(getTargetUrl()) + .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), file.length(), "application/octet-stream", UTF_8)).execute().get(); + assertEquals(response.getStatusCode(), 200); + } + } + + @Test(expectedExceptions = ExecutionException.class) + public void testPutLargeTextFileUnknownSizeThrowsExecutionException() throws Exception { + File file = createTempFile(1024 * 1024); + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + Response response = client.preparePut(getTargetUrl()) + .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); + assertEquals(response.getStatusCode(), 200); + } + } + @Test public void testPutLargeTextFile() throws Exception { File file = createTempFile(1024 * 1024); InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000).setDisableZeroCopy(true))) { Response response = client.preparePut(getTargetUrl()) .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), file.length(), "application/octet-stream", UTF_8)).execute().get(); assertEquals(response.getStatusCode(), 200); @@ -94,7 +136,7 @@ public void testPutLargeTextFileUnknownSize() throws Exception { File file = createTempFile(1024 * 1024); InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000).setDisableZeroCopy(true))) { Response response = client.preparePut(getTargetUrl()) .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); assertEquals(response.getStatusCode(), 200); diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java index fc54d396ac..382f2d208e 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java @@ -36,6 +36,7 @@ public class MultipartBodyTest { private static final List PARTS = new ArrayList<>(); private static long MAX_MULTIPART_CONTENT_LENGTH_ESTIMATE; + private static long MAX_MULTIPART_CONTENT_LENGTH_WITH_INPUT_STREAM_PART_ESTIMATE; static { try { @@ -54,6 +55,13 @@ public class MultipartBodyTest { } } + static { + try (MultipartBody dummyBody = buildMultipartWithInputStreamPart()) { + // separator is random + MAX_MULTIPART_CONTENT_LENGTH_WITH_INPUT_STREAM_PART_ESTIMATE = dummyBody.getContentLength() + 100; + } + } + private static File getTestfile() throws URISyntaxException { final ClassLoader cl = MultipartBodyTest.class.getClassLoader(); final URL url = cl.getResource("textfile.txt"); @@ -62,6 +70,10 @@ private static File getTestfile() throws URISyntaxException { } private static MultipartBody buildMultipart() { + return MultipartUtils.newMultipartBody(PARTS, EmptyHttpHeaders.INSTANCE); + } + + private static MultipartBody buildMultipartWithInputStreamPart() { List parts = new ArrayList<>(PARTS); try { File testFile = getTestfile(); @@ -128,6 +140,16 @@ public void transferWithCopy() throws Exception { } } + @Test + public void transferWithCopyAndInputStreamPart() throws Exception { + for (int bufferLength = 1; bufferLength < MAX_MULTIPART_CONTENT_LENGTH_WITH_INPUT_STREAM_PART_ESTIMATE + 1; bufferLength++) { + try (MultipartBody multipartBody = buildMultipartWithInputStreamPart()) { + long transferred = transferWithCopy(multipartBody, bufferLength); + assertEquals(transferred, multipartBody.getContentLength()); + } + } + } + @Test public void transferZeroCopy() throws Exception { for (int bufferLength = 1; bufferLength < MAX_MULTIPART_CONTENT_LENGTH_ESTIMATE + 1; bufferLength++) { @@ -137,4 +159,13 @@ public void transferZeroCopy() throws Exception { } } } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void transferZeroCopyWithInputStreamPart() throws Exception { + for (int bufferLength = 1; bufferLength < MAX_MULTIPART_CONTENT_LENGTH_WITH_INPUT_STREAM_PART_ESTIMATE + 1; bufferLength++) { + try (MultipartBody multipartBody = buildMultipartWithInputStreamPart()) { + transferZeroCopy(multipartBody, bufferLength); + } + } + } } diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java index 879a40a9d7..f8729b294a 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java @@ -41,6 +41,7 @@ import java.util.Arrays; import java.util.List; import java.util.UUID; +import java.util.concurrent.ExecutionException; import java.util.zip.GZIPInputStream; import static java.nio.charset.StandardCharsets.UTF_8; @@ -85,25 +86,16 @@ public void testSendingSmallFilesAndByteArray() throws Exception { testFiles.add(testResource1File); testFiles.add(testResource2File); testFiles.add(testResource3File); - testFiles.add(testResource3File); - testFiles.add(testResource2File); - testFiles.add(testResource1File); List expected = new ArrayList<>(); expected.add(expectedContents); expected.add(expectedContents2); expected.add(expectedContents3); - expected.add(expectedContents3); - expected.add(expectedContents2); - expected.add(expectedContents); List gzipped = new ArrayList<>(); gzipped.add(false); gzipped.add(true); gzipped.add(false); - gzipped.add(false); - gzipped.add(true); - gzipped.add(false); File tmpFile = File.createTempFile("textbytearray", ".txt"); try (OutputStream os = Files.newOutputStream(tmpFile.toPath())) { @@ -121,10 +113,41 @@ public void testSendingSmallFilesAndByteArray() throws Exception { .addBodyPart(new StringPart("Name", "Dominic")) .addBodyPart(new FilePart("file3", testResource3File, "text/plain", UTF_8)) .addBodyPart(new StringPart("Age", "3")).addBodyPart(new StringPart("Height", "shrimplike")) - .addBodyPart(new InputStreamPart("inputStream3", inputStreamFile3, testResource3File.getName(), testResource3File.length(), "text/plain", UTF_8)) - .addBodyPart(new InputStreamPart("inputStream2", inputStreamFile2, testResource2File.getName(), testResource2File.length(), "application/x-gzip", null)) .addBodyPart(new StringPart("Hair", "ridiculous")).addBodyPart(new ByteArrayPart("file4", expectedContents.getBytes(UTF_8), "text/plain", UTF_8, "bytearray.txt")) + .build(); + + Response res = c.executeRequest(r).get(); + + assertEquals(res.getStatusCode(), 200); + + testSentFile(expected, testFiles, res, gzipped); + } + + testFiles.add(testResource3File); + testFiles.add(testResource2File); + testFiles.add(testResource1File); + + expected.add(expectedContents3); + expected.add(expectedContents2); + expected.add(expectedContents); + + gzipped.add(false); + gzipped.add(true); + gzipped.add(false); + + // Zero-copy should be disabled when using InputStreamPart + try (AsyncHttpClient c = asyncHttpClient(config().setDisableZeroCopy(true))) { + Request r = post("http://localhost" + ":" + port1 + "/upload") + .addBodyPart(new FilePart("file1", testResource1File, "text/plain", UTF_8)) + .addBodyPart(new FilePart("file2", testResource2File, "application/x-gzip", null)) + .addBodyPart(new StringPart("Name", "Dominic")) + .addBodyPart(new FilePart("file3", testResource3File, "text/plain", UTF_8)) + .addBodyPart(new StringPart("Age", "3")).addBodyPart(new StringPart("Height", "shrimplike")) + .addBodyPart(new ByteArrayPart("file4", expectedContents.getBytes(UTF_8), "text/plain", UTF_8, "bytearray.txt")) + .addBodyPart(new InputStreamPart("inputStream3", inputStreamFile3, testResource3File.getName(), testResource3File.length(), "text/plain", UTF_8)) + .addBodyPart(new InputStreamPart("inputStream2", inputStreamFile2, testResource2File.getName(), testResource2File.length(), "application/x-gzip", null)) + .addBodyPart(new StringPart("Hair", "ridiculous")) .addBodyPart(new InputStreamPart("inputStream1", inputStreamFile1, testResource1File.getName(), testResource1File.length(), "text/plain", UTF_8)) .build(); @@ -174,7 +197,7 @@ public void testSendEmptyFileInputStream() throws Exception { sendEmptyFileInputStream(true); } - @Test + @Test(expectedExceptions = ExecutionException.class) public void testSendEmptyFileInputStreamZeroCopy() throws Exception { sendEmptyFileInputStream(false); } @@ -201,7 +224,7 @@ public void testSendFileInputStreamUnknownContentLength() throws Exception { sendFileInputStream(false, true); } - @Test + @Test(expectedExceptions = ExecutionException.class) public void testSendFileInputStreamZeroCopyUnknownContentLength() throws Exception { sendFileInputStream(false, false); } @@ -211,7 +234,7 @@ public void testSendFileInputStreamKnownContentLength() throws Exception { sendFileInputStream(true, true); } - @Test + @Test(expectedExceptions = ExecutionException.class) public void testSendFileInputStreamZeroCopyKnownContentLength() throws Exception { sendFileInputStream(true, false); } From 0a1e490e050e3d00cdc07cac7b1654f7940e851d Mon Sep 17 00:00:00 2001 From: Samridh Srinath Date: Fri, 26 Oct 2018 15:18:57 -0700 Subject: [PATCH 5/9] Update README.md to document lack of support for zero-copy with InputStreamPart --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index eed94ccf15..c438e3cab5 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,8 @@ This part can be of type: * `InputStreamPart` * `StringPart` +**NOTE**: `InputStreamPart` does not support zero-copy transfers. Explicitly disable zero-copy using `config.setDisableZeroCopy(true)` when one of the body parts is an `InputStreamPart`. + ### Dealing with Responses #### Blocking on the Future From db29e79754958d9984b09a7793ba7dcb3695d0ff Mon Sep 17 00:00:00 2001 From: Samridh Srinath Date: Sat, 27 Oct 2018 09:37:26 -0700 Subject: [PATCH 6/9] Revert "Update README.md to document lack of support for zero-copy with InputStreamPart" This reverts commit 0a1e490e050e3d00cdc07cac7b1654f7940e851d. --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index c438e3cab5..eed94ccf15 100644 --- a/README.md +++ b/README.md @@ -113,8 +113,6 @@ This part can be of type: * `InputStreamPart` * `StringPart` -**NOTE**: `InputStreamPart` does not support zero-copy transfers. Explicitly disable zero-copy using `config.setDisableZeroCopy(true)` when one of the body parts is an `InputStreamPart`. - ### Dealing with Responses #### Blocking on the Future From 7f5c687d3593161e9525ecf64f9d39cdc9c741b1 Mon Sep 17 00:00:00 2001 From: Samridh Srinath Date: Sat, 27 Oct 2018 09:37:41 -0700 Subject: [PATCH 7/9] Revert "Remove zero-copy implementation for InputStreamPart" This reverts commit 7ab6f45df0550217fcf34546530c22a16a5dbcb6. --- .../netty/request/body/NettyBodyBody.java | 8 ++- .../part/InputStreamMultipartPart.java | 44 ++++++++++++++- .../body/InputStreamPartLargeFileTest.java | 54 +++---------------- .../body/multipart/MultipartBodyTest.java | 31 ----------- .../body/multipart/MultipartUploadTest.java | 51 +++++------------- 5 files changed, 70 insertions(+), 118 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java index 728e2ec896..abfb6aba78 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java @@ -54,7 +54,13 @@ public void write(final Channel channel, NettyResponseFuture future) { Object msg; if (body instanceof RandomAccessBody && !ChannelManager.isSslHandlerConfigured(channel.pipeline()) && !config.isDisableZeroCopy()) { - msg = new BodyFileRegion((RandomAccessBody) body); + long contentLength = getContentLength(); + if (contentLength < 0) { + // contentLength unknown in advance, use chunked input + msg = new BodyChunkedInput(body); + } else { + msg = new BodyFileRegion((RandomAccessBody) body); + } } else { msg = new BodyChunkedInput(body); diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java index 52e35f234b..7340dc6e71 100644 --- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java @@ -1,10 +1,14 @@ package org.asynchttpclient.request.body.multipart.part; import io.netty.buffer.ByteBuf; +import org.asynchttpclient.netty.request.body.BodyChunkedInput; import org.asynchttpclient.request.body.multipart.InputStreamPart; import java.io.IOException; import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; import static org.asynchttpclient.util.MiscUtils.closeSilently; @@ -12,11 +16,27 @@ public class InputStreamMultipartPart extends FileLikeMultipartPart { private long position = 0L; + private ByteBuffer buffer; + private ReadableByteChannel channel; public InputStreamMultipartPart(InputStreamPart part, byte[] boundary) { super(part, boundary); } + private ByteBuffer getBuffer() { + if (buffer == null) { + buffer = ByteBuffer.allocateDirect(BodyChunkedInput.DEFAULT_CHUNK_SIZE); + } + return buffer; + } + + private ReadableByteChannel getChannel() { + if (channel == null) { + channel = Channels.newChannel(part.getInputStream()); + } + return channel; + } + @Override protected long getContentLength() { return part.getContentLength(); @@ -38,13 +58,35 @@ protected long transferContentTo(ByteBuf target) throws IOException { @Override protected long transferContentTo(WritableByteChannel target) throws IOException { - throw new UnsupportedOperationException("InputStreamPart does not support zero-copy transfers"); + ReadableByteChannel channel = getChannel(); + ByteBuffer buffer = getBuffer(); + + int transferred = 0; + int read = channel.read(buffer); + + if (read > 0) { + buffer.flip(); + while (buffer.hasRemaining()) { + transferred += target.write(buffer); + } + buffer.compact(); + position += transferred; + } + if (position == getContentLength() || read < 0) { + state = MultipartState.POST_CONTENT; + if (channel.isOpen()) { + channel.close(); + } + } + + return transferred; } @Override public void close() { super.close(); closeSilently(part.getInputStream()); + closeSilently(channel); } } diff --git a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java index 320b0289f5..19992b89aa 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java @@ -24,7 +24,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.*; -import java.util.concurrent.ExecutionException; import static java.nio.charset.StandardCharsets.UTF_8; import static org.asynchttpclient.Dsl.asyncHttpClient; @@ -60,71 +59,30 @@ public void handle(String target, Request baseRequest, HttpServletRequest req, H }; } - @Test(expectedExceptions = ExecutionException.class) - public void testPutImageFileThrowsExecutionException() throws Exception { - // Should throw ExecutionException when zero-copy is enabled - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { - InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); - client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), LARGE_IMAGE_FILE.length(), "application/octet-stream", UTF_8)).execute().get(); - } - } - - @Test(expectedExceptions = ExecutionException.class) - public void testPutImageFileUnknownSizeThrowsExecutionException() throws Exception { - // Should throw ExecutionException when zero-copy is enabled - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { - InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); - client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); - } - } - @Test public void testPutImageFile() throws Exception { - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000).setDisableZeroCopy(true))) { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); - client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), LARGE_IMAGE_FILE.length(), "application/octet-stream", UTF_8)).execute().get(); + Response response = client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), LARGE_IMAGE_FILE.length(), "application/octet-stream", UTF_8)).execute().get(); + assertEquals(response.getStatusCode(), 200); } } @Test public void testPutImageFileUnknownSize() throws Exception { - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000).setDisableZeroCopy(true))) { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); Response response = client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); assertEquals(response.getStatusCode(), 200); } } - @Test(expectedExceptions = ExecutionException.class) - public void testPutLargeTextFileThrowsExecutionException() throws Exception { - File file = createTempFile(1024 * 1024); - InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); - - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { - Response response = client.preparePut(getTargetUrl()) - .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), file.length(), "application/octet-stream", UTF_8)).execute().get(); - assertEquals(response.getStatusCode(), 200); - } - } - - @Test(expectedExceptions = ExecutionException.class) - public void testPutLargeTextFileUnknownSizeThrowsExecutionException() throws Exception { - File file = createTempFile(1024 * 1024); - InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); - - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { - Response response = client.preparePut(getTargetUrl()) - .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); - assertEquals(response.getStatusCode(), 200); - } - } - @Test public void testPutLargeTextFile() throws Exception { File file = createTempFile(1024 * 1024); InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000).setDisableZeroCopy(true))) { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { Response response = client.preparePut(getTargetUrl()) .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), file.length(), "application/octet-stream", UTF_8)).execute().get(); assertEquals(response.getStatusCode(), 200); @@ -136,7 +94,7 @@ public void testPutLargeTextFileUnknownSize() throws Exception { File file = createTempFile(1024 * 1024); InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); - try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000).setDisableZeroCopy(true))) { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { Response response = client.preparePut(getTargetUrl()) .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); assertEquals(response.getStatusCode(), 200); diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java index 382f2d208e..fc54d396ac 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java @@ -36,7 +36,6 @@ public class MultipartBodyTest { private static final List PARTS = new ArrayList<>(); private static long MAX_MULTIPART_CONTENT_LENGTH_ESTIMATE; - private static long MAX_MULTIPART_CONTENT_LENGTH_WITH_INPUT_STREAM_PART_ESTIMATE; static { try { @@ -55,13 +54,6 @@ public class MultipartBodyTest { } } - static { - try (MultipartBody dummyBody = buildMultipartWithInputStreamPart()) { - // separator is random - MAX_MULTIPART_CONTENT_LENGTH_WITH_INPUT_STREAM_PART_ESTIMATE = dummyBody.getContentLength() + 100; - } - } - private static File getTestfile() throws URISyntaxException { final ClassLoader cl = MultipartBodyTest.class.getClassLoader(); final URL url = cl.getResource("textfile.txt"); @@ -70,10 +62,6 @@ private static File getTestfile() throws URISyntaxException { } private static MultipartBody buildMultipart() { - return MultipartUtils.newMultipartBody(PARTS, EmptyHttpHeaders.INSTANCE); - } - - private static MultipartBody buildMultipartWithInputStreamPart() { List parts = new ArrayList<>(PARTS); try { File testFile = getTestfile(); @@ -140,16 +128,6 @@ public void transferWithCopy() throws Exception { } } - @Test - public void transferWithCopyAndInputStreamPart() throws Exception { - for (int bufferLength = 1; bufferLength < MAX_MULTIPART_CONTENT_LENGTH_WITH_INPUT_STREAM_PART_ESTIMATE + 1; bufferLength++) { - try (MultipartBody multipartBody = buildMultipartWithInputStreamPart()) { - long transferred = transferWithCopy(multipartBody, bufferLength); - assertEquals(transferred, multipartBody.getContentLength()); - } - } - } - @Test public void transferZeroCopy() throws Exception { for (int bufferLength = 1; bufferLength < MAX_MULTIPART_CONTENT_LENGTH_ESTIMATE + 1; bufferLength++) { @@ -159,13 +137,4 @@ public void transferZeroCopy() throws Exception { } } } - - @Test(expectedExceptions = UnsupportedOperationException.class) - public void transferZeroCopyWithInputStreamPart() throws Exception { - for (int bufferLength = 1; bufferLength < MAX_MULTIPART_CONTENT_LENGTH_WITH_INPUT_STREAM_PART_ESTIMATE + 1; bufferLength++) { - try (MultipartBody multipartBody = buildMultipartWithInputStreamPart()) { - transferZeroCopy(multipartBody, bufferLength); - } - } - } } diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java index f8729b294a..879a40a9d7 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java @@ -41,7 +41,6 @@ import java.util.Arrays; import java.util.List; import java.util.UUID; -import java.util.concurrent.ExecutionException; import java.util.zip.GZIPInputStream; import static java.nio.charset.StandardCharsets.UTF_8; @@ -86,16 +85,25 @@ public void testSendingSmallFilesAndByteArray() throws Exception { testFiles.add(testResource1File); testFiles.add(testResource2File); testFiles.add(testResource3File); + testFiles.add(testResource3File); + testFiles.add(testResource2File); + testFiles.add(testResource1File); List expected = new ArrayList<>(); expected.add(expectedContents); expected.add(expectedContents2); expected.add(expectedContents3); + expected.add(expectedContents3); + expected.add(expectedContents2); + expected.add(expectedContents); List gzipped = new ArrayList<>(); gzipped.add(false); gzipped.add(true); gzipped.add(false); + gzipped.add(false); + gzipped.add(true); + gzipped.add(false); File tmpFile = File.createTempFile("textbytearray", ".txt"); try (OutputStream os = Files.newOutputStream(tmpFile.toPath())) { @@ -113,41 +121,10 @@ public void testSendingSmallFilesAndByteArray() throws Exception { .addBodyPart(new StringPart("Name", "Dominic")) .addBodyPart(new FilePart("file3", testResource3File, "text/plain", UTF_8)) .addBodyPart(new StringPart("Age", "3")).addBodyPart(new StringPart("Height", "shrimplike")) - .addBodyPart(new StringPart("Hair", "ridiculous")).addBodyPart(new ByteArrayPart("file4", - expectedContents.getBytes(UTF_8), "text/plain", UTF_8, "bytearray.txt")) - .build(); - - Response res = c.executeRequest(r).get(); - - assertEquals(res.getStatusCode(), 200); - - testSentFile(expected, testFiles, res, gzipped); - } - - testFiles.add(testResource3File); - testFiles.add(testResource2File); - testFiles.add(testResource1File); - - expected.add(expectedContents3); - expected.add(expectedContents2); - expected.add(expectedContents); - - gzipped.add(false); - gzipped.add(true); - gzipped.add(false); - - // Zero-copy should be disabled when using InputStreamPart - try (AsyncHttpClient c = asyncHttpClient(config().setDisableZeroCopy(true))) { - Request r = post("http://localhost" + ":" + port1 + "/upload") - .addBodyPart(new FilePart("file1", testResource1File, "text/plain", UTF_8)) - .addBodyPart(new FilePart("file2", testResource2File, "application/x-gzip", null)) - .addBodyPart(new StringPart("Name", "Dominic")) - .addBodyPart(new FilePart("file3", testResource3File, "text/plain", UTF_8)) - .addBodyPart(new StringPart("Age", "3")).addBodyPart(new StringPart("Height", "shrimplike")) - .addBodyPart(new ByteArrayPart("file4", expectedContents.getBytes(UTF_8), "text/plain", UTF_8, "bytearray.txt")) .addBodyPart(new InputStreamPart("inputStream3", inputStreamFile3, testResource3File.getName(), testResource3File.length(), "text/plain", UTF_8)) .addBodyPart(new InputStreamPart("inputStream2", inputStreamFile2, testResource2File.getName(), testResource2File.length(), "application/x-gzip", null)) - .addBodyPart(new StringPart("Hair", "ridiculous")) + .addBodyPart(new StringPart("Hair", "ridiculous")).addBodyPart(new ByteArrayPart("file4", + expectedContents.getBytes(UTF_8), "text/plain", UTF_8, "bytearray.txt")) .addBodyPart(new InputStreamPart("inputStream1", inputStreamFile1, testResource1File.getName(), testResource1File.length(), "text/plain", UTF_8)) .build(); @@ -197,7 +174,7 @@ public void testSendEmptyFileInputStream() throws Exception { sendEmptyFileInputStream(true); } - @Test(expectedExceptions = ExecutionException.class) + @Test public void testSendEmptyFileInputStreamZeroCopy() throws Exception { sendEmptyFileInputStream(false); } @@ -224,7 +201,7 @@ public void testSendFileInputStreamUnknownContentLength() throws Exception { sendFileInputStream(false, true); } - @Test(expectedExceptions = ExecutionException.class) + @Test public void testSendFileInputStreamZeroCopyUnknownContentLength() throws Exception { sendFileInputStream(false, false); } @@ -234,7 +211,7 @@ public void testSendFileInputStreamKnownContentLength() throws Exception { sendFileInputStream(true, true); } - @Test(expectedExceptions = ExecutionException.class) + @Test public void testSendFileInputStreamZeroCopyKnownContentLength() throws Exception { sendFileInputStream(true, false); } From ec1f32ff2ea8d354d5a373bff326320444f073ef Mon Sep 17 00:00:00 2001 From: Samridh Srinath Date: Sat, 27 Oct 2018 09:41:27 -0700 Subject: [PATCH 8/9] Use BodyChunkedInput in contentLength < 0 in NettyBodyBody --- .../netty/request/body/NettyBodyBody.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java index abfb6aba78..1a7d50b3fd 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java @@ -53,14 +53,8 @@ public long getContentLength() { public void write(final Channel channel, NettyResponseFuture future) { Object msg; - if (body instanceof RandomAccessBody && !ChannelManager.isSslHandlerConfigured(channel.pipeline()) && !config.isDisableZeroCopy()) { - long contentLength = getContentLength(); - if (contentLength < 0) { - // contentLength unknown in advance, use chunked input - msg = new BodyChunkedInput(body); - } else { - msg = new BodyFileRegion((RandomAccessBody) body); - } + if (body instanceof RandomAccessBody && !ChannelManager.isSslHandlerConfigured(channel.pipeline()) && !config.isDisableZeroCopy() && getContentLength() > 0) { + msg = new BodyFileRegion((RandomAccessBody) body); } else { msg = new BodyChunkedInput(body); From 28b3cf8f5bfce10285e24d9f71b8efaed9108ae5 Mon Sep 17 00:00:00 2001 From: Samridh Srinath Date: Sat, 27 Oct 2018 10:28:00 -0700 Subject: [PATCH 9/9] Add license header to new files --- .../request/body/multipart/InputStreamPart.java | 13 +++++++++++++ .../multipart/part/InputStreamMultipartPart.java | 13 +++++++++++++ .../request/body/InputStreamPartLargeFileTest.java | 5 +++-- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java index 28c51cf119..ca7d0db367 100644 --- a/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2018 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ package org.asynchttpclient.request.body.multipart; import java.io.InputStream; diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java index 7340dc6e71..1c2ca251d3 100644 --- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/InputStreamMultipartPart.java @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2018 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ package org.asynchttpclient.request.body.multipart.part; import io.netty.buffer.ByteBuf; diff --git a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java index 19992b89aa..48d45341b5 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java @@ -1,9 +1,10 @@ /* - * Copyright (c) 2010-2012 Sonatype, Inc. All rights reserved. + * Copyright (c) 2018 AsyncHttpClient Project. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. * * Unless required by applicable law or agreed to in writing, * software distributed under the Apache License Version 2.0 is distributed on an