View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http.multipart;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.buffer.ByteBufAllocator;
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.handler.codec.DecoderResult;
22  import io.netty.handler.codec.http.DefaultFullHttpRequest;
23  import io.netty.handler.codec.http.DefaultHttpContent;
24  import io.netty.handler.codec.http.EmptyHttpHeaders;
25  import io.netty.handler.codec.http.FullHttpRequest;
26  import io.netty.handler.codec.http.HttpConstants;
27  import io.netty.handler.codec.http.HttpContent;
28  import io.netty.handler.codec.http.HttpHeaderNames;
29  import io.netty.handler.codec.http.HttpHeaderValues;
30  import io.netty.handler.codec.http.HttpHeaders;
31  import io.netty.handler.codec.http.HttpMethod;
32  import io.netty.handler.codec.http.HttpRequest;
33  import io.netty.handler.codec.http.HttpUtil;
34  import io.netty.handler.codec.http.HttpVersion;
35  import io.netty.handler.codec.http.LastHttpContent;
36  import io.netty.handler.stream.ChunkedInput;
37  import io.netty.util.internal.PlatformDependent;
38  import io.netty.util.internal.StringUtil;
39  
40  import java.io.File;
41  import java.io.IOException;
42  import java.io.UnsupportedEncodingException;
43  import java.net.URLEncoder;
44  import java.nio.charset.Charset;
45  import java.util.ArrayList;
46  import java.util.List;
47  import java.util.ListIterator;
48  import java.util.Map;
49  import java.util.regex.Pattern;
50  
51  import static io.netty.buffer.Unpooled.wrappedBuffer;
52  import static io.netty.util.internal.ObjectUtil.checkNotNull;
53  import static java.util.AbstractMap.SimpleImmutableEntry;
54  
55  /**
56   * This encoder will help to encode Request for a FORM as POST.
57   *
58   * <P>According to RFC 7231, POST, PUT and OPTIONS allow to have a body.
59   * This encoder will support widely all methods except TRACE since the RFC notes
60   * for GET, DELETE, HEAD and CONNECT: (replaces XXX by one of these methods)</P>
61   * <P>"A payload within a XXX request message has no defined semantics;
62   * sending a payload body on a XXX request might cause some existing
63   * implementations to reject the request."</P>
64   * <P>On the contrary, for TRACE method, RFC says:</P>
65   * <P>"A client MUST NOT send a message body in a TRACE request."</P>
66   */
67  public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> {
68  
69      /**
70       * Different modes to use to encode form data.
71       */
72      public enum EncoderMode {
73          /**
74           * Legacy mode which should work for most. It is known to not work with OAUTH. For OAUTH use
75           * {@link EncoderMode#RFC3986}. The W3C form recommendations this for submitting post form data.
76           */
77          RFC1738,
78  
79          /**
80           * Mode which is more new and is used for OAUTH
81           */
82          RFC3986,
83  
84          /**
85           * The HTML5 spec disallows mixed mode in multipart/form-data
86           * requests. More concretely this means that more files submitted
87           * under the same name will not be encoded using mixed mode, but
88           * will be treated as distinct fields.
89           *
90           * Reference:
91           *   http://www.w3.org/TR/html5/forms.html#multipart-form-data
92           */
93          HTML5
94      }
95  
96      private static final Map.Entry[] percentEncodings;
97  
98      static {
99          percentEncodings = new Map.Entry[] {
100                 new SimpleImmutableEntry<Pattern, String>(Pattern.compile("\\*"), "%2A"),
101                 new SimpleImmutableEntry<Pattern, String>(Pattern.compile("\\+"), "%20"),
102                 new SimpleImmutableEntry<Pattern, String>(Pattern.compile("~"), "%7E")
103         };
104     }
105 
106     /**
107      * Factory used to create InterfaceHttpData
108      */
109     private final HttpDataFactory factory;
110 
111     /**
112      * Request to encode
113      */
114     private final HttpRequest request;
115 
116     /**
117      * Default charset to use
118      */
119     private final Charset charset;
120 
121     /**
122      * Chunked false by default
123      */
124     private boolean isChunked;
125 
126     /**
127      * InterfaceHttpData for Body (without encoding)
128      */
129     private final List<InterfaceHttpData> bodyListDatas;
130     /**
131      * The final Multipart List of InterfaceHttpData including encoding
132      */
133     final List<InterfaceHttpData> multipartHttpDatas;
134 
135     /**
136      * Does this request is a Multipart request
137      */
138     private final boolean isMultipart;
139 
140     /**
141      * If multipart, this is the boundary for the flobal multipart
142      */
143     String multipartDataBoundary;
144 
145     /**
146      * If multipart, there could be internal multiparts (mixed) to the global multipart. Only one level is allowed.
147      */
148     String multipartMixedBoundary;
149     /**
150      * To check if the header has been finalized
151      */
152     private boolean headerFinalized;
153 
154     private final EncoderMode encoderMode;
155 
156     /**
157      *
158      * @param request
159      *            the request to encode
160      * @param multipart
161      *            True if the FORM is a ENCTYPE="multipart/form-data"
162      * @throws NullPointerException
163      *             for request
164      * @throws ErrorDataEncoderException
165      *             if the request is a TRACE
166      */
167     public HttpPostRequestEncoder(HttpRequest request, boolean multipart) throws ErrorDataEncoderException {
168         this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE), request, multipart,
169                 HttpConstants.DEFAULT_CHARSET, EncoderMode.RFC1738);
170     }
171 
172     /**
173      *
174      * @param factory
175      *            the factory used to create InterfaceHttpData
176      * @param request
177      *            the request to encode
178      * @param multipart
179      *            True if the FORM is a ENCTYPE="multipart/form-data"
180      * @throws NullPointerException
181      *             for request and factory
182      * @throws ErrorDataEncoderException
183      *             if the request is a TRACE
184      */
185     public HttpPostRequestEncoder(HttpDataFactory factory, HttpRequest request, boolean multipart)
186             throws ErrorDataEncoderException {
187         this(factory, request, multipart, HttpConstants.DEFAULT_CHARSET, EncoderMode.RFC1738);
188     }
189 
190     /**
191      *
192      * @param factory
193      *            the factory used to create InterfaceHttpData
194      * @param request
195      *            the request to encode
196      * @param multipart
197      *            True if the FORM is a ENCTYPE="multipart/form-data"
198      * @param charset
199      *            the charset to use as default
200      * @param encoderMode
201      *            the mode for the encoder to use. See {@link EncoderMode} for the details.
202      * @throws NullPointerException
203      *             for request or charset or factory
204      * @throws ErrorDataEncoderException
205      *             if the request is a TRACE
206      */
207     public HttpPostRequestEncoder(
208             HttpDataFactory factory, HttpRequest request, boolean multipart, Charset charset,
209             EncoderMode encoderMode)
210             throws ErrorDataEncoderException {
211         this.request = checkNotNull(request, "request");
212         this.charset = checkNotNull(charset, "charset");
213         this.factory = checkNotNull(factory, "factory");
214         if (HttpMethod.TRACE.equals(request.method())) {
215             throw new ErrorDataEncoderException("Cannot create a Encoder if request is a TRACE");
216         }
217         // Fill default values
218         bodyListDatas = new ArrayList<InterfaceHttpData>();
219         // default mode
220         isLastChunk = false;
221         isLastChunkSent = false;
222         isMultipart = multipart;
223         multipartHttpDatas = new ArrayList<InterfaceHttpData>();
224         this.encoderMode = encoderMode;
225         if (isMultipart) {
226             initDataMultipart();
227         }
228     }
229 
230     /**
231      * Clean all HttpDatas (on Disk) for the current request.
232      */
233     public void cleanFiles() {
234         factory.cleanRequestHttpData(request);
235     }
236 
237     /**
238      * Does the last non empty chunk already encoded so that next chunk will be empty (last chunk)
239      */
240     private boolean isLastChunk;
241     /**
242      * Last chunk already sent
243      */
244     private boolean isLastChunkSent;
245     /**
246      * The current FileUpload that is currently in encode process
247      */
248     private FileUpload currentFileUpload;
249     /**
250      * While adding a FileUpload, is the multipart currently in Mixed Mode
251      */
252     private boolean duringMixedMode;
253     /**
254      * Global Body size
255      */
256     private long globalBodySize;
257     /**
258      * Global Transfer progress
259      */
260     private long globalProgress;
261 
262     /**
263      * True if this request is a Multipart request
264      *
265      * @return True if this request is a Multipart request
266      */
267     public boolean isMultipart() {
268         return isMultipart;
269     }
270 
271     /**
272      * Init the delimiter for Global Part (Data).
273      */
274     private void initDataMultipart() {
275         multipartDataBoundary = getNewMultipartDelimiter();
276     }
277 
278     /**
279      * Init the delimiter for Mixed Part (Mixed).
280      */
281     private void initMixedMultipart() {
282         multipartMixedBoundary = getNewMultipartDelimiter();
283     }
284 
285     /**
286      *
287      * @return a newly generated Delimiter (either for DATA or MIXED)
288      */
289     private static String getNewMultipartDelimiter() {
290         // construct a generated delimiter
291         return Long.toHexString(PlatformDependent.threadLocalRandom().nextLong());
292     }
293 
294     /**
295      * This getMethod returns a List of all InterfaceHttpData from body part.<br>
296 
297      * @return the list of InterfaceHttpData from Body part
298      */
299     public List<InterfaceHttpData> getBodyListAttributes() {
300         return bodyListDatas;
301     }
302 
303     /**
304      * Set the Body HttpDatas list
305      *
306      * @throws NullPointerException
307      *             for datas
308      * @throws ErrorDataEncoderException
309      *             if the encoding is in error or if the finalize were already done
310      */
311     public void setBodyHttpDatas(List<InterfaceHttpData> datas) throws ErrorDataEncoderException {
312         if (datas == null) {
313             throw new NullPointerException("datas");
314         }
315         globalBodySize = 0;
316         bodyListDatas.clear();
317         currentFileUpload = null;
318         duringMixedMode = false;
319         multipartHttpDatas.clear();
320         for (InterfaceHttpData data : datas) {
321             addBodyHttpData(data);
322         }
323     }
324 
325     /**
326      * Add a simple attribute in the body as Name=Value
327      *
328      * @param name
329      *            name of the parameter
330      * @param value
331      *            the value of the parameter
332      * @throws NullPointerException
333      *             for name
334      * @throws ErrorDataEncoderException
335      *             if the encoding is in error or if the finalize were already done
336      */
337     public void addBodyAttribute(String name, String value) throws ErrorDataEncoderException {
338         String svalue = value != null? value : StringUtil.EMPTY_STRING;
339         Attribute data = factory.createAttribute(request, checkNotNull(name, "name"), svalue);
340         addBodyHttpData(data);
341     }
342 
343     /**
344      * Add a file as a FileUpload
345      *
346      * @param name
347      *            the name of the parameter
348      * @param file
349      *            the file to be uploaded (if not Multipart mode, only the filename will be included)
350      * @param contentType
351      *            the associated contentType for the File
352      * @param isText
353      *            True if this file should be transmitted in Text format (else binary)
354      * @throws NullPointerException
355      *             for name and file
356      * @throws ErrorDataEncoderException
357      *             if the encoding is in error or if the finalize were already done
358      */
359     public void addBodyFileUpload(String name, File file, String contentType, boolean isText)
360             throws ErrorDataEncoderException {
361         addBodyFileUpload(name, file.getName(), file, contentType, isText);
362     }
363 
364     /**
365      * Add a file as a FileUpload
366      *
367      * @param name
368      *            the name of the parameter
369      * @param file
370      *            the file to be uploaded (if not Multipart mode, only the filename will be included)
371      * @param filename
372      *            the filename to use for this File part, empty String will be ignored by
373      *            the encoder
374      * @param contentType
375      *            the associated contentType for the File
376      * @param isText
377      *            True if this file should be transmitted in Text format (else binary)
378      * @throws NullPointerException
379      *             for name and file
380      * @throws ErrorDataEncoderException
381      *             if the encoding is in error or if the finalize were already done
382      */
383     public void addBodyFileUpload(String name, String filename, File file, String contentType, boolean isText)
384             throws ErrorDataEncoderException {
385         checkNotNull(name, "name");
386         checkNotNull(file, "file");
387         if (filename == null) {
388             filename = StringUtil.EMPTY_STRING;
389         }
390         String scontentType = contentType;
391         String contentTransferEncoding = null;
392         if (contentType == null) {
393             if (isText) {
394                 scontentType = HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE;
395             } else {
396                 scontentType = HttpPostBodyUtil.DEFAULT_BINARY_CONTENT_TYPE;
397             }
398         }
399         if (!isText) {
400             contentTransferEncoding = HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value();
401         }
402         FileUpload fileUpload = factory.createFileUpload(request, name, filename, scontentType,
403                 contentTransferEncoding, null, file.length());
404         try {
405             fileUpload.setContent(file);
406         } catch (IOException e) {
407             throw new ErrorDataEncoderException(e);
408         }
409         addBodyHttpData(fileUpload);
410     }
411 
412     /**
413      * Add a series of Files associated with one File parameter
414      *
415      * @param name
416      *            the name of the parameter
417      * @param file
418      *            the array of files
419      * @param contentType
420      *            the array of content Types associated with each file
421      * @param isText
422      *            the array of isText attribute (False meaning binary mode) for each file
423      * @throws IllegalArgumentException
424      *             also throws if array have different sizes
425      * @throws ErrorDataEncoderException
426      *             if the encoding is in error or if the finalize were already done
427      */
428     public void addBodyFileUploads(String name, File[] file, String[] contentType, boolean[] isText)
429             throws ErrorDataEncoderException {
430         if (file.length != contentType.length && file.length != isText.length) {
431             throw new IllegalArgumentException("Different array length");
432         }
433         for (int i = 0; i < file.length; i++) {
434             addBodyFileUpload(name, file[i], contentType[i], isText[i]);
435         }
436     }
437 
438     /**
439      * Add the InterfaceHttpData to the Body list
440      *
441      * @throws NullPointerException
442      *             for data
443      * @throws ErrorDataEncoderException
444      *             if the encoding is in error or if the finalize were already done
445      */
446     public void addBodyHttpData(InterfaceHttpData data) throws ErrorDataEncoderException {
447         if (headerFinalized) {
448             throw new ErrorDataEncoderException("Cannot add value once finalized");
449         }
450         bodyListDatas.add(checkNotNull(data, "data"));
451         if (!isMultipart) {
452             if (data instanceof Attribute) {
453                 Attribute attribute = (Attribute) data;
454                 try {
455                     // name=value& with encoded name and attribute
456                     String key = encodeAttribute(attribute.getName(), charset);
457                     String value = encodeAttribute(attribute.getValue(), charset);
458                     Attribute newattribute = factory.createAttribute(request, key, value);
459                     multipartHttpDatas.add(newattribute);
460                     globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1;
461                 } catch (IOException e) {
462                     throw new ErrorDataEncoderException(e);
463                 }
464             } else if (data instanceof FileUpload) {
465                 // since not Multipart, only name=filename => Attribute
466                 FileUpload fileUpload = (FileUpload) data;
467                 // name=filename& with encoded name and filename
468                 String key = encodeAttribute(fileUpload.getName(), charset);
469                 String value = encodeAttribute(fileUpload.getFilename(), charset);
470                 Attribute newattribute = factory.createAttribute(request, key, value);
471                 multipartHttpDatas.add(newattribute);
472                 globalBodySize += newattribute.getName().length() + 1 + newattribute.length() + 1;
473             }
474             return;
475         }
476         /*
477          * Logic:
478          * if not Attribute:
479          *      add Data to body list
480          *      if (duringMixedMode)
481          *          add endmixedmultipart delimiter
482          *          currentFileUpload = null
483          *          duringMixedMode = false;
484          *      add multipart delimiter, multipart body header and Data to multipart list
485          *      reset currentFileUpload, duringMixedMode
486          * if FileUpload: take care of multiple file for one field => mixed mode
487          *      if (duringMixedMode)
488          *          if (currentFileUpload.name == data.name)
489          *              add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
490          *          else
491          *              add endmixedmultipart delimiter, multipart body header and Data to multipart list
492          *              currentFileUpload = data
493          *              duringMixedMode = false;
494          *      else
495          *          if (currentFileUpload.name == data.name)
496          *              change multipart body header of previous file into multipart list to
497          *                      mixedmultipart start, mixedmultipart body header
498          *              add mixedmultipart delimiter, mixedmultipart body header and Data to multipart list
499          *              duringMixedMode = true
500          *          else
501          *              add multipart delimiter, multipart body header and Data to multipart list
502          *              currentFileUpload = data
503          *              duringMixedMode = false;
504          * Do not add last delimiter! Could be:
505          * if duringmixedmode: endmixedmultipart + endmultipart
506          * else only endmultipart
507          */
508         if (data instanceof Attribute) {
509             if (duringMixedMode) {
510                 InternalAttribute internal = new InternalAttribute(charset);
511                 internal.addValue("\r\n--" + multipartMixedBoundary + "--");
512                 multipartHttpDatas.add(internal);
513                 multipartMixedBoundary = null;
514                 currentFileUpload = null;
515                 duringMixedMode = false;
516             }
517             InternalAttribute internal = new InternalAttribute(charset);
518             if (!multipartHttpDatas.isEmpty()) {
519                 // previously a data field so CRLF
520                 internal.addValue("\r\n");
521             }
522             internal.addValue("--" + multipartDataBoundary + "\r\n");
523             // content-disposition: form-data; name="field1"
524             Attribute attribute = (Attribute) data;
525             internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
526                     + HttpHeaderValues.NAME + "=\"" + attribute.getName() + "\"\r\n");
527             // Add Content-Length: xxx
528             internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " +
529                     attribute.length() + "\r\n");
530             Charset localcharset = attribute.getCharset();
531             if (localcharset != null) {
532                 // Content-Type: text/plain; charset=charset
533                 internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " +
534                         HttpPostBodyUtil.DEFAULT_TEXT_CONTENT_TYPE + "; " +
535                         HttpHeaderValues.CHARSET + '='
536                         + localcharset.name() + "\r\n");
537             }
538             // CRLF between body header and data
539             internal.addValue("\r\n");
540             multipartHttpDatas.add(internal);
541             multipartHttpDatas.add(data);
542             globalBodySize += attribute.length() + internal.size();
543         } else if (data instanceof FileUpload) {
544             FileUpload fileUpload = (FileUpload) data;
545             InternalAttribute internal = new InternalAttribute(charset);
546             if (!multipartHttpDatas.isEmpty()) {
547                 // previously a data field so CRLF
548                 internal.addValue("\r\n");
549             }
550             boolean localMixed;
551             if (duringMixedMode) {
552                 if (currentFileUpload != null && currentFileUpload.getName().equals(fileUpload.getName())) {
553                     // continue a mixed mode
554 
555                     localMixed = true;
556                 } else {
557                     // end a mixed mode
558 
559                     // add endmixedmultipart delimiter, multipart body header
560                     // and
561                     // Data to multipart list
562                     internal.addValue("--" + multipartMixedBoundary + "--");
563                     multipartHttpDatas.add(internal);
564                     multipartMixedBoundary = null;
565                     // start a new one (could be replaced if mixed start again
566                     // from here
567                     internal = new InternalAttribute(charset);
568                     internal.addValue("\r\n");
569                     localMixed = false;
570                     // new currentFileUpload and no more in Mixed mode
571                     currentFileUpload = fileUpload;
572                     duringMixedMode = false;
573                 }
574             } else {
575                 if (encoderMode != EncoderMode.HTML5 && currentFileUpload != null
576                         && currentFileUpload.getName().equals(fileUpload.getName())) {
577                     // create a new mixed mode (from previous file)
578 
579                     // change multipart body header of previous file into
580                     // multipart list to
581                     // mixedmultipart start, mixedmultipart body header
582 
583                     // change Internal (size()-2 position in multipartHttpDatas)
584                     // from (line starting with *)
585                     // --AaB03x
586                     // * Content-Disposition: form-data; name="files";
587                     // filename="file1.txt"
588                     // Content-Type: text/plain
589                     // to (lines starting with *)
590                     // --AaB03x
591                     // * Content-Disposition: form-data; name="files"
592                     // * Content-Type: multipart/mixed; boundary=BbC04y
593                     // *
594                     // * --BbC04y
595                     // * Content-Disposition: attachment; filename="file1.txt"
596                     // Content-Type: text/plain
597                     initMixedMultipart();
598                     InternalAttribute pastAttribute = (InternalAttribute) multipartHttpDatas.get(multipartHttpDatas
599                             .size() - 2);
600                     // remove past size
601                     globalBodySize -= pastAttribute.size();
602                     StringBuilder replacement = new StringBuilder(
603                             139 + multipartDataBoundary.length() + multipartMixedBoundary.length() * 2 +
604                                     fileUpload.getFilename().length() + fileUpload.getName().length())
605 
606                         .append("--")
607                         .append(multipartDataBoundary)
608                         .append("\r\n")
609 
610                         .append(HttpHeaderNames.CONTENT_DISPOSITION)
611                         .append(": ")
612                         .append(HttpHeaderValues.FORM_DATA)
613                         .append("; ")
614                         .append(HttpHeaderValues.NAME)
615                         .append("=\"")
616                         .append(fileUpload.getName())
617                         .append("\"\r\n")
618 
619                         .append(HttpHeaderNames.CONTENT_TYPE)
620                         .append(": ")
621                         .append(HttpHeaderValues.MULTIPART_MIXED)
622                         .append("; ")
623                         .append(HttpHeaderValues.BOUNDARY)
624                         .append('=')
625                         .append(multipartMixedBoundary)
626                         .append("\r\n\r\n")
627 
628                         .append("--")
629                         .append(multipartMixedBoundary)
630                         .append("\r\n")
631 
632                         .append(HttpHeaderNames.CONTENT_DISPOSITION)
633                         .append(": ")
634                         .append(HttpHeaderValues.ATTACHMENT);
635 
636                     if (!fileUpload.getFilename().isEmpty()) {
637                         replacement.append("; ")
638                                    .append(HttpHeaderValues.FILENAME)
639                                    .append("=\"")
640                                    .append(fileUpload.getFilename())
641                                    .append('"');
642                     }
643 
644                     replacement.append("\r\n");
645 
646                     pastAttribute.setValue(replacement.toString(), 1);
647                     pastAttribute.setValue("", 2);
648 
649                     // update past size
650                     globalBodySize += pastAttribute.size();
651 
652                     // now continue
653                     // add mixedmultipart delimiter, mixedmultipart body header
654                     // and
655                     // Data to multipart list
656                     localMixed = true;
657                     duringMixedMode = true;
658                 } else {
659                     // a simple new multipart
660                     // add multipart delimiter, multipart body header and Data
661                     // to multipart list
662                     localMixed = false;
663                     currentFileUpload = fileUpload;
664                     duringMixedMode = false;
665                 }
666             }
667 
668             if (localMixed) {
669                 // add mixedmultipart delimiter, mixedmultipart body header and
670                 // Data to multipart list
671                 internal.addValue("--" + multipartMixedBoundary + "\r\n");
672 
673                 if (fileUpload.getFilename().isEmpty()) {
674                     // Content-Disposition: attachment
675                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": "
676                             + HttpHeaderValues.ATTACHMENT + "\r\n");
677                 } else {
678                     // Content-Disposition: attachment; filename="file1.txt"
679                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": "
680                             + HttpHeaderValues.ATTACHMENT + "; "
681                             + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n");
682                 }
683             } else {
684                 internal.addValue("--" + multipartDataBoundary + "\r\n");
685 
686                 if (fileUpload.getFilename().isEmpty()) {
687                     // Content-Disposition: form-data; name="files";
688                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
689                             + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"\r\n");
690                 } else {
691                     // Content-Disposition: form-data; name="files";
692                     // filename="file1.txt"
693                     internal.addValue(HttpHeaderNames.CONTENT_DISPOSITION + ": " + HttpHeaderValues.FORM_DATA + "; "
694                             + HttpHeaderValues.NAME + "=\"" + fileUpload.getName() + "\"; "
695                             + HttpHeaderValues.FILENAME + "=\"" + fileUpload.getFilename() + "\"\r\n");
696                 }
697             }
698             // Add Content-Length: xxx
699             internal.addValue(HttpHeaderNames.CONTENT_LENGTH + ": " +
700                     fileUpload.length() + "\r\n");
701             // Content-Type: image/gif
702             // Content-Type: text/plain; charset=ISO-8859-1
703             // Content-Transfer-Encoding: binary
704             internal.addValue(HttpHeaderNames.CONTENT_TYPE + ": " + fileUpload.getContentType());
705             String contentTransferEncoding = fileUpload.getContentTransferEncoding();
706             if (contentTransferEncoding != null
707                     && contentTransferEncoding.equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) {
708                 internal.addValue("\r\n" + HttpHeaderNames.CONTENT_TRANSFER_ENCODING + ": "
709                         + HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value() + "\r\n\r\n");
710             } else if (fileUpload.getCharset() != null) {
711                 internal.addValue("; " + HttpHeaderValues.CHARSET + '=' + fileUpload.getCharset().name() + "\r\n\r\n");
712             } else {
713                 internal.addValue("\r\n\r\n");
714             }
715             multipartHttpDatas.add(internal);
716             multipartHttpDatas.add(data);
717             globalBodySize += fileUpload.length() + internal.size();
718         }
719     }
720 
721     /**
722      * Iterator to be used when encoding will be called chunk after chunk
723      */
724     private ListIterator<InterfaceHttpData> iterator;
725 
726     /**
727      * Finalize the request by preparing the Header in the request and returns the request ready to be sent.<br>
728      * Once finalized, no data must be added.<br>
729      * If the request does not need chunk (isChunked() == false), this request is the only object to send to the remote
730      * server.
731      *
732      * @return the request object (chunked or not according to size of body)
733      * @throws ErrorDataEncoderException
734      *             if the encoding is in error or if the finalize were already done
735      */
736     public HttpRequest finalizeRequest() throws ErrorDataEncoderException {
737         // Finalize the multipartHttpDatas
738         if (!headerFinalized) {
739             if (isMultipart) {
740                 InternalAttribute internal = new InternalAttribute(charset);
741                 if (duringMixedMode) {
742                     internal.addValue("\r\n--" + multipartMixedBoundary + "--");
743                 }
744                 internal.addValue("\r\n--" + multipartDataBoundary + "--\r\n");
745                 multipartHttpDatas.add(internal);
746                 multipartMixedBoundary = null;
747                 currentFileUpload = null;
748                 duringMixedMode = false;
749                 globalBodySize += internal.size();
750             }
751             headerFinalized = true;
752         } else {
753             throw new ErrorDataEncoderException("Header already encoded");
754         }
755 
756         HttpHeaders headers = request.headers();
757         List<String> contentTypes = headers.getAll(HttpHeaderNames.CONTENT_TYPE);
758         List<String> transferEncoding = headers.getAll(HttpHeaderNames.TRANSFER_ENCODING);
759         if (contentTypes != null) {
760             headers.remove(HttpHeaderNames.CONTENT_TYPE);
761             for (String contentType : contentTypes) {
762                 // "multipart/form-data; boundary=--89421926422648"
763                 String lowercased = contentType.toLowerCase();
764                 if (lowercased.startsWith(HttpHeaderValues.MULTIPART_FORM_DATA.toString()) ||
765                         lowercased.startsWith(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString())) {
766                     // ignore
767                 } else {
768                     headers.add(HttpHeaderNames.CONTENT_TYPE, contentType);
769                 }
770             }
771         }
772         if (isMultipart) {
773             String value = HttpHeaderValues.MULTIPART_FORM_DATA + "; " + HttpHeaderValues.BOUNDARY + '='
774                     + multipartDataBoundary;
775             headers.add(HttpHeaderNames.CONTENT_TYPE, value);
776         } else {
777             // Not multipart
778             headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED);
779         }
780         // Now consider size for chunk or not
781         long realSize = globalBodySize;
782         if (isMultipart) {
783             iterator = multipartHttpDatas.listIterator();
784         } else {
785             realSize -= 1; // last '&' removed
786             iterator = multipartHttpDatas.listIterator();
787         }
788         headers.set(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(realSize));
789         if (realSize > HttpPostBodyUtil.chunkSize || isMultipart) {
790             isChunked = true;
791             if (transferEncoding != null) {
792                 headers.remove(HttpHeaderNames.TRANSFER_ENCODING);
793                 for (CharSequence v : transferEncoding) {
794                     if (HttpHeaderValues.CHUNKED.contentEqualsIgnoreCase(v)) {
795                         // ignore
796                     } else {
797                         headers.add(HttpHeaderNames.TRANSFER_ENCODING, v);
798                     }
799                 }
800             }
801             HttpUtil.setTransferEncodingChunked(request, true);
802 
803             // wrap to hide the possible content
804             return new WrappedHttpRequest(request);
805         } else {
806             // get the only one body and set it to the request
807             HttpContent chunk = nextChunk();
808             if (request instanceof FullHttpRequest) {
809                 FullHttpRequest fullRequest = (FullHttpRequest) request;
810                 ByteBuf chunkContent = chunk.content();
811                 if (fullRequest.content() != chunkContent) {
812                     fullRequest.content().clear().writeBytes(chunkContent);
813                     chunkContent.release();
814                 }
815                 return fullRequest;
816             } else {
817                 return new WrappedFullHttpRequest(request, chunk);
818             }
819         }
820     }
821 
822     /**
823      * @return True if the request is by Chunk
824      */
825     public boolean isChunked() {
826         return isChunked;
827     }
828 
829     /**
830      * Encode one attribute
831      *
832      * @return the encoded attribute
833      * @throws ErrorDataEncoderException
834      *             if the encoding is in error
835      */
836     @SuppressWarnings("unchecked")
837     private String encodeAttribute(String s, Charset charset) throws ErrorDataEncoderException {
838         if (s == null) {
839             return "";
840         }
841         try {
842             String encoded = URLEncoder.encode(s, charset.name());
843             if (encoderMode == EncoderMode.RFC3986) {
844                 for (Map.Entry<Pattern, String> entry : percentEncodings) {
845                     String replacement = entry.getValue();
846                     encoded = entry.getKey().matcher(encoded).replaceAll(replacement);
847                 }
848             }
849             return encoded;
850         } catch (UnsupportedEncodingException e) {
851             throw new ErrorDataEncoderException(charset.name(), e);
852         }
853     }
854 
855     /**
856      * The ByteBuf currently used by the encoder
857      */
858     private ByteBuf currentBuffer;
859     /**
860      * The current InterfaceHttpData to encode (used if more chunks are available)
861      */
862     private InterfaceHttpData currentData;
863     /**
864      * If not multipart, does the currentBuffer stands for the Key or for the Value
865      */
866     private boolean isKey = true;
867 
868     /**
869      *
870      * @return the next ByteBuf to send as a HttpChunk and modifying currentBuffer accordingly
871      */
872     private ByteBuf fillByteBuf() {
873         int length = currentBuffer.readableBytes();
874         if (length > HttpPostBodyUtil.chunkSize) {
875             return currentBuffer.readRetainedSlice(HttpPostBodyUtil.chunkSize);
876         } else {
877             // to continue
878             ByteBuf slice = currentBuffer;
879             currentBuffer = null;
880             return slice;
881         }
882     }
883 
884     /**
885      * From the current context (currentBuffer and currentData), returns the next HttpChunk (if possible) trying to get
886      * sizeleft bytes more into the currentBuffer. This is the Multipart version.
887      *
888      * @param sizeleft
889      *            the number of bytes to try to get from currentData
890      * @return the next HttpChunk or null if not enough bytes were found
891      * @throws ErrorDataEncoderException
892      *             if the encoding is in error
893      */
894     private HttpContent encodeNextChunkMultipart(int sizeleft) throws ErrorDataEncoderException {
895         if (currentData == null) {
896             return null;
897         }
898         ByteBuf buffer;
899         if (currentData instanceof InternalAttribute) {
900             buffer = ((InternalAttribute) currentData).toByteBuf();
901             currentData = null;
902         } else {
903             try {
904                 buffer = ((HttpData) currentData).getChunk(sizeleft);
905             } catch (IOException e) {
906                 throw new ErrorDataEncoderException(e);
907             }
908             if (buffer.capacity() == 0) {
909                 // end for current InterfaceHttpData, need more data
910                 currentData = null;
911                 return null;
912             }
913         }
914         if (currentBuffer == null) {
915             currentBuffer = buffer;
916         } else {
917             currentBuffer = wrappedBuffer(currentBuffer, buffer);
918         }
919         if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
920             currentData = null;
921             return null;
922         }
923         buffer = fillByteBuf();
924         return new DefaultHttpContent(buffer);
925     }
926 
927     /**
928      * From the current context (currentBuffer and currentData), returns the next HttpChunk (if possible) trying to get
929      * sizeleft bytes more into the currentBuffer. This is the UrlEncoded version.
930      *
931      * @param sizeleft
932      *            the number of bytes to try to get from currentData
933      * @return the next HttpChunk or null if not enough bytes were found
934      * @throws ErrorDataEncoderException
935      *             if the encoding is in error
936      */
937     private HttpContent encodeNextChunkUrlEncoded(int sizeleft) throws ErrorDataEncoderException {
938         if (currentData == null) {
939             return null;
940         }
941         int size = sizeleft;
942         ByteBuf buffer;
943 
944         // Set name=
945         if (isKey) {
946             String key = currentData.getName();
947             buffer = wrappedBuffer(key.getBytes());
948             isKey = false;
949             if (currentBuffer == null) {
950                 currentBuffer = wrappedBuffer(buffer, wrappedBuffer("=".getBytes()));
951                 // continue
952                 size -= buffer.readableBytes() + 1;
953             } else {
954                 currentBuffer = wrappedBuffer(currentBuffer, buffer, wrappedBuffer("=".getBytes()));
955                 // continue
956                 size -= buffer.readableBytes() + 1;
957             }
958             if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
959                 buffer = fillByteBuf();
960                 return new DefaultHttpContent(buffer);
961             }
962         }
963 
964         // Put value into buffer
965         try {
966             buffer = ((HttpData) currentData).getChunk(size);
967         } catch (IOException e) {
968             throw new ErrorDataEncoderException(e);
969         }
970 
971         // Figure out delimiter
972         ByteBuf delimiter = null;
973         if (buffer.readableBytes() < size) {
974             isKey = true;
975             delimiter = iterator.hasNext() ? wrappedBuffer("&".getBytes()) : null;
976         }
977 
978         // End for current InterfaceHttpData, need potentially more data
979         if (buffer.capacity() == 0) {
980             currentData = null;
981             if (currentBuffer == null) {
982                 currentBuffer = delimiter;
983             } else {
984                 if (delimiter != null) {
985                     currentBuffer = wrappedBuffer(currentBuffer, delimiter);
986                 }
987             }
988             if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
989                 buffer = fillByteBuf();
990                 return new DefaultHttpContent(buffer);
991             }
992             return null;
993         }
994 
995         // Put it all together: name=value&
996         if (currentBuffer == null) {
997             if (delimiter != null) {
998                 currentBuffer = wrappedBuffer(buffer, delimiter);
999             } else {
1000                 currentBuffer = buffer;
1001             }
1002         } else {
1003             if (delimiter != null) {
1004                 currentBuffer = wrappedBuffer(currentBuffer, buffer, delimiter);
1005             } else {
1006                 currentBuffer = wrappedBuffer(currentBuffer, buffer);
1007             }
1008         }
1009 
1010         // end for current InterfaceHttpData, need more data
1011         if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
1012             currentData = null;
1013             isKey = true;
1014             return null;
1015         }
1016 
1017         buffer = fillByteBuf();
1018         return new DefaultHttpContent(buffer);
1019     }
1020 
1021     @Override
1022     public void close() throws Exception {
1023         // NO since the user can want to reuse (broadcast for instance)
1024         // cleanFiles();
1025     }
1026 
1027     @Deprecated
1028     @Override
1029     public HttpContent readChunk(ChannelHandlerContext ctx) throws Exception {
1030         return readChunk(ctx.alloc());
1031     }
1032 
1033     /**
1034      * Returns the next available HttpChunk. The caller is responsible to test if this chunk is the last one (isLast()),
1035      * in order to stop calling this getMethod.
1036      *
1037      * @return the next available HttpChunk
1038      * @throws ErrorDataEncoderException
1039      *             if the encoding is in error
1040      */
1041     @Override
1042     public HttpContent readChunk(ByteBufAllocator allocator) throws Exception {
1043         if (isLastChunkSent) {
1044             return null;
1045         } else {
1046             HttpContent nextChunk = nextChunk();
1047             globalProgress += nextChunk.content().readableBytes();
1048             return nextChunk;
1049         }
1050     }
1051 
1052     /**
1053      * Returns the next available HttpChunk. The caller is responsible to test if this chunk is the last one (isLast()),
1054      * in order to stop calling this getMethod.
1055      *
1056      * @return the next available HttpChunk
1057      * @throws ErrorDataEncoderException
1058      *             if the encoding is in error
1059      */
1060     private HttpContent nextChunk() throws ErrorDataEncoderException {
1061         if (isLastChunk) {
1062             isLastChunkSent = true;
1063             return LastHttpContent.EMPTY_LAST_CONTENT;
1064         }
1065         // first test if previous buffer is not empty
1066         int size = calculateRemainingSize();
1067         if (size <= 0) {
1068             // NextChunk from buffer
1069             ByteBuf buffer = fillByteBuf();
1070             return new DefaultHttpContent(buffer);
1071         }
1072         // size > 0
1073         if (currentData != null) {
1074             // continue to read data
1075             HttpContent chunk;
1076             if (isMultipart) {
1077                 chunk = encodeNextChunkMultipart(size);
1078             } else {
1079                 chunk = encodeNextChunkUrlEncoded(size);
1080             }
1081             if (chunk != null) {
1082                 // NextChunk from data
1083                 return chunk;
1084             }
1085             size = calculateRemainingSize();
1086         }
1087         if (!iterator.hasNext()) {
1088             return lastChunk();
1089         }
1090         while (size > 0 && iterator.hasNext()) {
1091             currentData = iterator.next();
1092             HttpContent chunk;
1093             if (isMultipart) {
1094                 chunk = encodeNextChunkMultipart(size);
1095             } else {
1096                 chunk = encodeNextChunkUrlEncoded(size);
1097             }
1098             if (chunk == null) {
1099                 // not enough
1100                 size = calculateRemainingSize();
1101                 continue;
1102             }
1103             // NextChunk from data
1104             return chunk;
1105         }
1106         // end since no more data
1107         return lastChunk();
1108     }
1109 
1110     private int calculateRemainingSize() {
1111         int size = HttpPostBodyUtil.chunkSize;
1112         if (currentBuffer != null) {
1113             size -= currentBuffer.readableBytes();
1114         }
1115         return size;
1116     }
1117 
1118     private HttpContent lastChunk() {
1119         isLastChunk = true;
1120         if (currentBuffer == null) {
1121             isLastChunkSent = true;
1122             // LastChunk with no more data
1123             return LastHttpContent.EMPTY_LAST_CONTENT;
1124         }
1125         // NextChunk as last non empty from buffer
1126         ByteBuf buffer = currentBuffer;
1127         currentBuffer = null;
1128         return new DefaultHttpContent(buffer);
1129     }
1130 
1131     @Override
1132     public boolean isEndOfInput() throws Exception {
1133         return isLastChunkSent;
1134     }
1135 
1136     @Override
1137     public long length() {
1138         return isMultipart? globalBodySize : globalBodySize - 1;
1139     }
1140 
1141     @Override
1142     public long progress() {
1143         return globalProgress;
1144     }
1145 
1146     /**
1147      * Exception when an error occurs while encoding
1148      */
1149     public static class ErrorDataEncoderException extends Exception {
1150         private static final long serialVersionUID = 5020247425493164465L;
1151 
1152         public ErrorDataEncoderException() {
1153         }
1154 
1155         public ErrorDataEncoderException(String msg) {
1156             super(msg);
1157         }
1158 
1159         public ErrorDataEncoderException(Throwable cause) {
1160             super(cause);
1161         }
1162 
1163         public ErrorDataEncoderException(String msg, Throwable cause) {
1164             super(msg, cause);
1165         }
1166     }
1167 
1168     private static class WrappedHttpRequest implements HttpRequest {
1169         private final HttpRequest request;
1170         WrappedHttpRequest(HttpRequest request) {
1171             this.request = request;
1172         }
1173 
1174         @Override
1175         public HttpRequest setProtocolVersion(HttpVersion version) {
1176             request.setProtocolVersion(version);
1177             return this;
1178         }
1179 
1180         @Override
1181         public HttpRequest setMethod(HttpMethod method) {
1182             request.setMethod(method);
1183             return this;
1184         }
1185 
1186         @Override
1187         public HttpRequest setUri(String uri) {
1188             request.setUri(uri);
1189             return this;
1190         }
1191 
1192         @Override
1193         public HttpMethod getMethod() {
1194             return request.method();
1195         }
1196 
1197         @Override
1198         public HttpMethod method() {
1199             return request.method();
1200         }
1201 
1202         @Override
1203         public String getUri() {
1204             return request.uri();
1205         }
1206 
1207         @Override
1208         public String uri() {
1209             return request.uri();
1210         }
1211 
1212         @Override
1213         public HttpVersion getProtocolVersion() {
1214             return request.protocolVersion();
1215         }
1216 
1217         @Override
1218         public HttpVersion protocolVersion() {
1219             return request.protocolVersion();
1220         }
1221 
1222         @Override
1223         public HttpHeaders headers() {
1224             return request.headers();
1225         }
1226 
1227         @Override
1228         public DecoderResult decoderResult() {
1229             return request.decoderResult();
1230         }
1231 
1232         @Override
1233         @Deprecated
1234         public DecoderResult getDecoderResult() {
1235             return request.getDecoderResult();
1236         }
1237 
1238         @Override
1239         public void setDecoderResult(DecoderResult result) {
1240             request.setDecoderResult(result);
1241         }
1242     }
1243 
1244     private static final class WrappedFullHttpRequest extends WrappedHttpRequest implements FullHttpRequest {
1245         private final HttpContent content;
1246 
1247         private WrappedFullHttpRequest(HttpRequest request, HttpContent content) {
1248             super(request);
1249             this.content = content;
1250         }
1251 
1252         @Override
1253         public FullHttpRequest setProtocolVersion(HttpVersion version) {
1254             super.setProtocolVersion(version);
1255             return this;
1256         }
1257 
1258         @Override
1259         public FullHttpRequest setMethod(HttpMethod method) {
1260             super.setMethod(method);
1261             return this;
1262         }
1263 
1264         @Override
1265         public FullHttpRequest setUri(String uri) {
1266             super.setUri(uri);
1267             return this;
1268         }
1269 
1270         @Override
1271         public FullHttpRequest copy() {
1272             return replace(content().copy());
1273         }
1274 
1275         @Override
1276         public FullHttpRequest duplicate() {
1277             return replace(content().duplicate());
1278         }
1279 
1280         @Override
1281         public FullHttpRequest retainedDuplicate() {
1282             return replace(content().retainedDuplicate());
1283         }
1284 
1285         @Override
1286         public FullHttpRequest replace(ByteBuf content) {
1287             DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest(protocolVersion(), method(), uri(), content);
1288             duplicate.headers().set(headers());
1289             duplicate.trailingHeaders().set(trailingHeaders());
1290             return duplicate;
1291         }
1292 
1293         @Override
1294         public FullHttpRequest retain(int increment) {
1295             content.retain(increment);
1296             return this;
1297         }
1298 
1299         @Override
1300         public FullHttpRequest retain() {
1301             content.retain();
1302             return this;
1303         }
1304 
1305         @Override
1306         public FullHttpRequest touch() {
1307             content.touch();
1308             return this;
1309         }
1310 
1311         @Override
1312         public FullHttpRequest touch(Object hint) {
1313             content.touch(hint);
1314             return this;
1315         }
1316 
1317         @Override
1318         public ByteBuf content() {
1319             return content.content();
1320         }
1321 
1322         @Override
1323         public HttpHeaders trailingHeaders() {
1324             if (content instanceof LastHttpContent) {
1325                 return ((LastHttpContent) content).trailingHeaders();
1326             } else {
1327                 return EmptyHttpHeaders.INSTANCE;
1328             }
1329         }
1330 
1331         @Override
1332         public int refCnt() {
1333             return content.refCnt();
1334         }
1335 
1336         @Override
1337         public boolean release() {
1338             return content.release();
1339         }
1340 
1341         @Override
1342         public boolean release(int decrement) {
1343             return content.release(decrement);
1344         }
1345     }
1346 }