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(charset);
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(charset);
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 + "=\"" + attribute.getName() + "\"\r\n");
461             Charset localcharset = attribute.getCharset();
462             if (localcharset != null) {
463                 // Content-Type: charset=charset
464                 internal.addValue(HttpHeaders.Names.CONTENT_TYPE + ": " +
465                         HttpPostBodyUtil.DEFAULT_TEXT_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(charset);
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(charset);
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                         fileUpload.getName() + "\"\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                         fileUpload.getFilename() + "\"\r\n";
539                     pastAttribute.setValue(replacement, 1);
540                     // update past size
541                     globalBodySize += pastAttribute.size();
542 
543                     // now continue
544                     // add mixedmultipart delimiter, mixedmultipart body header and
545                     // Data to multipart list
546                     localMixed = true;
547                     duringMixedMode = true;
548                 } else {
549                     // a simple new multipart
550                     //add multipart delimiter, multipart body header and Data to multipart list
551                     localMixed = false;
552                     currentFileUpload = fileUpload;
553                     duringMixedMode = false;
554                 }
555             }
556 
557             if (localMixed) {
558                 // add mixedmultipart delimiter, mixedmultipart body header and
559                 // Data to multipart list
560                 internal.addValue("--" + multipartMixedBoundary + "\r\n");
561                 // Content-Disposition: file; filename="file1.txt"
562                 internal.addValue(HttpPostBodyUtil.CONTENT_DISPOSITION + ": " +
563                         HttpPostBodyUtil.FILE + "; " + HttpPostBodyUtil.FILENAME + "=\"" +
564                         fileUpload.getFilename() +
565                         "\"\r\n");
566             } else {
567                 internal.addValue("--" + multipartDataBoundary + "\r\n");
568                 // Content-Disposition: form-data; name="files"; filename="file1.txt"
569                 internal.addValue(HttpPostBodyUtil.CONTENT_DISPOSITION + ": " +
570                         HttpPostBodyUtil.FORM_DATA + "; " + HttpPostBodyUtil.NAME + "=\"" +
571                         fileUpload.getName() + "\"; " +
572                         HttpPostBodyUtil.FILENAME + "=\"" +
573                         fileUpload.getFilename() +
574                         "\"\r\n");
575             }
576             // Content-Type: image/gif
577             // Content-Type: text/plain; charset=ISO-8859-1
578             // Content-Transfer-Encoding: binary
579             internal.addValue(HttpHeaders.Names.CONTENT_TYPE + ": " +
580                     fileUpload.getContentType());
581             String contentTransferEncoding = fileUpload.getContentTransferEncoding();
582             if (contentTransferEncoding != null &&
583                     contentTransferEncoding.equals(
584                             HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) {
585                 internal.addValue("\r\n" + HttpHeaders.Names.CONTENT_TRANSFER_ENCODING +
586                         ": " + HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value() +
587                         "\r\n\r\n");
588             } else if (fileUpload.getCharset() != null) {
589                 internal.addValue("; " + HttpHeaders.Values.CHARSET + '=' +
590                         fileUpload.getCharset() + "\r\n\r\n");
591             } else {
592                 internal.addValue("\r\n\r\n");
593             }
594             multipartHttpDatas.add(internal);
595             multipartHttpDatas.add(data);
596             globalBodySize += fileUpload.length() + internal.size();
597         }
598     }
599 
600     /**
601      * Iterator to be used when encoding will be called chunk after chunk
602      */
603     private ListIterator<InterfaceHttpData> iterator;
604 
605     /**
606      * Finalize the request by preparing the Header in the request and
607      * returns the request ready to be sent.<br>
608      * Once finalized, no data must be added.<br>
609      * If the request does not need chunk (isChunked() == false),
610      * this request is the only object to send to
611      * the remote server.
612      *
613      * @return the request object (chunked or not according to size of body)
614      * @throws ErrorDataEncoderException if the encoding is in error or if the finalize were already done
615      */
616     public HttpRequest finalizeRequest() throws ErrorDataEncoderException {
617         // Finalize the multipartHttpDatas
618         if (! headerFinalized) {
619             if (isMultipart) {
620                 InternalAttribute internal = new InternalAttribute(charset);
621                 if (duringMixedMode) {
622                     internal.addValue("\r\n--" + multipartMixedBoundary + "--");
623                 }
624                 internal.addValue("\r\n--" + multipartDataBoundary + "--\r\n");
625                 multipartHttpDatas.add(internal);
626                 multipartMixedBoundary = null;
627                 currentFileUpload = null;
628                 duringMixedMode = false;
629                 globalBodySize += internal.size();
630             }
631             headerFinalized = true;
632         } else {
633             throw new ErrorDataEncoderException("Header already encoded");
634         }
635         List<String> contentTypes = request.getHeaders(HttpHeaders.Names.CONTENT_TYPE);
636         List<String> transferEncoding =
637             request.getHeaders(HttpHeaders.Names.TRANSFER_ENCODING);
638         if (contentTypes != null) {
639             request.removeHeader(HttpHeaders.Names.CONTENT_TYPE);
640             for (String contentType: contentTypes) {
641                 // "multipart/form-data; boundary=--89421926422648"
642                 if (contentType.toLowerCase().startsWith(
643                         HttpHeaders.Values.MULTIPART_FORM_DATA)) {
644                     // ignore
645                 } else if (contentType.toLowerCase().startsWith(
646                         HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED)) {
647                     // ignore
648                 } else {
649                     request.addHeader(HttpHeaders.Names.CONTENT_TYPE, contentType);
650                 }
651             }
652         }
653         if (isMultipart) {
654             String value = HttpHeaders.Values.MULTIPART_FORM_DATA + "; " +
655                 HttpHeaders.Values.BOUNDARY + '=' + multipartDataBoundary;
656             request.addHeader(HttpHeaders.Names.CONTENT_TYPE, value);
657         } else {
658             // Not multipart
659             request.addHeader(HttpHeaders.Names.CONTENT_TYPE,
660                     HttpHeaders.Values.APPLICATION_X_WWW_FORM_URLENCODED);
661         }
662         // Now consider size for chunk or not
663         long realSize = globalBodySize;
664         if (isMultipart) {
665             iterator = multipartHttpDatas.listIterator();
666         } else {
667             realSize -= 1; // last '&' removed
668             iterator = multipartHttpDatas.listIterator();
669         }
670         request.setHeader(HttpHeaders.Names.CONTENT_LENGTH, String
671                 .valueOf(realSize));
672         if (realSize > HttpPostBodyUtil.chunkSize || isMultipart) {
673             isChunked = true;
674             if (transferEncoding != null) {
675                 request.removeHeader(HttpHeaders.Names.TRANSFER_ENCODING);
676                 for (String v: transferEncoding) {
677                     if (v.equalsIgnoreCase(HttpHeaders.Values.CHUNKED)) {
678                         // ignore
679                     } else {
680                         request.addHeader(HttpHeaders.Names.TRANSFER_ENCODING, v);
681                     }
682                 }
683             }
684             request.addHeader(HttpHeaders.Names.TRANSFER_ENCODING,
685                     HttpHeaders.Values.CHUNKED);
686             request.setContent(ChannelBuffers.EMPTY_BUFFER);
687         } else {
688             // get the only one body and set it to the request
689             HttpChunk chunk = nextChunk();
690             request.setContent(chunk.getContent());
691         }
692         return request;
693     }
694 
695     /**
696      * @return True if the request is by Chunk
697      */
698     public boolean isChunked() {
699         return isChunked;
700     }
701 
702     /**
703      * Encode one attribute
704      * @return the encoded attribute
705      * @throws ErrorDataEncoderException if the encoding is in error
706      */
707     private String encodeAttribute(String s, Charset charset)
708             throws ErrorDataEncoderException {
709         if (s == null) {
710             return "";
711         }
712         try {
713             String encoded = URLEncoder.encode(s, charset.name());
714             if (encoderMode == EncoderMode.RFC3986) {
715                 for (Map.Entry<Pattern, String> entry : percentEncodings.entrySet()) {
716                     String replacement = entry.getValue();
717                     encoded = entry.getKey().matcher(encoded).replaceAll(replacement);
718                 }
719             }
720             return encoded;
721         } catch (UnsupportedEncodingException e) {
722             throw new ErrorDataEncoderException(charset.name(), e);
723         }
724     }
725 
726     /**
727      * The ChannelBuffer currently used by the encoder
728      */
729     private ChannelBuffer currentBuffer;
730     /**
731      * The current InterfaceHttpData to encode (used if more chunks are available)
732      */
733     private InterfaceHttpData currentData;
734     /**
735      * If not multipart, does the currentBuffer stands for the Key or for the Value
736      */
737     private boolean isKey = true;
738 
739     /**
740      *
741      * @return the next ChannelBuffer to send as a HttpChunk and modifying currentBuffer
742      * accordingly
743      */
744     private ChannelBuffer fillChannelBuffer() {
745         int length = currentBuffer.readableBytes();
746         if (length > HttpPostBodyUtil.chunkSize) {
747             ChannelBuffer slice =
748                 currentBuffer.slice(currentBuffer.readerIndex(), HttpPostBodyUtil.chunkSize);
749             currentBuffer.skipBytes(HttpPostBodyUtil.chunkSize);
750             return slice;
751         } else {
752             // to continue
753             ChannelBuffer slice = currentBuffer;
754             currentBuffer = null;
755             return slice;
756         }
757     }
758 
759     /**
760      * From the current context (currentBuffer and currentData), returns the next HttpChunk
761      * (if possible) trying to get sizeleft bytes more into the currentBuffer.
762      * This is the Multipart version.
763      *
764      * @param sizeleft the number of bytes to try to get from currentData
765      * @return the next HttpChunk or null if not enough bytes were found
766      * @throws ErrorDataEncoderException if the encoding is in error
767      */
768     private HttpChunk encodeNextChunkMultipart(int sizeleft) throws ErrorDataEncoderException {
769         if (currentData == null) {
770             return null;
771         }
772         ChannelBuffer buffer;
773         if (currentData instanceof InternalAttribute) {
774 
775             buffer = ((InternalAttribute) currentData).toChannelBuffer();
776             currentData = null;
777         } else {
778             if (currentData instanceof Attribute) {
779                 try {
780                     buffer = ((Attribute) currentData).getChunk(sizeleft);
781                 } catch (IOException e) {
782                     throw new ErrorDataEncoderException(e);
783                 }
784             } else {
785                 try {
786                     buffer = ((HttpData) currentData).getChunk(sizeleft);
787                 } catch (IOException e) {
788                     throw new ErrorDataEncoderException(e);
789                 }
790             }
791             if (buffer.capacity() == 0) {
792                 // end for current InterfaceHttpData, need more data
793                 currentData = null;
794                 return null;
795             }
796         }
797         if (currentBuffer == null) {
798             currentBuffer = buffer;
799         } else {
800             currentBuffer = ChannelBuffers.wrappedBuffer(currentBuffer,
801                 buffer);
802         }
803         if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
804             currentData = null;
805             return null;
806         }
807         buffer = fillChannelBuffer();
808         return new DefaultHttpChunk(buffer);
809     }
810 
811     /**
812      * From the current context (currentBuffer and currentData), returns the next HttpChunk
813      * (if possible) trying to get sizeleft bytes more into the currentBuffer.
814      * This is the UrlEncoded version.
815      *
816      * @param sizeleft the number of bytes to try to get from currentData
817      * @return the next HttpChunk or null if not enough bytes were found
818      * @throws ErrorDataEncoderException if the encoding is in error
819      */
820     private HttpChunk encodeNextChunkUrlEncoded(int sizeleft) throws ErrorDataEncoderException {
821         if (currentData == null) {
822             return null;
823         }
824         int size = sizeleft;
825         ChannelBuffer buffer;
826         if (isKey) {
827             // get name
828             String key = currentData.getName();
829             buffer = ChannelBuffers.wrappedBuffer(key.getBytes());
830             isKey = false;
831             if (currentBuffer == null) {
832                 currentBuffer = ChannelBuffers.wrappedBuffer(
833                         buffer, ChannelBuffers.wrappedBuffer("=".getBytes()));
834                 //continue
835                 size -= buffer.readableBytes() + 1;
836             } else {
837                 currentBuffer = ChannelBuffers.wrappedBuffer(currentBuffer,
838                     buffer, ChannelBuffers.wrappedBuffer("=".getBytes()));
839                 //continue
840                 size -= buffer.readableBytes() + 1;
841             }
842             if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
843                 buffer = fillChannelBuffer();
844                 return new DefaultHttpChunk(buffer);
845             }
846         }
847         try {
848             buffer = ((HttpData) currentData).getChunk(size);
849         } catch (IOException e) {
850             throw new ErrorDataEncoderException(e);
851         }
852         ChannelBuffer delimiter = null;
853         if (buffer.readableBytes() < size) {
854             // delimiter
855             isKey = true;
856             delimiter = iterator.hasNext() ?
857                     ChannelBuffers.wrappedBuffer("&".getBytes()) :
858                         null;
859         }
860         if (buffer.capacity() == 0) {
861             // end for current InterfaceHttpData, need potentially more data
862             currentData = null;
863             if (currentBuffer == null) {
864                 currentBuffer = delimiter;
865             } else {
866                 if (delimiter != null) {
867                     currentBuffer = ChannelBuffers.wrappedBuffer(currentBuffer,
868                         delimiter);
869                 }
870             }
871             if (currentBuffer.readableBytes() >= HttpPostBodyUtil.chunkSize) {
872                 buffer = fillChannelBuffer();
873                 return new DefaultHttpChunk(buffer);
874             }
875             return null;
876         }
877         if (currentBuffer == null) {
878             if (delimiter != null) {
879                 currentBuffer = ChannelBuffers.wrappedBuffer(buffer,
880                     delimiter);
881             } else {
882                 currentBuffer = buffer;
883             }
884         } else {
885             if (delimiter != null) {
886                 currentBuffer = ChannelBuffers.wrappedBuffer(currentBuffer,
887                     buffer, delimiter);
888             } else {
889                 currentBuffer = ChannelBuffers.wrappedBuffer(currentBuffer,
890                         buffer);
891             }
892         }
893         if (currentBuffer.readableBytes() < HttpPostBodyUtil.chunkSize) {
894             // end for current InterfaceHttpData, need more data
895             currentData = null;
896             isKey = true;
897             return null;
898         }
899         buffer = fillChannelBuffer();
900         // size = 0
901         return new DefaultHttpChunk(buffer);
902     }
903 
904     public void close() throws Exception {
905         //NO since the user can want to reuse (broadcast for instance) cleanFiles();
906     }
907 
908     /**
909      * Returns the next available HttpChunk. The caller is responsible to test if this chunk is the
910      * last one (isLast()), in order to stop calling this method.
911      *
912      * @return the next available HttpChunk
913      * @throws ErrorDataEncoderException if the encoding is in error
914      */
915     public HttpChunk nextChunk() throws ErrorDataEncoderException {
916         if (isLastChunk) {
917             isLastChunkSent = true;
918             return new DefaultHttpChunk(ChannelBuffers.EMPTY_BUFFER);
919         }
920         ChannelBuffer buffer;
921         int size = HttpPostBodyUtil.chunkSize;
922         // first test if previous buffer is not empty
923         if (currentBuffer != null) {
924             size -= currentBuffer.readableBytes();
925         }
926         if (size <= 0) {
927             //NextChunk from buffer
928             buffer = fillChannelBuffer();
929             return new DefaultHttpChunk(buffer);
930         }
931         // size > 0
932         if (currentData != null) {
933             // continue to read data
934             if (isMultipart) {
935                 HttpChunk chunk = encodeNextChunkMultipart(size);
936                 if (chunk != null) {
937                     return chunk;
938                 }
939             } else {
940                 HttpChunk chunk = encodeNextChunkUrlEncoded(size);
941                 if (chunk != null) {
942                     //NextChunk Url from currentData
943                     return chunk;
944                 }
945             }
946             size = HttpPostBodyUtil.chunkSize - currentBuffer.readableBytes();
947         }
948         if (! iterator.hasNext()) {
949             isLastChunk = true;
950             //NextChunk as last non empty from buffer
951             buffer = currentBuffer;
952             currentBuffer = null;
953             return new DefaultHttpChunk(buffer);
954         }
955         while (size > 0 && iterator.hasNext()) {
956             currentData = iterator.next();
957             HttpChunk chunk;
958             if (isMultipart) {
959                 chunk = encodeNextChunkMultipart(size);
960             } else {
961                 chunk = encodeNextChunkUrlEncoded(size);
962             }
963             if (chunk == null) {
964                 // not enough
965                 size = HttpPostBodyUtil.chunkSize - currentBuffer.readableBytes();
966                 continue;
967             }
968             //NextChunk from data
969             return chunk;
970         }
971         // end since no more data
972         isLastChunk = true;
973         if (currentBuffer == null) {
974             isLastChunkSent = true;
975             //LastChunk with no more data
976             return new DefaultHttpChunk(ChannelBuffers.EMPTY_BUFFER);
977         }
978         //Previous LastChunk with no more data
979         buffer = currentBuffer;
980         currentBuffer = null;
981         return new DefaultHttpChunk(buffer);
982     }
983 
984     public boolean isEndOfInput() throws Exception {
985         return isLastChunkSent;
986     }
987 
988     public boolean hasNextChunk() throws Exception {
989       return !isLastChunkSent;
990     }
991 
992     /**
993      * Exception when an error occurs while encoding
994      */
995     public static class ErrorDataEncoderException extends Exception {
996         private static final long serialVersionUID = 5020247425493164465L;
997 
998         public ErrorDataEncoderException() {
999         }
1000 
1001         public ErrorDataEncoderException(String msg) {
1002             super(msg);
1003         }
1004 
1005         public ErrorDataEncoderException(Throwable cause) {
1006             super(cause);
1007         }
1008 
1009         public ErrorDataEncoderException(String msg, Throwable cause) {
1010             super(msg, cause);
1011         }
1012     }
1013 }