TestMultipartEntityBuilder.java
/*
* ====================================================================
* 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
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.entity.mime;
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;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HeaderElement;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.message.BasicHeaderValueParser;
import org.apache.hc.core5.http.message.BasicNameValuePair;
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();
Assertions.assertNotNull(entity);
Assertions.assertTrue(entity.getMultipart() instanceof HttpStrictMultipart);
Assertions.assertEquals(0, entity.getMultipart().getParts().size());
}
@Test
void testMultipartOptions() {
final MultipartFormEntity entity = MultipartEntityBuilder.create()
.setBoundary("blah-blah")
.setCharset(StandardCharsets.UTF_8)
.setLaxMode()
.buildEntity();
Assertions.assertNotNull(entity);
Assertions.assertTrue(entity.getMultipart() instanceof LegacyMultipart);
Assertions.assertEquals("blah-blah", entity.getMultipart().boundary);
Assertions.assertEquals(StandardCharsets.UTF_8, entity.getMultipart().charset);
}
@Test
void testAddBodyPartsFile() {
final MultipartFormEntity entity = MultipartEntityBuilder.create()
.addTextBody("p1", "stuff")
.addBinaryBody("p2", new File("stuff"))
.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<MultipartPart> bodyParts = entity.getMultipart().getParts();
Assertions.assertNotNull(bodyParts);
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<MultipartPart> bodyParts = entity.getMultipart().getParts();
Assertions.assertNotNull(bodyParts);
Assertions.assertEquals(5, bodyParts.size());
}
@Test
void testMultipartCustomContentType() {
final MultipartFormEntity entity = MultipartEntityBuilder.create()
.setContentType(ContentType.APPLICATION_XML)
.setBoundary("blah-blah")
.setCharset(StandardCharsets.UTF_8)
.setLaxMode()
.buildEntity();
Assertions.assertNotNull(entity);
Assertions.assertEquals("application/xml; charset=UTF-8; boundary=blah-blah", entity.getContentType());
}
@Test
void testMultipartContentTypeParameter() {
final MultipartFormEntity entity = MultipartEntityBuilder.create()
.setContentType(ContentType.MULTIPART_FORM_DATA.withParameters(
new BasicNameValuePair("boundary", "yada-yada"),
new BasicNameValuePair("charset", "ascii")))
.buildEntity();
Assertions.assertNotNull(entity);
Assertions.assertEquals("multipart/form-data; boundary=yada-yada; charset=ascii", entity.getContentType());
Assertions.assertEquals("yada-yada", entity.getMultipart().boundary);
Assertions.assertEquals(StandardCharsets.US_ASCII, entity.getMultipart().charset);
}
@Test
void testMultipartDefaultContentTypeOmitsCharset() {
final MultipartFormEntity entity = MultipartEntityBuilder.create()
.setCharset(StandardCharsets.UTF_8)
.setBoundary("yada-yada")
.buildEntity();
Assertions.assertNotNull(entity);
Assertions.assertEquals("multipart/mixed; boundary=yada-yada", entity.getContentType());
Assertions.assertEquals("yada-yada", entity.getMultipart().boundary);
}
@Test
void testMultipartFormDataContentTypeOmitsCharset() {
// Note: org.apache.hc.core5.http.ContentType.MULTIPART_FORM_DATA uses StandardCharsets.ISO_8859_1,
// so we create a custom ContentType here
final MultipartFormEntity entity = MultipartEntityBuilder.create()
.setContentType(ContentType.create("multipart/form-data"))
.setCharset(StandardCharsets.UTF_8)
.setBoundary("yada-yada")
.buildEntity();
Assertions.assertNotNull(entity);
Assertions.assertEquals("multipart/form-data; boundary=yada-yada", entity.getContentType());
Assertions.assertEquals("yada-yada", entity.getMultipart().boundary);
}
@Test
void testMultipartCustomContentTypeParameterOverrides() {
final MultipartFormEntity entity = MultipartEntityBuilder.create()
.setContentType(ContentType.MULTIPART_FORM_DATA.withParameters(
new BasicNameValuePair("boundary", "yada-yada"),
new BasicNameValuePair("charset", "ascii"),
new BasicNameValuePair("my", "stuff")))
.setBoundary("blah-blah")
.setCharset(StandardCharsets.UTF_8)
.setLaxMode()
.buildEntity();
Assertions.assertNotNull(entity);
Assertions.assertEquals("multipart/form-data; boundary=blah-blah; charset=ascii; my=stuff",
entity.getContentType());
}
@Test
void testMultipartCustomContentTypeUsingAddParameter() {
final MultipartEntityBuilder eb = MultipartEntityBuilder.create();
eb.setMimeSubtype("related");
eb.addParameter(new BasicNameValuePair("boundary", "yada-yada"));
eb.addParameter(new BasicNameValuePair("charset", "ascii"));
eb.addParameter(new BasicNameValuePair("my", "stuff"));
eb.buildEntity();
final MultipartFormEntity entity = eb.buildEntity();
Assertions.assertNotNull(entity);
Assertions.assertEquals("multipart/related; boundary=yada-yada; charset=ascii; my=stuff",
entity.getContentType());
}
@Test
void testMultipartWriteTo() throws Exception {
final String helloWorld = "hello world";
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()
.setStrictMode()
.setBoundary("xxxxxxxxxxxxxxxxxxxxxxxx")
.addPart(new FormBodyPartBuilder()
.setName("test")
.setBody(new StringBody("hello world", ContentType.TEXT_PLAIN))
.addField("Content-Disposition", "multipart/form-data", parameters)
.build())
.buildEntity();
final ByteArrayOutputStream out = new ByteArrayOutputStream();
entity.writeTo(out);
out.close();
Assertions.assertEquals("--xxxxxxxxxxxxxxxxxxxxxxxx\r\n" +
"Content-Disposition: multipart/form-data; name=\"test\"; filename=\"hello world\"\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"\r\n" +
helloWorld + "\r\n" +
"--xxxxxxxxxxxxxxxxxxxxxxxx--\r\n", out.toString(StandardCharsets.US_ASCII.name()));
}
@Test
void testMultipartWriteToRFC7578Mode() 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)
.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())
.buildEntity();
final ByteArrayOutputStream out = new ByteArrayOutputStream();
entity.writeTo(out);
out.close();
Assertions.assertEquals("--xxxxxxxxxxxxxxxxxxxxxxxx\r\n" +
"Content-Disposition: multipart/form-data; name=\"test\"; filename=\"hello%20%CE%BA%CF%8C%CF%83%CE%BC%CE%B5!%25\"\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", out.toString(StandardCharsets.ISO_8859_1.name()));
}
@Test
void testMultipartWriteToRFC6532Mode() 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())
.buildEntity();
final ByteArrayOutputStream out = new ByteArrayOutputStream();
entity.writeTo(out);
out.close();
Assertions.assertEquals("--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", out.toString(StandardCharsets.ISO_8859_1.name()));
}
@Test
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()));
}
@Test
void testMultipartWriteToRFC7578ModeWithFilenameStar() 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_START, helloWorld));
final MultipartFormEntity entity = MultipartEntityBuilder.create()
.setMode(HttpMultipartMode.EXTENDED)
.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())
.buildEntity();
final ByteArrayOutputStream out = new ByteArrayOutputStream();
entity.writeTo(out);
out.close();
Assertions.assertEquals("--xxxxxxxxxxxxxxxxxxxxxxxx\r\n" +
"Content-Disposition: multipart/form-data; name=\"test\"; filename*=\"UTF-8''hello%20%CE%BA%CF%8C%CF%83%CE%BC%CE%B5!%25\"\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", out.toString(StandardCharsets.ISO_8859_1.name()));
}
@Test
void testRandomBoundary() {
final MultipartFormEntity entity = MultipartEntityBuilder.create()
.buildEntity();
final NameValuePair boundaryParam = extractBoundary(entity.getContentType(), "multipart/mixed");
final String boundary = boundaryParam.getValue();
Assertions.assertNotNull(boundary);
Assertions.assertEquals(56, boundary.length());
Assertions.assertTrue(boundary.startsWith("httpclient_boundary_"));
Assertions.assertTrue(boundary.substring(20).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"));
}
@Test
void testRandomBoundaryWriteTo() throws Exception {
final String helloWorld = "hello world";
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()
.setStrictMode()
.addPart(new FormBodyPartBuilder()
.setName("test")
.setBody(new StringBody("hello world", ContentType.TEXT_PLAIN))
.addField("Content-Disposition", "multipart/form-data", parameters)
.build())
.buildEntity();
final NameValuePair boundaryParam = extractBoundary(entity.getContentType(), "multipart/form-data");
final String boundary = boundaryParam.getValue();
Assertions.assertNotNull(boundary);
Assertions.assertEquals(56, boundary.length());
Assertions.assertTrue(boundary.startsWith("httpclient_boundary_"));
Assertions.assertTrue(boundary.substring(20).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"));
final ByteArrayOutputStream out = new ByteArrayOutputStream();
entity.writeTo(out);
out.close();
Assertions.assertEquals("--" + boundary + "\r\n" +
"Content-Disposition: multipart/form-data; name=\"test\"; filename=\"hello world\"\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"\r\n" +
helloWorld + "\r\n" +
"--" + boundary + "--\r\n", out.toString(StandardCharsets.US_ASCII.name()));
}
private NameValuePair extractBoundary(final String contentType, final String expectedName) {
final BasicHeaderValueParser parser = BasicHeaderValueParser.INSTANCE;
final ParserCursor cursor = new ParserCursor(0, contentType.length());
final HeaderElement elem = parser.parseHeaderElement(contentType, cursor);
Assertions.assertEquals(expectedName, elem.getName());
return elem.getParameterByName("boundary");
}
}