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