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