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