View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.client5.http.entity.mime;
29  
30  import java.io.File;
31  import java.io.InputStream;
32  import java.nio.CharBuffer;
33  import java.nio.charset.Charset;
34  import java.nio.charset.StandardCharsets;
35  import java.util.ArrayList;
36  import java.util.Collections;
37  import java.util.List;
38  import java.util.concurrent.ThreadLocalRandom;
39  
40  import org.apache.hc.core5.http.ContentType;
41  import org.apache.hc.core5.http.HttpEntity;
42  import org.apache.hc.core5.http.NameValuePair;
43  import org.apache.hc.core5.http.message.BasicNameValuePair;
44  import org.apache.hc.core5.util.Args;
45  
46  /**
47   * Builder for multipart {@link HttpEntity}s.
48   *
49   * @since 5.0
50   */
51      public class MultipartEntityBuilder {
52  
53      /**
54       * The pool of ASCII chars to be used for generating a multipart boundary.
55       */
56      private final static char[] MULTIPART_CHARS =
57              "-_1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
58                      .toCharArray();
59  
60      private ContentType contentType;
61      private HttpMultipartMode mode = HttpMultipartMode.STRICT;
62      private String boundary;
63      private Charset charset;
64      private List<MultipartPart> multipartParts;
65  
66      /**
67       * The preamble of the multipart message.
68       * This field stores the optional preamble that should be added at the beginning of the multipart message.
69       * It can be {@code null} if no preamble is needed.
70       */
71      private String preamble;
72  
73      /**
74       * The epilogue of the multipart message.
75       * This field stores the optional epilogue that should be added at the end of the multipart message.
76       * It can be {@code null} if no epilogue is needed.
77       */
78      private String epilogue;
79  
80      /**
81       * An empty immutable {@code NameValuePair} array.
82       */
83      private static final NameValuePair[] EMPTY_NAME_VALUE_ARRAY = {};
84  
85      public static MultipartEntityBuilder create() {
86          return new MultipartEntityBuilder();
87      }
88  
89      MultipartEntityBuilder() {
90      }
91  
92      public MultipartEntityBuilder setMode(final HttpMultipartMode mode) {
93          this.mode = mode;
94          return this;
95      }
96  
97      public MultipartEntityBuilder setLaxMode() {
98          this.mode = HttpMultipartMode.LEGACY;
99          return this;
100     }
101 
102     public MultipartEntityBuilder setStrictMode() {
103         this.mode = HttpMultipartMode.STRICT;
104         return this;
105     }
106 
107     public MultipartEntityBuilder setBoundary(final String boundary) {
108         this.boundary = boundary;
109         return this;
110     }
111 
112     /**
113      * @since 4.4
114      */
115     public MultipartEntityBuilder setMimeSubtype(final String subType) {
116         Args.notBlank(subType, "MIME subtype");
117         this.contentType = ContentType.create("multipart/" + subType);
118         return this;
119     }
120 
121     /**
122      * @since 4.5
123      */
124     public MultipartEntityBuilder setContentType(final ContentType contentType) {
125         Args.notNull(contentType, "Content type");
126         this.contentType = contentType;
127         return this;
128     }
129     /**
130      *  Add parameter to the current {@link ContentType}.
131      *
132      * @param parameter The name-value pair parameter to add to the {@link ContentType}.
133      * @return the {@link MultipartEntityBuilder} instance.
134      * @since 5.2
135      */
136     public MultipartEntityBuilder addParameter(final BasicNameValuePair parameter) {
137         this.contentType = contentType.withParameters(parameter);
138         return this;
139     }
140 
141     public MultipartEntityBuilder setCharset(final Charset charset) {
142         this.charset = charset;
143         return this;
144     }
145 
146     /**
147      * @since 4.4
148      */
149     public MultipartEntityBuilder addPart(final MultipartPart multipartPart) {
150         if (multipartPart == null) {
151             return this;
152         }
153         if (this.multipartParts == null) {
154             this.multipartParts = new ArrayList<>();
155         }
156         this.multipartParts.add(multipartPart);
157         return this;
158     }
159 
160     public MultipartEntityBuilder addPart(final String name, final ContentBody contentBody) {
161         Args.notNull(name, "Name");
162         Args.notNull(contentBody, "Content body");
163         return addPart(FormBodyPartBuilder.create(name, contentBody).build());
164     }
165 
166     public MultipartEntityBuilder addTextBody(
167             final String name, final String text, final ContentType contentType) {
168         return addPart(name, new StringBody(text, contentType));
169     }
170 
171     public MultipartEntityBuilder addTextBody(
172             final String name, final String text) {
173         return addTextBody(name, text, ContentType.DEFAULT_TEXT);
174     }
175 
176     public MultipartEntityBuilder addBinaryBody(
177             final String name, final byte[] b, final ContentType contentType, final String filename) {
178         return addPart(name, new ByteArrayBody(b, contentType, filename));
179     }
180 
181     public MultipartEntityBuilder addBinaryBody(
182             final String name, final byte[] b) {
183         return addPart(name, new ByteArrayBody(b, ContentType.DEFAULT_BINARY));
184     }
185 
186     public MultipartEntityBuilder addBinaryBody(
187             final String name, final File file, final ContentType contentType, final String filename) {
188         return addPart(name, new FileBody(file, contentType, filename));
189     }
190 
191     public MultipartEntityBuilder addBinaryBody(
192             final String name, final File file) {
193         return addBinaryBody(name, file, ContentType.DEFAULT_BINARY, file != null ? file.getName() : null);
194     }
195 
196     public MultipartEntityBuilder addBinaryBody(
197             final String name, final InputStream stream, final ContentType contentType,
198             final String filename) {
199         return addPart(name, new InputStreamBody(stream, contentType, filename));
200     }
201 
202     public MultipartEntityBuilder addBinaryBody(final String name, final InputStream stream) {
203         return addBinaryBody(name, stream, ContentType.DEFAULT_BINARY, null);
204     }
205 
206     /**
207      * Adds a preamble to the multipart entity being constructed. The preamble is the text that appears before the first
208      * boundary delimiter. The preamble is optional and may be null.
209      *
210      * @param preamble The preamble text to add to the multipart entity
211      * @return This MultipartEntityBuilder instance, to allow for method chaining
212      *
213      * @since 5.3
214      */
215     public MultipartEntityBuilder addPreamble(final String preamble) {
216         this.preamble = preamble;
217         return this;
218     }
219 
220     /**
221      * Adds an epilogue to the multipart entity being constructed. The epilogue is the text that appears after the last
222      * boundary delimiter. The epilogue is optional and may be null.
223      *
224      * @param epilogue The epilogue text to add to the multipart entity
225      * @return This MultipartEntityBuilder instance, to allow for method chaining
226      * @since 5.3
227      */
228     public MultipartEntityBuilder addEpilogue(final String epilogue) {
229         this.epilogue = epilogue;
230         return this;
231     }
232 
233     private String generateBoundary() {
234         final ThreadLocalRandom rand = ThreadLocalRandom.current();
235         final int count = rand.nextInt(30, 41); // a random size from 30 to 40
236         final CharBuffer buffer = CharBuffer.allocate(count);
237         while (buffer.hasRemaining()) {
238             buffer.put(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
239         }
240         buffer.flip();
241         return buffer.toString();
242     }
243 
244     MultipartFormEntity buildEntity() {
245         String boundaryCopy = boundary;
246         if (boundaryCopy == null && contentType != null) {
247             boundaryCopy = contentType.getParameter("boundary");
248         }
249         if (boundaryCopy == null) {
250             boundaryCopy = generateBoundary();
251         }
252         Charset charsetCopy = charset;
253         if (charsetCopy == null && contentType != null) {
254             charsetCopy = contentType.getCharset();
255         }
256         final List<NameValuePair> paramsList = new ArrayList<>(2);
257         paramsList.add(new BasicNameValuePair("boundary", boundaryCopy));
258         if (charsetCopy != null) {
259             paramsList.add(new BasicNameValuePair("charset", charsetCopy.name()));
260         }
261         final NameValuePair[] params = paramsList.toArray(EMPTY_NAME_VALUE_ARRAY);
262 
263         final ContentType contentTypeCopy;
264         if (contentType != null) {
265             contentTypeCopy = contentType.withParameters(params);
266         } else {
267             boolean formData = false;
268             if (multipartParts != null) {
269                 for (final MultipartPart multipartPart : multipartParts) {
270                     if (multipartPart instanceof FormBodyPart) {
271                         formData = true;
272                         break;
273                     }
274                 }
275             }
276 
277             if (formData) {
278                 contentTypeCopy = ContentType.MULTIPART_FORM_DATA.withParameters(params);
279             } else {
280                 contentTypeCopy = ContentType.create("multipart/mixed", params);
281             }
282         }
283         final List<MultipartPart> multipartPartsCopy = multipartParts != null ? new ArrayList<>(multipartParts) :
284                 Collections.emptyList();
285         final HttpMultipartMode modeCopy = mode != null ? mode : HttpMultipartMode.STRICT;
286         final AbstractMultipartFormat form;
287         switch (modeCopy) {
288             case LEGACY:
289                 form = new LegacyMultipart(charsetCopy, boundaryCopy, multipartPartsCopy);
290                 break;
291             case EXTENDED:
292                 if (contentTypeCopy.isSameMimeType(ContentType.MULTIPART_FORM_DATA)) {
293                     if (charsetCopy == null) {
294                         charsetCopy = StandardCharsets.UTF_8;
295                     }
296                     form = new HttpRFC7578Multipart(charsetCopy, boundaryCopy, multipartPartsCopy, preamble, epilogue);
297                 } else {
298                     form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, multipartPartsCopy, preamble, epilogue);
299                 }
300                 break;
301             default:
302                 form = new HttpStrictMultipart(StandardCharsets.US_ASCII, boundaryCopy, multipartPartsCopy, preamble, epilogue);
303         }
304         return new MultipartFormEntity(form, contentTypeCopy, form.getTotalLength());
305     }
306 
307     public HttpEntity build() {
308         return buildEntity();
309     }
310 
311 }