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