From 17da6d24ca54d55496728f1e89ae3de4da976243 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Fri, 17 Mar 2023 23:51:53 +0100 Subject: [PATCH] Add support for preamble and epilogue in multipart entities Previously, multipart entities did not support adding a preamble or epilogue to the message. This commit adds support for these features by modifying the AbstractMultipartFormat class to accept preamble and epilogue strings in its constructor. The HttpRFC6532Multipart, HttpRFC7578Multipart, and HttpStrictMultipart classes are updated to pass these parameters to the parent constructor when creating instances of multipart entities. This change allows users to include custom content at the beginning and end of their multipart messages, which can be useful in certain scenarios such as adding metadata or information about the message contents. --- .../entity/mime/AbstractMultipartFormat.java | 70 ++++++++++++++++--- .../entity/mime/HttpRFC6532Multipart.java | 29 +++++++- .../entity/mime/HttpRFC7578Multipart.java | 29 +++++++- .../http/entity/mime/HttpStrictMultipart.java | 28 +++++++- .../http/entity/mime/LegacyMultipart.java | 2 +- .../entity/mime/MultipartEntityBuilder.java | 49 +++++++++++-- .../mime/TestMultipartEntityBuilder.java | 34 +++++++++ .../http/entity/mime/TestMultipartForm.java | 41 +++++++++++ 8 files changed, 262 insertions(+), 20 deletions(-) diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartFormat.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartFormat.java index 3f3c2c8e0..3f106b751 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartFormat.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartFormat.java @@ -46,6 +46,16 @@ import org.apache.hc.core5.util.ByteArrayBuffer; */ abstract class AbstractMultipartFormat { + /** + * The preamble to be included before the multipart content. + */ + private String preamble; + + /** + * The epilogue to be included after the multipart content. + */ + private String epilogue; + static ByteArrayBuffer encode( final Charset charset, final CharSequence string) { final ByteBuffer encoded = charset.encode(CharBuffer.wrap(string)); @@ -130,7 +140,7 @@ abstract class AbstractMultipartFormat { /** * Creates an instance with the specified settings. * - * @param charset the character set to use. May be {@code null}, in which case {@link StandardCharsets#ISO_8859_1} is used. + * @param charset the character set to use. May be {@code null}, in which case {@link StandardCharsets#ISO_8859_1} is used. * @param boundary to use - must not be {@code null} * @throws IllegalArgumentException if charset is null or boundary is null */ @@ -141,18 +151,54 @@ abstract class AbstractMultipartFormat { this.boundary = boundary; } + /* */ + + /** + * Constructs a new instance of {@code AbstractMultipartFormat} with the given charset, boundary, preamble, and epilogue. + * + * @param charset the charset to use. + * @param boundary the boundary string to use. + * @param preamble the preamble string to use. Can be {@code null}. + * @param epilogue the epilogue string to use. Can be {@code null}. + * @throws IllegalArgumentException if the boundary string is {@code null}. + */ + public AbstractMultipartFormat(final Charset charset, final String boundary, final String preamble, final String epilogue) { + super(); + Args.notNull(boundary, "Multipart boundary"); + this.charset = charset != null ? charset : StandardCharsets.ISO_8859_1; + this.boundary = boundary; + this.preamble = preamble; + this.epilogue = epilogue; + } + public AbstractMultipartFormat(final String boundary) { this(null, boundary); } public abstract List getParts(); + /** + * Writes the multipart message to the specified output stream. + *

+ * If {@code writeContent} is {@code true}, the content of each part will also be written. + * + *

If {@code preamble} is not {@code null}, it will be written before the first boundary. + * If {@code epilogue} is not {@code null}, it will be written after the last boundary. + * + * @param out the output stream to write the message to. + * @param writeContent whether to write the content of each part. + * @throws IOException if an I/O error occurs. + */ void doWriteTo( - final OutputStream out, - final boolean writeContent) throws IOException { + final OutputStream out, + final boolean writeContent) throws IOException { final ByteArrayBuffer boundaryEncoded = encode(this.charset, this.boundary); - for (final MultipartPart part: getParts()) { + if (this.preamble != null) { + writeBytes(this.preamble, out); + writeBytes(CR_LF, out); + } + for (final MultipartPart part : getParts()) { writeBytes(TWO_HYPHENS, out); writeBytes(boundaryEncoded, out); writeBytes(CR_LF, out); @@ -170,14 +216,18 @@ abstract class AbstractMultipartFormat { writeBytes(boundaryEncoded, out); writeBytes(TWO_HYPHENS, out); writeBytes(CR_LF, out); + if (this.epilogue != null) { + writeBytes(this.epilogue, out); + writeBytes(CR_LF, out); + } } /** - * Write the multipart header fields; depends on the style. - */ + * Write the multipart header fields; depends on the style. + */ protected abstract void formatMultipartHeader( - final MultipartPart part, - final OutputStream out) throws IOException; + final MultipartPart part, + final OutputStream out) throws IOException; /** * Writes out the content in the multipart/form encoding. This method @@ -200,11 +250,11 @@ abstract class AbstractMultipartFormat { *

* * @return total length of the multipart entity if known, {@code -1} - * otherwise. + * otherwise. */ public long getTotalLength() { long contentLen = 0; - for (final MultipartPart part: getParts()) { + for (final MultipartPart part : getParts()) { final ContentBody body = part.getBody(); final long len = body.getContentLength(); if (len >= 0) { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC6532Multipart.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC6532Multipart.java index 50cdf7e48..7bc626665 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC6532Multipart.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC6532Multipart.java @@ -37,12 +37,37 @@ class HttpRFC6532Multipart extends AbstractMultipartFormat { private final List parts; + /** + * Constructs a new instance of {@code HttpRFC6532Multipart}. + * + * @param charset The charset to use for the message. + * @param boundary The boundary string to use for the message. + * @param parts The list of parts that make up the message. + * @param preamble The preamble to include at the beginning of the message, or {@code null} if none. + * @param epilogue The epilogue to include at the end of the message, or {@code null} if none. + */ + public HttpRFC6532Multipart( + final Charset charset, + final String boundary, + final List parts, + final String preamble, + final String epilogue) { + super(charset, boundary, preamble, epilogue); + this.parts = parts; + } + + /** + * Constructs a new instance of {@code HttpRFC6532Multipart} with the given charset, boundary, and parts. + * + * @param charset the charset to use. + * @param boundary the boundary string to use. + * @param parts the list of parts to include in the multipart message. + */ public HttpRFC6532Multipart( final Charset charset, final String boundary, final List parts) { - super(charset, boundary); - this.parts = parts; + this(charset,boundary,parts,null, null); } @Override diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC7578Multipart.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC7578Multipart.java index 64407f08f..8df79a124 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC7578Multipart.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpRFC7578Multipart.java @@ -45,14 +45,39 @@ class HttpRFC7578Multipart extends AbstractMultipartFormat { private final List parts; + /** + * Constructs a new instance of {@code HttpRFC7578Multipart} with the given charset, boundary, parts, preamble, and epilogue. + * + * @param charset the charset to use. + * @param boundary the boundary string to use. + * @param parts the list of parts to include in the multipart message. + * @param preamble the optional preamble string to include before the first part. May be {@code null}. + * @param epilogue the optional epilogue string to include after the last part. May be {@code null}. + */ public HttpRFC7578Multipart( final Charset charset, final String boundary, - final List parts) { - super(charset, boundary); + final List parts, + final String preamble, + final String epilogue) { + super(charset, boundary, preamble, epilogue); this.parts = parts; } + /** + * Constructs a new instance of {@code HttpRFC7578Multipart} with the given charset, boundary, and parts. + * + * @param charset the charset to use. + * @param boundary the boundary string to use. + * @param parts the list of parts to include in the multipart message. + */ + public HttpRFC7578Multipart( + final Charset charset, + final String boundary, + final List parts) { + this(charset,boundary,parts,null, null); + } + @Override public List getParts() { return parts; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpStrictMultipart.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpStrictMultipart.java index a78cfbcd5..66236dbb5 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpStrictMultipart.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpStrictMultipart.java @@ -36,11 +36,37 @@ class HttpStrictMultipart extends AbstractMultipartFormat { private final List parts; + /** + * Constructs a new instance of {@code HttpStrictMultipart} with the given charset, boundary, and parts. + * + * @param charset the charset to use. + * @param boundary the boundary string to use. + * @param parts the list of parts to include in the multipart message. + */ public HttpStrictMultipart( final Charset charset, final String boundary, final List parts) { - super(charset, boundary); + this(charset,boundary,parts,null, null); + } + + + /** + * Constructs a new instance of {@code HttpStrictMultipart} with the given charset, boundary, parts, preamble, and epilogue. + * + * @param charset the charset to use. + * @param boundary the boundary string to use. + * @param parts the list of parts to include in the multipart message. + * @param preamble the preamble string to use. + * @param epilogue the epilogue string to use. + */ + public HttpStrictMultipart( + final Charset charset, + final String boundary, + final List parts, + final String preamble, + final String epilogue) { + super(charset, boundary, preamble, epilogue); this.parts = parts; } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/LegacyMultipart.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/LegacyMultipart.java index adda69de3..b1c4aa39d 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/LegacyMultipart.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/LegacyMultipart.java @@ -42,7 +42,7 @@ class LegacyMultipart extends AbstractMultipartFormat { private final List parts; - public LegacyMultipart( + public LegacyMultipart( final Charset charset, final String boundary, final List parts) { 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 aae926697..8234123ee 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 @@ -48,7 +48,7 @@ import org.apache.hc.core5.util.Args; * * @since 5.0 */ -public class MultipartEntityBuilder { + public class MultipartEntityBuilder { /** * The pool of ASCII chars to be used for generating a multipart boundary. @@ -63,6 +63,20 @@ public class MultipartEntityBuilder { private Charset charset; private List multipartParts; + /** + * The preamble of the multipart message. + * This field stores the optional preamble that should be added at the beginning of the multipart message. + * It can be {@code null} if no preamble is needed. + */ + private String preamble; + + /** + * The epilogue of the multipart message. + * This field stores the optional epilogue that should be added at the end of the multipart message. + * It can be {@code null} if no epilogue is needed. + */ + private String epilogue; + /** * An empty immutable {@code NameValuePair} array. */ @@ -189,6 +203,33 @@ public class MultipartEntityBuilder { return addBinaryBody(name, stream, ContentType.DEFAULT_BINARY, null); } + /** + * Adds a preamble to the multipart entity being constructed. The preamble is the text that appears before the first + * boundary delimiter. The preamble is optional and may be null. + * + * @param preamble The preamble text to add to the multipart entity + * @return This MultipartEntityBuilder instance, to allow for method chaining + * + * @since 5.3 + */ + public MultipartEntityBuilder addPreamble(final String preamble) { + this.preamble = preamble; + return this; + } + + /** + * Adds an epilogue to the multipart entity being constructed. The epilogue is the text that appears after the last + * boundary delimiter. The epilogue is optional and may be null. + * + * @param epilogue The epilogue text to add to the multipart entity + * @return This MultipartEntityBuilder instance, to allow for method chaining + * @since 5.3 + */ + public MultipartEntityBuilder addEpilogue(final String epilogue) { + this.epilogue = epilogue; + return this; + } + private String generateBoundary() { final ThreadLocalRandom rand = ThreadLocalRandom.current(); final int count = rand.nextInt(30, 41); // a random size from 30 to 40 @@ -252,13 +293,13 @@ public class MultipartEntityBuilder { if (charsetCopy == null) { charsetCopy = StandardCharsets.UTF_8; } - form = new HttpRFC7578Multipart(charsetCopy, boundaryCopy, multipartPartsCopy); + form = new HttpRFC7578Multipart(charsetCopy, boundaryCopy, multipartPartsCopy, preamble, epilogue); } else { - form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, multipartPartsCopy); + form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, multipartPartsCopy, preamble, epilogue); } break; default: - form = new HttpStrictMultipart(StandardCharsets.US_ASCII, boundaryCopy, multipartPartsCopy); + form = new HttpStrictMultipart(StandardCharsets.US_ASCII, boundaryCopy, multipartPartsCopy, preamble, epilogue); } return new MultipartFormEntity(form, contentTypeCopy, form.getTotalLength()); } 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 54eb17e07..babdf1420 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 @@ -220,4 +220,38 @@ public class TestMultipartEntityBuilder { "--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n", out.toString(StandardCharsets.ISO_8859_1.name())); } + @Test + public void testMultipartWriteToWithPreambleAndEpilogue() throws Exception { + final String helloWorld = "hello \u03BA\u03CC\u03C3\u03BC\u03B5!%"; + final List parameters = new ArrayList<>(); + parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_NAME, "test")); + parameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_FILENAME, helloWorld)); + + final MultipartFormEntity entity = MultipartEntityBuilder.create() + .setMode(HttpMultipartMode.EXTENDED) + .setContentType(ContentType.create("multipart/other")) + .setBoundary("xxxxxxxxxxxxxxxxxxxxxxxx") + .addPart(new FormBodyPartBuilder() + .setName("test") + .setBody(new StringBody(helloWorld, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8))) + .addField("Content-Disposition", "multipart/form-data", parameters) + .build()) + .addPreamble("This is the preamble.") + .addEpilogue("This is the epilogue.") + .buildEntity(); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + entity.writeTo(out); + out.close(); + Assertions.assertEquals("This is the preamble.\r\n" + + "--xxxxxxxxxxxxxxxxxxxxxxxx\r\n" + + "Content-Disposition: multipart/form-data; name=\"test\"; " + + "filename=\"hello \u00ce\u00ba\u00cf\u008c\u00cf\u0083\u00ce\u00bc\u00ce\u00b5!%\"\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n" + + "hello \u00ce\u00ba\u00cf\u008c\u00cf\u0083\u00ce\u00bc\u00ce\u00b5!%\r\n" + + "--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n" + + "This is the epilogue.\r\n", out.toString(StandardCharsets.ISO_8859_1.name())); + } + } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartForm.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartForm.java index c1f7d5737..d211f96f8 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartForm.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartForm.java @@ -373,4 +373,45 @@ public class TestMultipartForm { Assertions.assertEquals(expected.length, multipart.getTotalLength()); } + @Test + public void testMultipartFormBinaryPartsPreamblEpilogue() throws Exception { + tmpfile = File.createTempFile("tmp", ".bin"); + try (Writer writer = new FileWriter(tmpfile)) { + writer.append("some random whatever"); + } + + final FormBodyPart p1 = FormBodyPartBuilder.create( + "field1", + new FileBody(tmpfile)).build(); + @SuppressWarnings("resource") + final FormBodyPart p2 = FormBodyPartBuilder.create( + "field2", + new InputStreamBody(new FileInputStream(tmpfile), "file.tmp")).build(); + final HttpStrictMultipart multipart = new HttpStrictMultipart(null, "foo", + Arrays.asList(p1, p2), "This is the preamble", "This is the epilogue"); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + multipart.writeTo(out); + out.close(); + + final String expected = + "This is the preamble\r\n" + + "--foo\r\n" + + "Content-Disposition: form-data; name=\"field1\"; " + + "filename=\"" + tmpfile.getName() + "\"\r\n" + + "Content-Type: application/octet-stream\r\n" + + "\r\n" + + "some random whatever\r\n" + + "--foo\r\n" + + "Content-Disposition: form-data; name=\"field2\"; " + + "filename=\"file.tmp\"\r\n" + + "Content-Type: application/octet-stream\r\n" + + "\r\n" + + "some random whatever\r\n" + + "--foo--\r\n" + + "This is the epilogue\r\n"; + final String s = out.toString("US-ASCII"); + Assertions.assertEquals(expected, s); + Assertions.assertEquals(-1, multipart.getTotalLength()); + } }