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.nio.charset.CharsetEncoder;
31  import java.nio.charset.StandardCharsets;
32  import java.util.ArrayList;
33  import java.util.List;
34  
35  import org.apache.hc.core5.http.ContentType;
36  import org.apache.hc.core5.http.NameValuePair;
37  import org.apache.hc.core5.http.message.BasicNameValuePair;
38  import org.apache.hc.core5.net.PercentCodec;
39  import org.apache.hc.core5.util.Args;
40  import org.apache.hc.core5.util.Asserts;
41  
42  /**
43   * Builder for individual {@link org.apache.hc.client5.http.entity.mime.FormBodyPart}s.
44   *
45   * @since 4.4
46   */
47  public class FormBodyPartBuilder {
48  
49      private String name;
50      private ContentBody body;
51      private final Header header;
52  
53      /**
54       * The multipart mode determining how filenames are encoded in the {@code Content-Disposition}
55       * header, defaults to {@link HttpMultipartMode#STRICT}.
56       *
57       * @since 5.5
58       */
59      private HttpMultipartMode mode;
60  
61      /**
62       * Encoder used to check if strings can be encoded in ISO-8859-1, supporting filename
63       * compatibility determinations in multipart form data.
64       */
65      private CharsetEncoder iso8859_1Encoder;
66  
67      /**
68       * Creates a new builder instance with the specified name, content body, and multipart mode.
69       *
70       * @param name the name of the form field
71       * @param body the content body of the part
72       * @param mode the {@link HttpMultipartMode} to use, determining filename encoding behavior;
73       *
74       * @return a new {@code FormBodyPartBuilder} instance
75       * @since 5.5
76       */
77      public static FormBodyPartBuilder create(final String name, final ContentBody body, final HttpMultipartMode mode) {
78          return new FormBodyPartBuilder(name, body, mode);
79      }
80  
81      public static FormBodyPartBuilder create(final String name, final ContentBody body) {
82          return new FormBodyPartBuilder(name, body, HttpMultipartMode.STRICT);
83      }
84  
85      public static FormBodyPartBuilder create() {
86          return new FormBodyPartBuilder();
87      }
88  
89      FormBodyPartBuilder(final String name, final ContentBody body, final HttpMultipartMode mode) {
90          this();
91          this.name = name;
92          this.body = body;
93          this.mode = mode != null ? mode : HttpMultipartMode.STRICT;
94      }
95  
96      FormBodyPartBuilder() {
97          this.header = new Header();
98          this.mode = HttpMultipartMode.STRICT;
99      }
100 
101     public FormBodyPartBuilder setName(final String name) {
102         this.name = name;
103         return this;
104     }
105 
106     public FormBodyPartBuilder setBody(final ContentBody body) {
107         this.body = body;
108         return this;
109     }
110 
111     /**
112      * @since 4.6
113      */
114     public FormBodyPartBuilder addField(final String name, final String value, final List<NameValuePair> parameters) {
115         Args.notNull(name, "Field name");
116         this.header.addField(new MimeField(name, value, parameters));
117         return this;
118     }
119 
120     public FormBodyPartBuilder addField(final String name, final String value) {
121         Args.notNull(name, "Field name");
122         this.header.addField(new MimeField(name, value));
123         return this;
124     }
125 
126     public FormBodyPartBuilder setField(final String name, final String value) {
127         Args.notNull(name, "Field name");
128         this.header.setField(new MimeField(name, value));
129         return this;
130     }
131 
132     public FormBodyPartBuilder removeFields(final String name) {
133         Args.notNull(name, "Field name");
134         this.header.removeFields(name);
135         return this;
136     }
137 
138     /**
139      * Determines whether the given string can be encoded in ISO-8859-1 without loss of data.
140      * This is used to decide whether the {@code filename} parameter can be used as-is or if
141      * the {@code filename*} parameter is needed for non-ISO-8859-1 characters.
142      *
143      * @param input the string to check, must not be {@code null}
144      * @return {@code true} if the string can be encoded in ISO-8859-1, {@code false} otherwise
145      * @since 5.5
146      */
147     private boolean canEncodeToISO8859_1(final String input) {
148         if (iso8859_1Encoder == null) {
149             iso8859_1Encoder = StandardCharsets.ISO_8859_1.newEncoder();
150         }
151         return iso8859_1Encoder.canEncode(input);
152     }
153 
154     /**
155      * Encodes the given filename according to RFC 5987, prefixing it with {@code UTF-8''} and
156      * applying percent-encoding to non-ASCII characters. This is used for the {@code filename*}
157      * parameter in the {@code Content-Disposition} header when non-ISO-8859-1 characters are present.
158      *
159      * @param filename the filename to encode, must not be {@code null}
160      * @return the RFC 5987-encoded string, e.g., {@code UTF-8''example%20text}
161      * @since 5.5
162      */
163     private static String encodeRFC5987(final String filename) {
164         return "UTF-8''" + PercentCodec.RFC5987.encode(filename);
165     }
166 
167     public FormBodyPart build() {
168         Asserts.notBlank(this.name, "Name");
169         Asserts.notNull(this.body, "Content body");
170         final Header headerCopy = new Header();
171         final List<MimeField> fields = this.header.getFields();
172         for (final MimeField field: fields) {
173             headerCopy.addField(field);
174         }
175         if (headerCopy.getField(MimeConsts.CONTENT_DISPOSITION) == null) {
176             final List<NameValuePair> fieldParameters = new ArrayList<>();
177             fieldParameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_NAME, this.name));
178             if (this.body.getFilename() != null) {
179                 final String filename = this.body.getFilename();
180                 fieldParameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_FILENAME, filename));
181                 // Add filename* only if non-ISO-8859-1 and not in LEGACY mode
182                 if (mode != HttpMultipartMode.LEGACY && !canEncodeToISO8859_1(filename)) {
183                     fieldParameters.add(new BasicNameValuePair(MimeConsts.FIELD_PARAM_FILENAME_START, encodeRFC5987(filename)));
184                 }
185             }
186             headerCopy.addField(new MimeField(MimeConsts.CONTENT_DISPOSITION, "form-data", fieldParameters));
187         }
188         if (headerCopy.getField(MimeConsts.CONTENT_TYPE) == null) {
189             final ContentType contentType;
190             if (body instanceof AbstractContentBody) {
191                 contentType = ((AbstractContentBody) body).getContentType();
192             } else {
193                 contentType = null;
194             }
195             if (contentType != null) {
196                 headerCopy.addField(new MimeField(MimeConsts.CONTENT_TYPE, contentType.toString()));
197             } else {
198                 final StringBuilder buffer = new StringBuilder();
199                 buffer.append(this.body.getMimeType()); // MimeType cannot be null
200                 if (this.body.getCharset() != null) { // charset may legitimately be null
201                     buffer.append("; charset=");
202                     buffer.append(this.body.getCharset());
203                 }
204                 headerCopy.addField(new MimeField(MimeConsts.CONTENT_TYPE, buffer.toString()));
205             }
206         }
207         return new FormBodyPart(this.name, this.body, headerCopy);
208     }
209 }