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.
This commit is contained in:
Arturo Bernal 2023-03-17 23:51:53 +01:00 committed by Oleg Kalnichevski
parent 0df9e63932
commit 17da6d24ca
8 changed files with 262 additions and 20 deletions

View File

@ -46,6 +46,16 @@ import org.apache.hc.core5.util.ByteArrayBuffer;
*/ */
abstract class AbstractMultipartFormat { 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( static ByteArrayBuffer encode(
final Charset charset, final CharSequence string) { final Charset charset, final CharSequence string) {
final ByteBuffer encoded = charset.encode(CharBuffer.wrap(string)); final ByteBuffer encoded = charset.encode(CharBuffer.wrap(string));
@ -141,18 +151,54 @@ abstract class AbstractMultipartFormat {
this.boundary = boundary; 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) { public AbstractMultipartFormat(final String boundary) {
this(null, boundary); this(null, boundary);
} }
public abstract List<MultipartPart> getParts(); public abstract List<MultipartPart> getParts();
/**
* Writes the multipart message to the specified output stream.
* <p>
* If {@code writeContent} is {@code true}, the content of each part will also be written.
*
* <p>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( void doWriteTo(
final OutputStream out, final OutputStream out,
final boolean writeContent) throws IOException { final boolean writeContent) throws IOException {
final ByteArrayBuffer boundaryEncoded = encode(this.charset, this.boundary); 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(TWO_HYPHENS, out);
writeBytes(boundaryEncoded, out); writeBytes(boundaryEncoded, out);
writeBytes(CR_LF, out); writeBytes(CR_LF, out);
@ -170,6 +216,10 @@ abstract class AbstractMultipartFormat {
writeBytes(boundaryEncoded, out); writeBytes(boundaryEncoded, out);
writeBytes(TWO_HYPHENS, out); writeBytes(TWO_HYPHENS, out);
writeBytes(CR_LF, out); writeBytes(CR_LF, out);
if (this.epilogue != null) {
writeBytes(this.epilogue, out);
writeBytes(CR_LF, out);
}
} }
/** /**
@ -204,7 +254,7 @@ abstract class AbstractMultipartFormat {
*/ */
public long getTotalLength() { public long getTotalLength() {
long contentLen = 0; long contentLen = 0;
for (final MultipartPart part: getParts()) { for (final MultipartPart part : getParts()) {
final ContentBody body = part.getBody(); final ContentBody body = part.getBody();
final long len = body.getContentLength(); final long len = body.getContentLength();
if (len >= 0) { if (len >= 0) {

View File

@ -37,12 +37,37 @@ class HttpRFC6532Multipart extends AbstractMultipartFormat {
private final List<MultipartPart> parts; private final List<MultipartPart> 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<MultipartPart> 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( public HttpRFC6532Multipart(
final Charset charset, final Charset charset,
final String boundary, final String boundary,
final List<MultipartPart> parts) { final List<MultipartPart> parts) {
super(charset, boundary); this(charset,boundary,parts,null, null);
this.parts = parts;
} }
@Override @Override

View File

@ -45,12 +45,37 @@ class HttpRFC7578Multipart extends AbstractMultipartFormat {
private final List<MultipartPart> parts; private final List<MultipartPart> 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<MultipartPart> 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( public HttpRFC7578Multipart(
final Charset charset, final Charset charset,
final String boundary, final String boundary,
final List<MultipartPart> parts) { final List<MultipartPart> parts) {
super(charset, boundary); this(charset,boundary,parts,null, null);
this.parts = parts;
} }
@Override @Override

View File

@ -36,11 +36,37 @@ class HttpStrictMultipart extends AbstractMultipartFormat {
private final List<MultipartPart> parts; private final List<MultipartPart> 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( public HttpStrictMultipart(
final Charset charset, final Charset charset,
final String boundary, final String boundary,
final List<MultipartPart> parts) { final List<MultipartPart> 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<MultipartPart> parts,
final String preamble,
final String epilogue) {
super(charset, boundary, preamble, epilogue);
this.parts = parts; this.parts = parts;
} }

View File

@ -48,7 +48,7 @@ import org.apache.hc.core5.util.Args;
* *
* @since 5.0 * @since 5.0
*/ */
public class MultipartEntityBuilder { public class MultipartEntityBuilder {
/** /**
* The pool of ASCII chars to be used for generating a multipart boundary. * The pool of ASCII chars to be used for generating a multipart boundary.
@ -63,6 +63,20 @@ public class MultipartEntityBuilder {
private Charset charset; private Charset charset;
private List<MultipartPart> multipartParts; private List<MultipartPart> 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. * An empty immutable {@code NameValuePair} array.
*/ */
@ -189,6 +203,33 @@ public class MultipartEntityBuilder {
return addBinaryBody(name, stream, ContentType.DEFAULT_BINARY, null); 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() { private String generateBoundary() {
final ThreadLocalRandom rand = ThreadLocalRandom.current(); final ThreadLocalRandom rand = ThreadLocalRandom.current();
final int count = rand.nextInt(30, 41); // a random size from 30 to 40 final int count = rand.nextInt(30, 41); // a random size from 30 to 40
@ -252,13 +293,13 @@ public class MultipartEntityBuilder {
if (charsetCopy == null) { if (charsetCopy == null) {
charsetCopy = StandardCharsets.UTF_8; charsetCopy = StandardCharsets.UTF_8;
} }
form = new HttpRFC7578Multipart(charsetCopy, boundaryCopy, multipartPartsCopy); form = new HttpRFC7578Multipart(charsetCopy, boundaryCopy, multipartPartsCopy, preamble, epilogue);
} else { } else {
form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, multipartPartsCopy); form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, multipartPartsCopy, preamble, epilogue);
} }
break; break;
default: 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()); return new MultipartFormEntity(form, contentTypeCopy, form.getTotalLength());
} }

View File

@ -220,4 +220,38 @@ public class TestMultipartEntityBuilder {
"--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n", out.toString(StandardCharsets.ISO_8859_1.name())); "--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<NameValuePair> 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()));
}
} }

View File

@ -373,4 +373,45 @@ public class TestMultipartForm {
Assertions.assertEquals(expected.length, multipart.getTotalLength()); 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());
}
} }