View Javadoc

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