diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartForm.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartFormat.java similarity index 94% rename from httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartForm.java rename to httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartFormat.java index b36afc864..93befa639 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartForm.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/AbstractMultipartFormat.java @@ -46,7 +46,7 @@ import org.apache.hc.core5.util.ByteArrayBuffer; * * @since 4.3 */ -abstract class AbstractMultipartForm { +abstract class AbstractMultipartFormat { static ByteArrayBuffer encode( final Charset charset, final String string) { @@ -103,25 +103,25 @@ abstract class AbstractMultipartForm { * @param boundary to use - must not be {@code null} * @throws IllegalArgumentException if charset is null or boundary is null */ - public AbstractMultipartForm(final Charset charset, final String boundary) { + public AbstractMultipartFormat(final Charset charset, final String boundary) { super(); Args.notNull(boundary, "Multipart boundary"); this.charset = charset != null ? charset : StandardCharsets.ISO_8859_1; this.boundary = boundary; } - public AbstractMultipartForm(final String boundary) { + public AbstractMultipartFormat(final String boundary) { this(null, boundary); } - public abstract List getBodyParts(); + public abstract List getParts(); void doWriteTo( final OutputStream out, final boolean writeContent) throws IOException { final ByteArrayBuffer boundaryEncoded = encode(this.charset, this.boundary); - for (final FormBodyPart part: getBodyParts()) { + for (final MultipartPart part: getParts()) { writeBytes(TWO_DASHES, out); writeBytes(boundaryEncoded, out); writeBytes(CR_LF, out); @@ -145,7 +145,7 @@ abstract class AbstractMultipartForm { * Write the multipart header fields; depends on the style. */ protected abstract void formatMultipartHeader( - final FormBodyPart part, + final MultipartPart part, final OutputStream out) throws IOException; /** @@ -173,7 +173,7 @@ abstract class AbstractMultipartForm { */ public long getTotalLength() { long contentLen = 0; - for (final FormBodyPart part: getBodyParts()) { + 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/FormBodyPart.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/FormBodyPart.java index 4f585a7ad..9f3e9261b 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/FormBodyPart.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/FormBodyPart.java @@ -36,36 +36,24 @@ import org.apache.hc.core5.util.Args; * * @since 4.0 */ -public class FormBodyPart { +public class FormBodyPart extends MultipartPart { private final String name; - private final Header header; - private final ContentBody body; FormBodyPart(final String name, final ContentBody body, final Header header) { - super(); + super(body, header); Args.notNull(name, "Name"); Args.notNull(body, "Body"); this.name = name; - this.body = body; - this.header = header != null ? header : new Header(); } public String getName() { return this.name; } - public ContentBody getBody() { - return this.body; - } - - public Header getHeader() { - return this.header; - } - public void addField(final String name, final String value) { Args.notNull(name, "Field name"); - this.header.addField(new MinimalField(name, value)); + super.addField(name, value); } } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpBrowserCompatibleMultipart.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpBrowserCompatibleMultipart.java index b252ae5bc..f16d593b7 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpBrowserCompatibleMultipart.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/HttpBrowserCompatibleMultipart.java @@ -38,20 +38,20 @@ import java.util.List; * * @since 4.3 */ -class HttpBrowserCompatibleMultipart extends AbstractMultipartForm { +class HttpBrowserCompatibleMultipart extends AbstractMultipartFormat { - private final List parts; + private final List parts; public HttpBrowserCompatibleMultipart( final Charset charset, final String boundary, - final List parts) { + final List parts) { super(charset, boundary); this.parts = parts; } @Override - public List getBodyParts() { + public List getParts() { return this.parts; } @@ -60,13 +60,15 @@ class HttpBrowserCompatibleMultipart extends AbstractMultipartForm { */ @Override protected void formatMultipartHeader( - final FormBodyPart part, + final MultipartPart part, final OutputStream out) throws IOException { // For browser-compatible, only write Content-Disposition // Use content charset final Header header = part.getHeader(); final MinimalField cd = header.getField(MIME.CONTENT_DISPOSITION); - writeField(cd, this.charset, out); + if (cd != null) { + writeField(cd, this.charset, out); + } final String filename = part.getBody().getFilename(); if (filename != null) { final MinimalField ct = header.getField(MIME.CONTENT_TYPE); 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 8c139487b..40dc156ab 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 @@ -40,26 +40,26 @@ import java.util.List; * * @since 4.3 */ -class HttpRFC6532Multipart extends AbstractMultipartForm { +class HttpRFC6532Multipart extends AbstractMultipartFormat { - private final List parts; + private final List parts; public HttpRFC6532Multipart( final Charset charset, final String boundary, - final List parts) { + final List parts) { super(charset, boundary); this.parts = parts; } @Override - public List getBodyParts() { + public List getParts() { return this.parts; } @Override protected void formatMultipartHeader( - final FormBodyPart part, + final MultipartPart part, final OutputStream out) throws IOException { // For RFC6532, we output all fields with UTF-8 encoding. 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 e3437f5c8..8249575b9 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 @@ -40,27 +40,27 @@ import org.apache.commons.codec.DecoderException; import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.util.ByteArrayBuffer; -public class HttpRFC7578Multipart extends AbstractMultipartForm { +public class HttpRFC7578Multipart extends AbstractMultipartFormat { private static final PercentCodec PERCENT_CODEC = new PercentCodec(); - private final List parts; + private final List parts; public HttpRFC7578Multipart( final Charset charset, final String boundary, - final List parts) { + final List parts) { super(charset, boundary); this.parts = parts; } @Override - public List getBodyParts() { + public List getParts() { return parts; } @Override - protected void formatMultipartHeader(final FormBodyPart part, final OutputStream out) throws IOException { + protected void formatMultipartHeader(final MultipartPart part, final OutputStream out) throws IOException { for (final MinimalField field: part.getHeader()) { if (MIME.CONTENT_DISPOSITION.equalsIgnoreCase(field.getName())) { writeBytes(field.getName(), charset, out); 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 499d82e72..1c7a017f4 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 @@ -39,26 +39,26 @@ import java.util.List; * * @since 4.3 */ -class HttpStrictMultipart extends AbstractMultipartForm { +class HttpStrictMultipart extends AbstractMultipartFormat { - private final List parts; + private final List parts; public HttpStrictMultipart( final Charset charset, final String boundary, - final List parts) { + final List parts) { super(charset, boundary); this.parts = parts; } @Override - public List getBodyParts() { + public List getParts() { return this.parts; } @Override protected void formatMultipartHeader( - final FormBodyPart part, + final MultipartPart part, final OutputStream out) throws IOException { // For strict, we output all fields with MIME-standard encoding. 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 441b1c56f..86f24c98e 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 @@ -45,7 +45,7 @@ import org.apache.hc.core5.util.Args; /** * Builder for multipart {@link HttpEntity}s. * - * @since 4.3 + * @since 5.0 */ public class MultipartEntityBuilder { @@ -56,13 +56,14 @@ public class MultipartEntityBuilder { "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" .toCharArray(); - private final static String DEFAULT_SUBTYPE = "form-data"; + private final static String FORM_SUBTYPE = "form-data"; + private final static String MIXED_SUBTYPE = "mixed"; private ContentType contentType; private HttpMultipartMode mode = HttpMultipartMode.STRICT; private String boundary = null; private Charset charset = null; - private List bodyParts = null; + private List multipartParts = null; public static MultipartEntityBuilder create() { return new MultipartEntityBuilder(); @@ -117,14 +118,14 @@ public class MultipartEntityBuilder { /** * @since 4.4 */ - public MultipartEntityBuilder addPart(final FormBodyPart bodyPart) { - if (bodyPart == null) { + public MultipartEntityBuilder addPart(final MultipartPart multipartPart) { + if (multipartPart == null) { return this; } - if (this.bodyParts == null) { - this.bodyParts = new ArrayList<>(); + if (this.multipartParts == null) { + this.multipartParts = new ArrayList<>(); } - this.bodyParts.add(bodyPart); + this.multipartParts.add(multipartPart); return this; } @@ -202,28 +203,46 @@ public class MultipartEntityBuilder { paramsList.add(new BasicNameValuePair("charset", charsetCopy.name())); } final NameValuePair[] params = paramsList.toArray(new NameValuePair[paramsList.size()]); - final ContentType contentTypeCopy = contentType != null ? - contentType.withParameters(params) : - ContentType.create("multipart/" + DEFAULT_SUBTYPE, params); - final List bodyPartsCopy = bodyParts != null ? new ArrayList<>(bodyParts) : - Collections.emptyList(); + + final ContentType contentTypeCopy; + if (contentType != null) { + contentTypeCopy = contentType.withParameters(params); + } else { + boolean formData = false; + if (multipartParts != null) { + for (final MultipartPart multipartPart : multipartParts) { + if (multipartPart instanceof FormBodyPart) { + formData = true; + break; + } + } + } + + if (formData) { + contentTypeCopy = ContentType.create("multipart/" + FORM_SUBTYPE, params); + } else { + contentTypeCopy = ContentType.create("multipart/" + MIXED_SUBTYPE, params); + } + } + final List multipartPartsCopy = multipartParts != null ? new ArrayList<>(multipartParts) : + Collections.emptyList(); final HttpMultipartMode modeCopy = mode != null ? mode : HttpMultipartMode.STRICT; - final AbstractMultipartForm form; + final AbstractMultipartFormat form; switch (modeCopy) { case BROWSER_COMPATIBLE: - form = new HttpBrowserCompatibleMultipart(charsetCopy, boundaryCopy, bodyPartsCopy); + form = new HttpBrowserCompatibleMultipart(charsetCopy, boundaryCopy, multipartPartsCopy); break; case RFC6532: - form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, bodyPartsCopy); + form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, multipartPartsCopy); break; case RFC7578: if (charsetCopy == null) { charsetCopy = StandardCharsets.UTF_8; } - form = new HttpRFC7578Multipart(charsetCopy, boundaryCopy, bodyPartsCopy); + form = new HttpRFC7578Multipart(charsetCopy, boundaryCopy, multipartPartsCopy); break; default: - form = new HttpStrictMultipart(StandardCharsets.US_ASCII, boundaryCopy, bodyPartsCopy); + form = new HttpStrictMultipart(StandardCharsets.US_ASCII, boundaryCopy, multipartPartsCopy); } return new MultipartFormEntity(form, contentTypeCopy, form.getTotalLength()); } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartFormEntity.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartFormEntity.java index 9615fc5df..73f91415b 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartFormEntity.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartFormEntity.java @@ -43,12 +43,12 @@ import org.apache.hc.core5.http.HttpEntity; class MultipartFormEntity implements HttpEntity { - private final AbstractMultipartForm multipart; + private final AbstractMultipartFormat multipart; private final ContentType contentType; private final long contentLength; MultipartFormEntity( - final AbstractMultipartForm multipart, + final AbstractMultipartFormat multipart, final ContentType contentType, final long contentLength) { super(); @@ -57,7 +57,7 @@ class MultipartFormEntity implements HttpEntity { this.contentLength = contentLength; } - AbstractMultipartForm getMultipart() { + AbstractMultipartFormat getMultipart() { return this.multipart; } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartPart.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartPart.java new file mode 100644 index 000000000..05ebf773e --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartPart.java @@ -0,0 +1,63 @@ +/* + * ==================================================================== + * 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; + +/** + * MultipartPart class represents a content body that can be used as a part of multipart encoded + * entities. This class automatically populates the header with standard fields based on + * the content description of the enclosed body. + * + * @since 5.0 + */ +public class MultipartPart { + + private final Header header; + private final ContentBody body; + + MultipartPart(final ContentBody body, final Header header) { + super(); + this.body = body; + this.header = header != null ? header : new Header(); + } + + public ContentBody getBody() { + return this.body; + } + + public Header getHeader() { + return this.header; + } + + void addField(final String name, final String value) { + addField(new MinimalField(name, value)); + } + + void addField(final MinimalField field) { + this.header.addField(field); + } +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartPartBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartPartBuilder.java new file mode 100644 index 000000000..e5ef44848 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/entity/mime/MultipartPartBuilder.java @@ -0,0 +1,121 @@ +/* + * ==================================================================== + * 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.util.List; + +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.util.Args; +import org.apache.hc.core5.util.Asserts; + +/** + * Builder for individual {@link MultipartPart}s. + * + * @since 4.4 + */ +public class MultipartPartBuilder { + + private ContentBody body; + private final Header header; + + public static MultipartPartBuilder create(final ContentBody body) { + return new MultipartPartBuilder(body); + } + + public static MultipartPartBuilder create() { + return new MultipartPartBuilder(); + } + + MultipartPartBuilder(final ContentBody body) { + this(); + this.body = body; + } + + MultipartPartBuilder() { + this.header = new Header(); + } + + public MultipartPartBuilder setBody(final ContentBody body) { + this.body = body; + return this; + } + + public MultipartPartBuilder addHeader(final String name, final String value, final List parameters) { + Args.notNull(name, "Header name"); + this.header.addField(new MinimalField(name, value, parameters)); + return this; + } + + public MultipartPartBuilder addHeader(final String name, final String value) { + Args.notNull(name, "Header name"); + this.header.addField(new MinimalField(name, value)); + return this; + } + + public MultipartPartBuilder setHeader(final String name, final String value) { + Args.notNull(name, "Header name"); + this.header.setField(new MinimalField(name, value)); + return this; + } + + public MultipartPartBuilder removeHeaders(final String name) { + Args.notNull(name, "Header name"); + this.header.removeFields(name); + return this; + } + + public MultipartPart build() { + Asserts.notNull(this.body, "Content body"); + final Header headerCopy = new Header(); + final List fields = this.header.getFields(); + for (final MinimalField field: fields) { + headerCopy.addField(field); + } + if (headerCopy.getField(MIME.CONTENT_TYPE) == null) { + final ContentType contentType; + if (body instanceof AbstractContentBody) { + contentType = ((AbstractContentBody) body).getContentType(); + } else { + contentType = null; + } + if (contentType != null) { + headerCopy.addField(new MinimalField(MIME.CONTENT_TYPE, contentType.toString())); + } else { + final StringBuilder buffer = new StringBuilder(); + buffer.append(this.body.getMimeType()); // MimeType cannot be null + if (this.body.getCharset() != null) { // charset may legitimately be null + buffer.append("; charset="); + buffer.append(this.body.getCharset()); + } + headerCopy.addField(new MinimalField(MIME.CONTENT_TYPE, buffer.toString())); + } + } + return new MultipartPart(this.body, headerCopy); + } +} 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 16b12711b..937a89d93 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 @@ -47,7 +47,7 @@ public class TestMultipartEntityBuilder { final MultipartFormEntity entity = MultipartEntityBuilder.create().buildEntity(); Assert.assertNotNull(entity); Assert.assertTrue(entity.getMultipart() instanceof HttpStrictMultipart); - Assert.assertEquals(0, entity.getMultipart().getBodyParts().size()); + Assert.assertEquals(0, entity.getMultipart().getParts().size()); } @Test @@ -72,7 +72,7 @@ public class TestMultipartEntityBuilder { .addBinaryBody("p4", new ByteArrayInputStream(new byte[]{})) .buildEntity(); Assert.assertNotNull(entity); - final List bodyParts = entity.getMultipart().getBodyParts(); + final List bodyParts = entity.getMultipart().getParts(); Assert.assertNotNull(bodyParts); Assert.assertEquals(4, bodyParts.size()); } 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 90bf5caa8..98d6ee64d 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 @@ -65,7 +65,7 @@ public class TestMultipartForm { "field3", new StringBody("all kind of stuff", ContentType.DEFAULT_TEXT)).build(); final HttpStrictMultipart multipart = new HttpStrictMultipart(null, "foo", - Arrays.asList(p1, p2, p3)); + Arrays.asList(p1, p2, p3)); final ByteArrayOutputStream out = new ByteArrayOutputStream(); multipart.writeTo(out); @@ -102,7 +102,7 @@ public class TestMultipartForm { "field2", new StringBody("that stuff", ContentType.parse("stuff/plain; param=value"))).build(); final HttpStrictMultipart multipart = new HttpStrictMultipart(null, "foo", - Arrays.asList(p1, p2)); + Arrays.asList(p1, p2)); final ByteArrayOutputStream out = new ByteArrayOutputStream(); multipart.writeTo(out); @@ -140,7 +140,7 @@ public class TestMultipartForm { "field2", new InputStreamBody(new FileInputStream(tmpfile), "file.tmp")).build(); final HttpStrictMultipart multipart = new HttpStrictMultipart(null, "foo", - Arrays.asList(p1, p2)); + Arrays.asList(p1, p2)); final ByteArrayOutputStream out = new ByteArrayOutputStream(); multipart.writeTo(out); @@ -183,7 +183,7 @@ public class TestMultipartForm { "field3", new InputStreamBody(new FileInputStream(tmpfile), "file.tmp")).build(); final HttpStrictMultipart multipart = new HttpStrictMultipart(null, "foo", - Arrays.asList(p1, p2, p3)); + Arrays.asList(p1, p2, p3)); final ByteArrayOutputStream out = new ByteArrayOutputStream(); multipart.writeTo(out); @@ -232,7 +232,7 @@ public class TestMultipartForm { "field3", new InputStreamBody(new FileInputStream(tmpfile), "file.tmp")).build(); final HttpRFC6532Multipart multipart = new HttpRFC6532Multipart(null, "foo", - Arrays.asList(p1, p2, p3)); + Arrays.asList(p1, p2, p3)); final ByteArrayOutputStream out = new ByteArrayOutputStream(); multipart.writeTo(out); @@ -302,7 +302,7 @@ public class TestMultipartForm { new InputStreamBody(new FileInputStream(tmpfile), s2 + ".tmp")).build(); final HttpBrowserCompatibleMultipart multipart = new HttpBrowserCompatibleMultipart( StandardCharsets.UTF_8, "foo", - Arrays.asList(p1, p2)); + Arrays.asList(p1, p2)); final ByteArrayOutputStream out = new ByteArrayOutputStream(); multipart.writeTo(out); @@ -339,7 +339,7 @@ public class TestMultipartForm { "field2", new StringBody(s2, ContentType.create("text/plain", Charset.forName("KOI8-R")))).build(); final HttpStrictMultipart multipart = new HttpStrictMultipart(null, "foo", - Arrays.asList(p1, p2)); + Arrays.asList(p1, p2)); final ByteArrayOutputStream out1 = new ByteArrayOutputStream(); multipart.writeTo(out1); diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartFormHttpEntity.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartFormHttpEntity.java index 8838e59d0..76ea284b9 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartFormHttpEntity.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartFormHttpEntity.java @@ -54,7 +54,7 @@ public class TestMultipartFormHttpEntity { final String contentType = entity.getContentType(); final HeaderElement elem = BasicHeaderValueParser.INSTANCE.parseHeaderElement(contentType, new ParserCursor(0, contentType.length())); - Assert.assertEquals("multipart/form-data", elem.getName()); + Assert.assertEquals("multipart/mixed", elem.getName()); final NameValuePair p1 = elem.getParameterByName("boundary"); Assert.assertNotNull(p1); Assert.assertEquals("whatever", p1.getValue()); @@ -70,7 +70,7 @@ public class TestMultipartFormHttpEntity { final String contentType = entity.getContentType(); final HeaderElement elem = BasicHeaderValueParser.INSTANCE.parseHeaderElement(contentType, new ParserCursor(0, contentType.length())); - Assert.assertEquals("multipart/form-data", elem.getName()); + Assert.assertEquals("multipart/mixed", elem.getName()); final NameValuePair p1 = elem.getParameterByName("boundary"); Assert.assertNotNull(p1); 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 new file mode 100644 index 000000000..714d5ff93 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartMixed.java @@ -0,0 +1,332 @@ +/* + * ==================================================================== + * 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.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.apache.hc.core5.http.ContentType; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +public class TestMultipartMixed { + + private File tmpfile; + + @After + public void cleanup() { + if (tmpfile != null) { + tmpfile.delete(); + } + } + + @Test + public void testMultipartPartStringParts() throws Exception { + final MultipartPart p1 = MultipartPartBuilder.create( + new StringBody("this stuff", ContentType.DEFAULT_TEXT)).build(); + final MultipartPart p2 = MultipartPartBuilder.create( + new StringBody("that stuff", ContentType.create( + ContentType.TEXT_PLAIN.getMimeType(), StandardCharsets.UTF_8))).build(); + final MultipartPart p3 = MultipartPartBuilder.create( + new StringBody("all kind of stuff", ContentType.DEFAULT_TEXT)).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: text/plain; charset=ISO-8859-1\r\n" + + "\r\n" + + "this stuff\r\n" + + "--foo\r\n" + + "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n" + + "that stuff\r\n" + + "--foo\r\n" + + "Content-Type: text/plain; charset=ISO-8859-1\r\n" + + "\r\n" + + "all kind of stuff\r\n" + + "--foo--\r\n"; + final String s = out.toString("US-ASCII"); + Assert.assertEquals(expected, s); + Assert.assertEquals(s.length(), multipart.getTotalLength()); + } + + @Test + public void testMultipartPartCustomContentType() throws Exception { + final MultipartPart p1 = MultipartPartBuilder.create( + new StringBody("this stuff", ContentType.DEFAULT_TEXT)).build(); + final MultipartPart p2 = MultipartPartBuilder.create( + new StringBody("that stuff", ContentType.parse("stuff/plain; param=value"))).build(); + final HttpStrictMultipart multipart = new HttpStrictMultipart(null, "foo", + Arrays.asList(p1, p2)); + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + multipart.writeTo(out); + out.close(); + + final String expected = + "--foo\r\n" + + "Content-Type: text/plain; charset=ISO-8859-1\r\n" + + "\r\n" + + "this stuff\r\n" + + "--foo\r\n" + + "Content-Type: stuff/plain; param=value\r\n" + + "\r\n" + + "that stuff\r\n" + + "--foo--\r\n"; + final String s = out.toString("US-ASCII"); + Assert.assertEquals(expected, s); + Assert.assertEquals(s.length(), multipart.getTotalLength()); + } + + @Test + public void testMultipartPartBinaryParts() throws Exception { + tmpfile = File.createTempFile("tmp", ".bin"); + try (Writer writer = new FileWriter(tmpfile)) { + writer.append("some random whatever"); + } + + final MultipartPart p1 = MultipartPartBuilder.create( + new FileBody(tmpfile)).build(); + @SuppressWarnings("resource") + final MultipartPart p2 = MultipartPartBuilder.create( + new InputStreamBody(new FileInputStream(tmpfile), "file.tmp")).build(); + final HttpStrictMultipart multipart = new HttpStrictMultipart(null, "foo", + Arrays.asList(p1, p2)); + + 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: application/octet-stream\r\n" + + "\r\n" + + "some random whatever\r\n" + + "--foo--\r\n"; + final String s = out.toString("US-ASCII"); + Assert.assertEquals(expected, s); + Assert.assertEquals(-1, multipart.getTotalLength()); + } + + @Test + public void testMultipartPartStrict() throws Exception { + tmpfile = File.createTempFile("tmp", ".bin"); + try (Writer writer = new FileWriter(tmpfile)) { + writer.append("some random whatever"); + } + + final MultipartPart p1 = MultipartPartBuilder.create( + new FileBody(tmpfile)).build(); + final MultipartPart p2 = MultipartPartBuilder.create( + new FileBody(tmpfile, 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"); + Assert.assertEquals(expected, s); + Assert.assertEquals(-1, multipart.getTotalLength()); + } + + @Test + public void testMultipartPartRFC6532() throws Exception { + tmpfile = File.createTempFile("tmp", ".bin"); + try (Writer writer = new FileWriter(tmpfile)) { + writer.append("some random whatever"); + } + + final MultipartPart p1 = MultipartPartBuilder.create( + new FileBody(tmpfile)).build(); + final MultipartPart p2 = MultipartPartBuilder.create( + new FileBody(tmpfile, 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 HttpRFC6532Multipart multipart = new HttpRFC6532Multipart(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("UTF-8"); + Assert.assertEquals(expected, s); + Assert.assertEquals(-1, multipart.getTotalLength()); + } + + private static final int SWISS_GERMAN_HELLO [] = { + 0x47, 0x72, 0xFC, 0x65, 0x7A, 0x69, 0x5F, 0x7A, 0xE4, 0x6D, 0xE4 + }; + + private static final int RUSSIAN_HELLO [] = { + 0x412, 0x441, 0x435, 0x43C, 0x5F, 0x43F, 0x440, 0x438, + 0x432, 0x435, 0x442 + }; + + private static String constructString(final int [] unicodeChars) { + final StringBuilder buffer = new StringBuilder(); + if (unicodeChars != null) { + for (final int unicodeChar : unicodeChars) { + buffer.append((char)unicodeChar); + } + } + return buffer.toString(); + } + + @Test + public void testMultipartPartBrowserCompatibleNonASCIIHeaders() throws Exception { + final String s1 = constructString(SWISS_GERMAN_HELLO); + final String s2 = constructString(RUSSIAN_HELLO); + + tmpfile = File.createTempFile("tmp", ".bin"); + try (Writer writer = new FileWriter(tmpfile)) { + writer.append("some random whatever"); + } + + @SuppressWarnings("resource") + final MultipartPart p1 = MultipartPartBuilder.create( + new InputStreamBody(new FileInputStream(tmpfile), s1 + ".tmp")).build(); + @SuppressWarnings("resource") + final MultipartPart p2 = MultipartPartBuilder.create( + new InputStreamBody(new FileInputStream(tmpfile), s2 + ".tmp")).build(); + final HttpBrowserCompatibleMultipart multipart = new HttpBrowserCompatibleMultipart( + StandardCharsets.UTF_8, "foo", + Arrays.asList(p1, p2)); + + 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: application/octet-stream\r\n" + + "\r\n" + + "some random whatever\r\n" + + "--foo--\r\n"; + final String s = out.toString("UTF-8"); + Assert.assertEquals(expected, s); + Assert.assertEquals(-1, multipart.getTotalLength()); + } + + @Test + public void testMultipartPartStringPartsMultiCharsets() throws Exception { + final String s1 = constructString(SWISS_GERMAN_HELLO); + final String s2 = constructString(RUSSIAN_HELLO); + + final MultipartPart p1 = MultipartPartBuilder.create( + new StringBody(s1, ContentType.create("text/plain", Charset.forName("ISO-8859-1")))).build(); + final MultipartPart p2 = MultipartPartBuilder.create( + new StringBody(s2, ContentType.create("text/plain", Charset.forName("KOI8-R")))).build(); + final HttpStrictMultipart multipart = new HttpStrictMultipart(null, "foo", + Arrays.asList(p1, p2)); + + final ByteArrayOutputStream out1 = new ByteArrayOutputStream(); + multipart.writeTo(out1); + out1.close(); + + final ByteArrayOutputStream out2 = new ByteArrayOutputStream(); + + out2.write(( + "--foo\r\n" + + "Content-Type: text/plain; charset=ISO-8859-1\r\n" + + "\r\n").getBytes(StandardCharsets.US_ASCII)); + out2.write(s1.getBytes(StandardCharsets.ISO_8859_1)); + out2.write(("\r\n" + + "--foo\r\n" + + "Content-Type: text/plain; charset=KOI8-R\r\n" + + "\r\n").getBytes(StandardCharsets.US_ASCII)); + out2.write(s2.getBytes(Charset.forName("KOI8-R"))); + out2.write(("\r\n" + + "--foo--\r\n").getBytes(StandardCharsets.US_ASCII)); + out2.close(); + + final byte[] actual = out1.toByteArray(); + final byte[] expected = out2.toByteArray(); + + Assert.assertEquals(expected.length, actual.length); + for (int i = 0; i < actual.length; i++) { + Assert.assertEquals(expected[i], actual[i]); + } + Assert.assertEquals(expected.length, multipart.getTotalLength()); + } + +} diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartPartBuilder.java b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartPartBuilder.java new file mode 100644 index 000000000..6304dd797 --- /dev/null +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/entity/mime/TestMultipartPartBuilder.java @@ -0,0 +1,157 @@ +/* + * ==================================================================== + * 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.File; +import java.util.Arrays; +import java.util.List; + +import org.apache.hc.core5.http.ContentType; +import org.junit.Assert; +import org.junit.Test; + +public class TestMultipartPartBuilder { + + @Test + public void testBuildBodyPartBasics() throws Exception { + final StringBody stringBody = new StringBody("stuff", ContentType.TEXT_PLAIN); + final MultipartPart part = MultipartPartBuilder.create() + .setBody(stringBody) + .build(); + Assert.assertNotNull(part); + Assert.assertEquals(stringBody, part.getBody()); + final Header header = part.getHeader(); + Assert.assertNotNull(header); + assertFields(Arrays.asList( + new MinimalField("Content-Type", "text/plain; charset=ISO-8859-1")), + header.getFields()); + } + + @Test + public void testBuildBodyPartMultipleBuilds() throws Exception { + final StringBody stringBody = new StringBody("stuff", ContentType.TEXT_PLAIN); + final MultipartPartBuilder builder = MultipartPartBuilder.create(); + final MultipartPart part1 = builder + .setBody(stringBody) + .build(); + Assert.assertNotNull(part1); + Assert.assertEquals(stringBody, part1.getBody()); + final Header header1 = part1.getHeader(); + Assert.assertNotNull(header1); + assertFields(Arrays.asList( + new MinimalField("Content-Type", "text/plain; charset=ISO-8859-1")), + header1.getFields()); + final FileBody fileBody = new FileBody(new File("/path/stuff.bin"), ContentType.DEFAULT_BINARY); + final MultipartPart part2 = builder + .setBody(fileBody) + .build(); + + Assert.assertNotNull(part2); + Assert.assertEquals(fileBody, part2.getBody()); + final Header header2 = part2.getHeader(); + Assert.assertNotNull(header2); + assertFields(Arrays.asList( + new MinimalField("Content-Type", "application/octet-stream")), + header2.getFields()); + } + + @Test + public void testBuildBodyPartCustomHeaders() throws Exception { + final StringBody stringBody = new StringBody("stuff", ContentType.TEXT_PLAIN); + final MultipartPartBuilder builder = MultipartPartBuilder.create(stringBody); + final MultipartPart part1 = builder + .addHeader("header1", "blah") + .addHeader("header3", "blah") + .addHeader("header3", "blah") + .addHeader("header3", "blah") + .addHeader("header3", "blah") + .addHeader("header3", "blah") + .build(); + + Assert.assertNotNull(part1); + final Header header1 = part1.getHeader(); + Assert.assertNotNull(header1); + + assertFields(Arrays.asList( + new MinimalField("header1", "blah"), + new MinimalField("header3", "blah"), + new MinimalField("header3", "blah"), + new MinimalField("header3", "blah"), + new MinimalField("header3", "blah"), + new MinimalField("header3", "blah"), + new MinimalField("Content-Type", "text/plain; charset=ISO-8859-1")), + header1.getFields()); + + final MultipartPart part2 = builder + .addHeader("header2", "yada") + .removeHeaders("header3") + .build(); + + Assert.assertNotNull(part2); + final Header header2 = part2.getHeader(); + Assert.assertNotNull(header2); + + assertFields(Arrays.asList( + new MinimalField("header1", "blah"), + new MinimalField("header2", "yada"), + new MinimalField("Content-Type", "text/plain; charset=ISO-8859-1")), + header2.getFields()); + + final MultipartPart part3 = builder + .addHeader("Content-Disposition", "disposition stuff") + .addHeader("Content-Type", "type stuff") + .addHeader("Content-Transfer-Encoding", "encoding stuff") + .build(); + + Assert.assertNotNull(part3); + final Header header3 = part3.getHeader(); + Assert.assertNotNull(header3); + + assertFields(Arrays.asList( + new MinimalField("header1", "blah"), + new MinimalField("header2", "yada"), + new MinimalField("Content-Disposition", "disposition stuff"), + new MinimalField("Content-Type", "type stuff"), + new MinimalField("Content-Transfer-Encoding", "encoding stuff")), + header3.getFields()); + + } + + private static void assertFields(final List expected, final List result) { + Assert.assertNotNull(result); + Assert.assertEquals(expected.size(), result.size()); + for (int i = 0; i < expected.size(); i++) { + final MinimalField expectedField = expected.get(i); + final MinimalField resultField = result.get(i); + Assert.assertNotNull(resultField); + Assert.assertEquals(expectedField.getName(), resultField.getName()); + Assert.assertEquals(expectedField.getBody(), resultField.getBody()); + } + } + +}