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 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.codec.http.DefaultHttpChunk;
    31  import org.jboss.netty.handler.codec.http.HttpChunk;
    32  import org.jboss.netty.handler.codec.http.HttpConstants;
    33  import org.jboss.netty.handler.codec.http.HttpHeaders;
    34  import org.jboss.netty.handler.codec.http.HttpMethod;
    35  import org.jboss.netty.handler.codec.http.HttpRequest;
    36  import org.jboss.netty.handler.stream.ChunkedInput;
    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.size() > 0) {
    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.size() > 0) {
    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 = ((InternalAttribute) 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 = ((FileUpload) 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 = ((Attribute) 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 arg0
    968          */
    969         public ErrorDataEncoderException(String arg0) {
    970             super(arg0);
    971         }
    972
    973         /**
    974          * @param arg0
    975          */
    976         public ErrorDataEncoderException(Throwable arg0) {
    977             super(arg0);
    978         }
    979
    980         /**
    981          * @param arg0
    982          * @param arg1
    983          */
    984         public ErrorDataEncoderException(String arg0, Throwable arg1) {
    985             super(arg0, arg1);
    986         }
    987     }
    988 }