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       * An empty immutable {@code NameValuePair} array.
68       */
69      private static final NameValuePair[] EMPTY_NAME_VALUE_ARRAY = {};
70  
71      public static MultipartEntityBuilder create() {
72          return new MultipartEntityBuilder();
73      }
74  
75      MultipartEntityBuilder() {
76      }
77  
78      public MultipartEntityBuilder setMode(final HttpMultipartMode mode) {
79          this.mode = mode;
80          return this;
81      }
82  
83      public MultipartEntityBuilder setLaxMode() {
84          this.mode = HttpMultipartMode.LEGACY;
85          return this;
86      }
87  
88      public MultipartEntityBuilder setStrictMode() {
89          this.mode = HttpMultipartMode.STRICT;
90          return this;
91      }
92  
93      public MultipartEntityBuilder setBoundary(final String boundary) {
94          this.boundary = boundary;
95          return this;
96      }
97  
98      /**
99       * @since 4.4
100      */
101     public MultipartEntityBuilder setMimeSubtype(final String subType) {
102         Args.notBlank(subType, "MIME subtype");
103         this.contentType = ContentType.create("multipart/" + subType);
104         return this;
105     }
106 
107     /**
108      * @since 4.5
109      */
110     public MultipartEntityBuilder setContentType(final ContentType contentType) {
111         Args.notNull(contentType, "Content type");
112         this.contentType = contentType;
113         return this;
114     }
115     /**
116      *  Add parameter to the current {@link ContentType}.
117      *
118      * @param parameter The name-value pair parameter to add to the {@link ContentType}.
119      * @return the {@link MultipartEntityBuilder} instance.
120      * @since 5.2
121      */
122     public MultipartEntityBuilder addParameter(final BasicNameValuePair parameter) {
123         this.contentType = contentType.withParameters(parameter);
124         return this;
125     }
126 
127     public MultipartEntityBuilder setCharset(final Charset charset) {
128         this.charset = charset;
129         return this;
130     }
131 
132     /**
133      * @since 4.4
134      */
135     public MultipartEntityBuilder addPart(final MultipartPart multipartPart) {
136         if (multipartPart == null) {
137             return this;
138         }
139         if (this.multipartParts == null) {
140             this.multipartParts = new ArrayList<>();
141         }
142         this.multipartParts.add(multipartPart);
143         return this;
144     }
145 
146     public MultipartEntityBuilder addPart(final String name, final ContentBody contentBody) {
147         Args.notNull(name, "Name");
148         Args.notNull(contentBody, "Content body");
149         return addPart(FormBodyPartBuilder.create(name, contentBody).build());
150     }
151 
152     public MultipartEntityBuilder addTextBody(
153             final String name, final String text, final ContentType contentType) {
154         return addPart(name, new StringBody(text, contentType));
155     }
156 
157     public MultipartEntityBuilder addTextBody(
158             final String name, final String text) {
159         return addTextBody(name, text, ContentType.DEFAULT_TEXT);
160     }
161 
162     public MultipartEntityBuilder addBinaryBody(
163             final String name, final byte[] b, final ContentType contentType, final String filename) {
164         return addPart(name, new ByteArrayBody(b, contentType, filename));
165     }
166 
167     public MultipartEntityBuilder addBinaryBody(
168             final String name, final byte[] b) {
169         return addPart(name, new ByteArrayBody(b, ContentType.DEFAULT_BINARY));
170     }
171 
172     public MultipartEntityBuilder addBinaryBody(
173             final String name, final File file, final ContentType contentType, final String filename) {
174         return addPart(name, new FileBody(file, contentType, filename));
175     }
176 
177     public MultipartEntityBuilder addBinaryBody(
178             final String name, final File file) {
179         return addBinaryBody(name, file, ContentType.DEFAULT_BINARY, file != null ? file.getName() : null);
180     }
181 
182     public MultipartEntityBuilder addBinaryBody(
183             final String name, final InputStream stream, final ContentType contentType,
184             final String filename) {
185         return addPart(name, new InputStreamBody(stream, contentType, filename));
186     }
187 
188     public MultipartEntityBuilder addBinaryBody(final String name, final InputStream stream) {
189         return addBinaryBody(name, stream, ContentType.DEFAULT_BINARY, null);
190     }
191 
192     private String generateBoundary() {
193         final ThreadLocalRandom rand = ThreadLocalRandom.current();
194         final int count = rand.nextInt(30, 41); // a random size from 30 to 40
195         final CharBuffer buffer = CharBuffer.allocate(count);
196         while (buffer.hasRemaining()) {
197             buffer.put(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
198         }
199         buffer.flip();
200         return buffer.toString();
201     }
202 
203     MultipartFormEntity buildEntity() {
204         String boundaryCopy = boundary;
205         if (boundaryCopy == null && contentType != null) {
206             boundaryCopy = contentType.getParameter("boundary");
207         }
208         if (boundaryCopy == null) {
209             boundaryCopy = generateBoundary();
210         }
211         Charset charsetCopy = charset;
212         if (charsetCopy == null && contentType != null) {
213             charsetCopy = contentType.getCharset();
214         }
215         final List<NameValuePair> paramsList = new ArrayList<>(2);
216         paramsList.add(new BasicNameValuePair("boundary", boundaryCopy));
217         if (charsetCopy != null) {
218             paramsList.add(new BasicNameValuePair("charset", charsetCopy.name()));
219         }
220         final NameValuePair[] params = paramsList.toArray(EMPTY_NAME_VALUE_ARRAY);
221 
222         final ContentType contentTypeCopy;
223         if (contentType != null) {
224             contentTypeCopy = contentType.withParameters(params);
225         } else {
226             boolean formData = false;
227             if (multipartParts != null) {
228                 for (final MultipartPart multipartPart : multipartParts) {
229                     if (multipartPart instanceof FormBodyPart) {
230                         formData = true;
231                         break;
232                     }
233                 }
234             }
235 
236             if (formData) {
237                 contentTypeCopy = ContentType.MULTIPART_FORM_DATA.withParameters(params);
238             } else {
239                 contentTypeCopy = ContentType.create("multipart/mixed", params);
240             }
241         }
242         final List<MultipartPart> multipartPartsCopy = multipartParts != null ? new ArrayList<>(multipartParts) :
243                 Collections.emptyList();
244         final HttpMultipartMode modeCopy = mode != null ? mode : HttpMultipartMode.STRICT;
245         final AbstractMultipartFormat form;
246         switch (modeCopy) {
247             case LEGACY:
248                 form = new LegacyMultipart(charsetCopy, boundaryCopy, multipartPartsCopy);
249                 break;
250             case EXTENDED:
251                 if (contentTypeCopy.isSameMimeType(ContentType.MULTIPART_FORM_DATA)) {
252                     if (charsetCopy == null) {
253                         charsetCopy = StandardCharsets.UTF_8;
254                     }
255                     form = new HttpRFC7578Multipart(charsetCopy, boundaryCopy, multipartPartsCopy);
256                 } else {
257                     form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, multipartPartsCopy);
258                 }
259                 break;
260             default:
261                 form = new HttpStrictMultipart(StandardCharsets.US_ASCII, boundaryCopy, multipartPartsCopy);
262         }
263         return new MultipartFormEntity(form, contentTypeCopy, form.getTotalLength());
264     }
265 
266     public HttpEntity build() {
267         return buildEntity();
268     }
269 
270 }