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