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