From 0039b22eca0908039cc0e07edf2c26f7e73be659 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Wed, 4 Jun 2025 10:20:07 -0400 Subject: [PATCH] Add org.apache.hc.client5.http.entity.mime.PathBody - Add org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder.addBinaryBody(String, File) - Add org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder.addBinaryBody(String, Path, ContentType, String) --- RELEASE_NOTES.txt | 7 +- httpclient5/pom.xml | 5 + .../entity/mime/MultipartEntityBuilder.java | 27 ++++ .../hc/client5/http/entity/mime/PathBody.java | 128 ++++++++++++++++ .../http/entity/mime/FormBodyPartTest.java | 18 ++- .../mime/TestMultipartEntityBuilder.java | 23 ++- .../http/entity/mime/TestMultipartMixed.java | 42 +++++- .../http/entity/mime/TestPathBody.java | 142 ++++++++++++++++++ pom.xml | 5 + 9 files changed, 391 insertions(+), 6 deletions(-) create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/PathBody.java create mode 100644 httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestPathBody.java diff --git a/RELEASE_NOTES.txt b/RELEASE_NOTES.txt index ef49b3a9f0..ba58f5cb13 100644 --- a/RELEASE_NOTES.txt +++ b/RELEASE_NOTES.txt @@ -1,4 +1,4 @@ -Release 5.5.1 +Release 5.6-alpha1 ------------------ Change Log @@ -10,6 +10,11 @@ Change Log * Bump testcontainers.version from 1.20.6 to 1.21.1 #638. Contributed by Gary Gregory +* Add org.apache.hc.client5.http.entity.mime.PathBody + Add org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder.addBinaryBody(String, File) + Add org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder.addBinaryBody(String, Path, ContentType, String) + Contributed by Gary Gregory + Release 5.5 ------------------ diff --git a/httpclient5/pom.xml b/httpclient5/pom.xml index f7d1d26227..47777876b0 100644 --- a/httpclient5/pom.xml +++ b/httpclient5/pom.xml @@ -103,6 +103,11 @@ mockito-core test + + commons-io + commons-io + test + diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartEntityBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartEntityBuilder.java index de16e98c2e..bba4989f28 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartEntityBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartEntityBuilder.java @@ -31,6 +31,7 @@ import java.io.InputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -215,6 +216,32 @@ public MultipartEntityBuilder addBinaryBody( return addPart(name, new FileBody(file, contentType, filename)); } + /** + * Adds body with contents from the given source Path. + * + * @param name The part name. + * @param path The source path. + * @return {@code this} instance. + * @since 5.6 + */ + public MultipartEntityBuilder addBinaryBody(final String name, final Path path) { + return addBinaryBody(name, path, ContentType.DEFAULT_BINARY, path != null ? path.getFileName().toString() : null); + } + + /** + * Adds body with contents from the given source Path. + * + * @param name The part name. + * @param path The source path. + * @param contentType The content type. + * @param fileName The file name to override the Path's file name. + * @return {@code this} instance. + * @since 5.6 + */ + public MultipartEntityBuilder addBinaryBody(final String name, final Path path, final ContentType contentType, final String fileName) { + return addPart(name, new PathBody(path, contentType, fileName)); + } + public MultipartEntityBuilder addBinaryBody( final String name, final File file) { return addBinaryBody(name, file, ContentType.DEFAULT_BINARY, file != null ? file.getName() : null); diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/PathBody.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/PathBody.java new file mode 100644 index 0000000000..0140e227ed --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/PathBody.java @@ -0,0 +1,128 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.entity.mime; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.util.Args; + +/** + * Binary body part backed by an NIO {@link Path}. + * + * @see org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder + * + * @since 5.6 + */ +public class PathBody extends AbstractContentBody { + + private static String getFileName(final Path path) { + return path != null ? Objects.toString(path.getFileName(), null) : null; + } + + private final String fileName; + + private final Path path; + + /** + * Constructs a new instance for a given Path. + * + * @param path the source Path. + */ + public PathBody(final Path path) { + this(path, ContentType.DEFAULT_BINARY, getFileName(path)); + } + + /** + * Constructs a new instance for a given Path. + * + * @param path the source Path. + * @param contentType the content type. + */ + public PathBody(final Path path, final ContentType contentType) { + this(path, contentType, getFileName(path)); + } + + /** + * Constructs a new instance for a given Path. + * + * @param path the source Path. + * @param contentType the content type. + * @param fileName The file name to override the Path's file name. + */ + public PathBody(final Path path, final ContentType contentType, final String fileName) { + super(contentType); + this.path = Args.notNull(path, "path"); + this.fileName = fileName == null ? getFileName(path) : fileName; + } + + @Override + public long getContentLength() { + try { + return Files.size(path); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public String getFilename() { + return fileName; + } + + /** + * Gets a new input stream. + * + * @return a new input stream. + * @throws IOException if an I/O error occurs + */ + public InputStream getInputStream() throws IOException { + return Files.newInputStream(path); + } + + /** + * Gets the source Path. + * + * @return the source Path. + */ + public Path getPath() { + return path; + } + + @Override + public void writeTo(final OutputStream out) throws IOException { + Files.copy(path, Args.notNull(out, "Output stream")); + } + +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/FormBodyPartTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/FormBodyPartTest.java index c66f6e17db..b2c0ca42bf 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/FormBodyPartTest.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/FormBodyPartTest.java @@ -28,19 +28,31 @@ package org.apache.hc.client5.http.entity.mime; import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; import org.apache.hc.core5.http.ContentType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; class FormBodyPartTest { + @TempDir + Path tempDir; + @Test - void testConstructorCompat() throws Exception { - final File tmp = File.createTempFile("test", "test"); - tmp.deleteOnExit(); + void testFileConstructorCompat() throws Exception { + final File tmp = Files.createTempFile(tempDir, "test-", "-file.bin").toFile(); final FileBody obj = new FileBody(tmp, ContentType.APPLICATION_OCTET_STREAM); Assertions.assertEquals(tmp.getName(), obj.getFilename()); } + @Test + void testPathConstructorCompat() throws Exception { + final Path tmp = Files.createTempFile(tempDir, "test-", "-path.bin"); + final PathBody obj = new PathBody(tmp, ContentType.APPLICATION_OCTET_STREAM); + Assertions.assertEquals(tmp.getFileName().toString(), obj.getFilename()); + } + } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartEntityBuilder.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartEntityBuilder.java index 79e00e39e3..5fb2243b37 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartEntityBuilder.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartEntityBuilder.java @@ -30,7 +30,10 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -42,9 +45,13 @@ import org.apache.hc.core5.http.message.ParserCursor; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; class TestMultipartEntityBuilder { + @TempDir + Path tempDir; + @Test void testBasics() { final MultipartFormEntity entity = MultipartEntityBuilder.create().buildEntity(); @@ -67,7 +74,7 @@ void testMultipartOptions() { } @Test - void testAddBodyParts() { + void testAddBodyPartsFile() { final MultipartFormEntity entity = MultipartEntityBuilder.create() .addTextBody("p1", "stuff") .addBinaryBody("p2", new File("stuff")) @@ -81,6 +88,20 @@ void testAddBodyParts() { Assertions.assertEquals(5, bodyParts.size()); } + @Test + void testAddBodyPartsPath() throws IOException { + final MultipartFormEntity entity = MultipartEntityBuilder.create() + .addTextBody("p1", "stuff") + .addBinaryBody("p2", Files.createTempFile(tempDir, "test-", ".bin")) + .addBinaryBody("p3", new byte[]{}) + .addBinaryBody("p4", new ByteArrayInputStream(new byte[]{})) + .addBinaryBody("p5", new ByteArrayInputStream(new byte[]{}), ContentType.DEFAULT_BINARY, "filename") + .buildEntity(); + Assertions.assertNotNull(entity); + final List bodyParts = entity.getMultipart().getParts(); + Assertions.assertNotNull(bodyParts); + Assertions.assertEquals(5, bodyParts.size()); + } @Test void testMultipartCustomContentType() { diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartMixed.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartMixed.java index 6a45103ca7..f7e8f0fd10 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartMixed.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartMixed.java @@ -150,7 +150,7 @@ void testMultipartPartBinaryParts() throws Exception { } @Test - void testMultipartPartStrict() throws Exception { + void testMultipartPartFileStrict() throws Exception { tmpfile = File.createTempFile("tmp", ".bin"); try (Writer writer = new FileWriter(tmpfile)) { writer.append("some random whatever"); @@ -189,6 +189,46 @@ void testMultipartPartStrict() throws Exception { Assertions.assertEquals(-1, multipart.getTotalLength()); } + @Test + void testMultipartPartPathStrict() throws Exception { + tmpfile = File.createTempFile("tmp", ".bin"); + try (Writer writer = new FileWriter(tmpfile)) { + writer.append("some random whatever"); + } + + final MultipartPart p1 = MultipartPartBuilder.create( + new PathBody(tmpfile.toPath())).build(); + final MultipartPart p2 = MultipartPartBuilder.create( + new PathBody(tmpfile.toPath(), ContentType.create("text/plain", "ANSI_X3.4-1968"), "test-file")).build(); + @SuppressWarnings("resource") + final MultipartPart p3 = MultipartPartBuilder.create( + new InputStreamBody(new FileInputStream(tmpfile), "file.tmp")).build(); + final HttpStrictMultipart multipart = new HttpStrictMultipart(null, "foo", + Arrays.asList(p1, p2, p3)); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + multipart.writeTo(out); + out.close(); + + final String expected = + "--foo\r\n" + + "Content-Type: application/octet-stream\r\n" + + "\r\n" + + "some random whatever\r\n" + + "--foo\r\n" + + "Content-Type: text/plain; charset=US-ASCII\r\n" + + "\r\n" + + "some random whatever\r\n" + + "--foo\r\n" + + "Content-Type: application/octet-stream\r\n" + + "\r\n" + + "some random whatever\r\n" + + "--foo--\r\n"; + final String s = out.toString("US-ASCII"); + Assertions.assertEquals(expected, s); + Assertions.assertEquals(-1, multipart.getTotalLength()); + } + @Test void testMultipartPartRFC6532() throws Exception { tmpfile = File.createTempFile("tmp", ".bin"); diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestPathBody.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestPathBody.java new file mode 100644 index 0000000000..7c2abda15e --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestPathBody.java @@ -0,0 +1,142 @@ +/* + * ==================================================================== + * 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.client5.http.entity.mime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.commons.io.IOUtils; +import org.apache.hc.core5.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +/** + * Tests {@link PathBody}. + */ +class TestPathBody { + + private static final String DATA = "Hello World!"; + + private String fileName; + + private Path path; + + @TempDir + Path tempDir; + + @BeforeEach + void beforeEach() throws Exception { + path = Files.createTempFile(tempDir, "test-", "-path.bin"); + fileName = path.getFileName().toString(); + Files.write(path, DATA.getBytes(StandardCharsets.US_ASCII)); + } + + @Test + void testGetCharset() throws Exception { + assertNull(new PathBody(path).getCharset()); + } + + @Test + void testGetContentLength() throws Exception { + assertEquals(DATA.length(), new PathBody(path).getContentLength()); + } + + @Test + void testGetContentType() throws Exception { + assertEquals(ContentType.APPLICATION_OCTET_STREAM, new PathBody(path).getContentType()); + assertEquals(ContentType.APPLICATION_ATOM_XML, new PathBody(path, ContentType.APPLICATION_ATOM_XML).getContentType()); + assertEquals(ContentType.APPLICATION_ATOM_XML, new PathBody(path, ContentType.APPLICATION_ATOM_XML, "TheBin").getContentType()); + } + + @Test + void testGetFileName() throws Exception { + assertEquals(fileName, new PathBody(path).getFilename()); + assertEquals(fileName, new PathBody(path, ContentType.APPLICATION_ATOM_XML).getFilename()); + assertEquals("TheBin", new PathBody(path, ContentType.APPLICATION_ATOM_XML, "TheBin").getFilename()); + } + + @Test + void testGetInputStream() throws Exception { + try (InputStream inputStream = new PathBody(path).getInputStream()) { + assertEquals(DATA, IOUtils.toString(inputStream, StandardCharsets.US_ASCII)); + } + } + + @Test + void testGetMediaType() throws Exception { + assertEquals("application", new PathBody(path).getMediaType()); + assertEquals("multipart", new PathBody(path, ContentType.MULTIPART_FORM_DATA).getMediaType()); + assertEquals("multipart", new PathBody(path, ContentType.MULTIPART_FORM_DATA, "TheBin").getMediaType()); + } + + @Test + void testGetMimeType() throws Exception { + assertEquals("application/octet-stream", new PathBody(path).getMimeType()); + assertEquals("multipart/form-data", new PathBody(path, ContentType.MULTIPART_FORM_DATA).getMimeType()); + assertEquals("multipart/form-data", new PathBody(path, ContentType.MULTIPART_FORM_DATA, "TheBin").getMimeType()); + } + + @Test + void testGetPath() throws Exception { + assertEquals(path, new PathBody(path).getPath()); + assertEquals(path, new PathBody(path, ContentType.MULTIPART_FORM_DATA).getPath()); + assertEquals(path, new PathBody(path, ContentType.MULTIPART_FORM_DATA, "TheBin").getPath()); + } + + @Test + void testGetSubType() throws Exception { + assertEquals("octet-stream", new PathBody(path).getSubType()); + assertEquals("form-data", new PathBody(path, ContentType.MULTIPART_FORM_DATA).getSubType()); + assertEquals("form-data", new PathBody(path, ContentType.MULTIPART_FORM_DATA, "TheBin").getSubType()); + } + + @Test + void testPathConstructorPath() throws Exception { + final PathBody obj = new PathBody(path); + assertEquals(path.getFileName().toString(), obj.getFilename()); + } + + @Test + void testPathConstructorPathContentType() throws Exception { + final PathBody obj = new PathBody(path, ContentType.APPLICATION_OCTET_STREAM); + assertEquals(path.getFileName().toString(), obj.getFilename()); + } + + @Test + void testPathConstructorPathContentTypeString() throws Exception { + final PathBody obj = new PathBody(path, ContentType.APPLICATION_OCTET_STREAM, "TheBin"); + assertEquals("TheBin", obj.getFilename()); + } + +} diff --git a/pom.xml b/pom.xml index b4695f092f..785f50ca52 100644 --- a/pom.xml +++ b/pom.xml @@ -199,6 +199,11 @@ junit-jupiter ${testcontainers.version} + + commons-io + commons-io + 2.19.0 +