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 = new NameValuePair[0];
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     public MultipartEntityBuilder setCharset(final Charset charset) {
117         this.charset = charset;
118         return this;
119     }
120 
121     /**
122      * @since 4.4
123      */
124     public MultipartEntityBuilder addPart(final MultipartPart multipartPart) {
125         if (multipartPart == null) {
126             return this;
127         }
128         if (this.multipartParts == null) {
129             this.multipartParts = new ArrayList<>();
130         }
131         this.multipartParts.add(multipartPart);
132         return this;
133     }
134 
135     public MultipartEntityBuilder addPart(final String name, final ContentBody contentBody) {
136         Args.notNull(name, "Name");
137         Args.notNull(contentBody, "Content body");
138         return addPart(FormBodyPartBuilder.create(name, contentBody).build());
139     }
140 
141     public MultipartEntityBuilder addTextBody(
142             final String name, final String text, final ContentType contentType) {
143         return addPart(name, new StringBody(text, contentType));
144     }
145 
146     public MultipartEntityBuilder addTextBody(
147             final String name, final String text) {
148         return addTextBody(name, text, ContentType.DEFAULT_TEXT);
149     }
150 
151     public MultipartEntityBuilder addBinaryBody(
152             final String name, final byte[] b, final ContentType contentType, final String filename) {
153         return addPart(name, new ByteArrayBody(b, contentType, filename));
154     }
155 
156     public MultipartEntityBuilder addBinaryBody(
157             final String name, final byte[] b) {
158         return addBinaryBody(name, b, ContentType.DEFAULT_BINARY, null);
159     }
160 
161     public MultipartEntityBuilder addBinaryBody(
162             final String name, final File file, final ContentType contentType, final String filename) {
163         return addPart(name, new FileBody(file, contentType, filename));
164     }
165 
166     public MultipartEntityBuilder addBinaryBody(
167             final String name, final File file) {
168         return addBinaryBody(name, file, ContentType.DEFAULT_BINARY, file != null ? file.getName() : null);
169     }
170 
171     public MultipartEntityBuilder addBinaryBody(
172             final String name, final InputStream stream, final ContentType contentType,
173             final String filename) {
174         return addPart(name, new InputStreamBody(stream, contentType, filename));
175     }
176 
177     public MultipartEntityBuilder addBinaryBody(final String name, final InputStream stream) {
178         return addBinaryBody(name, stream, ContentType.DEFAULT_BINARY, null);
179     }
180 
181     private String generateBoundary() {
182         final ThreadLocalRandom rand = ThreadLocalRandom.current();
183         final int count = rand.nextInt(30, 41); // a random size from 30 to 40
184         final CharBuffer buffer = CharBuffer.allocate(count);
185         while (buffer.hasRemaining()) {
186             buffer.put(MULTIPART_CHARS[rand.nextInt(MULTIPART_CHARS.length)]);
187         }
188         buffer.flip();
189         return buffer.toString();
190     }
191 
192     MultipartFormEntity buildEntity() {
193         String boundaryCopy = boundary;
194         if (boundaryCopy == null && contentType != null) {
195             boundaryCopy = contentType.getParameter("boundary");
196         }
197         if (boundaryCopy == null) {
198             boundaryCopy = generateBoundary();
199         }
200         Charset charsetCopy = charset;
201         if (charsetCopy == null && contentType != null) {
202             charsetCopy = contentType.getCharset();
203         }
204         final List<NameValuePair> paramsList = new ArrayList<>(2);
205         paramsList.add(new BasicNameValuePair("boundary", boundaryCopy));
206         if (charsetCopy != null) {
207             paramsList.add(new BasicNameValuePair("charset", charsetCopy.name()));
208         }
209         final NameValuePair[] params = paramsList.toArray(EMPTY_NAME_VALUE_ARRAY);
210 
211         final ContentType contentTypeCopy;
212         if (contentType != null) {
213             contentTypeCopy = contentType.withParameters(params);
214         } else {
215             boolean formData = false;
216             if (multipartParts != null) {
217                 for (final MultipartPart multipartPart : multipartParts) {
218                     if (multipartPart instanceof FormBodyPart) {
219                         formData = true;
220                         break;
221                     }
222                 }
223             }
224 
225             if (formData) {
226                 contentTypeCopy = ContentType.MULTIPART_FORM_DATA.withParameters(params);
227             } else {
228                 contentTypeCopy = ContentType.create("multipart/mixed", params);
229             }
230         }
231         final List<MultipartPart> multipartPartsCopy = multipartParts != null ? new ArrayList<>(multipartParts) :
232                 Collections.<MultipartPart>emptyList();
233         final HttpMultipartMode modeCopy = mode != null ? mode : HttpMultipartMode.STRICT;
234         final AbstractMultipartFormat form;
235         switch (modeCopy) {
236             case LEGACY:
237                 form = new LegacyMultipart(charsetCopy, boundaryCopy, multipartPartsCopy);
238                 break;
239             case EXTENDED:
240                 if (contentTypeCopy.isSameMimeType(ContentType.MULTIPART_FORM_DATA)) {
241                     if (charsetCopy == null) {
242                         charsetCopy = StandardCharsets.UTF_8;
243                     }
244                     form = new HttpRFC7578Multipart(charsetCopy, boundaryCopy, multipartPartsCopy);
245                 } else {
246                     form = new HttpRFC6532Multipart(charsetCopy, boundaryCopy, multipartPartsCopy);
247                 }
248                 break;
249             default:
250                 form = new HttpStrictMultipart(StandardCharsets.US_ASCII, boundaryCopy, multipartPartsCopy);
251         }
252         return new MultipartFormEntity(form, contentTypeCopy, form.getTotalLength());
253     }
254 
255     public HttpEntity build() {
256         return buildEntity();
257     }
258 
259 }