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.HttpChunk;
21  import org.jboss.netty.handler.codec.http.HttpConstants;
22  import org.jboss.netty.handler.codec.http.HttpHeaders;
23  import org.jboss.netty.handler.codec.http.HttpMethod;
24  import org.jboss.netty.handler.codec.http.HttpRequest;
25  import org.jboss.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadNoBackArrayException;
26  import org.jboss.netty.handler.codec.http.multipart.HttpPostBodyUtil.SeekAheadOptimize;
27  import org.jboss.netty.handler.codec.http.multipart.HttpPostBodyUtil.TransferEncodingMechanism;
28  import org.jboss.netty.util.internal.CaseIgnoringComparator;
29  import org.jboss.netty.util.internal.StringUtil;
30  
31  import java.io.IOException;
32  import java.io.UnsupportedEncodingException;
33  import java.net.URLDecoder;
34  import java.nio.charset.Charset;
35  import java.util.ArrayList;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.TreeMap;
39  
40  /**
41   * This decoder will decode Body and can handle POST BODY.
42   */
43  public class HttpPostRequestDecoder {
44      /**
45       * Factory used to create InterfaceHttpData
46       */
47      private final HttpDataFactory factory;
48  
49      /**
50       * Request to decode
51       */
52      private final HttpRequest request;
53  
54      /**
55       * Default charset to use
56       */
57      private final Charset charset;
58  
59      /**
60       * Does request have a body to decode
61       */
62      private boolean bodyToDecode;
63  
64      /**
65       * Does the last chunk already received
66       */
67      private boolean isLastChunk;
68  
69      /**
70       * HttpDatas from Body
71       */
72      private final List<InterfaceHttpData> bodyListHttpData = new ArrayList<InterfaceHttpData>();
73  
74      /**
75       * HttpDatas as Map from Body
76       */
77      private final Map<String, List<InterfaceHttpData>> bodyMapHttpData = new TreeMap<String, List<InterfaceHttpData>>(
78              CaseIgnoringComparator.INSTANCE);
79  
80      /**
81       * The current channelBuffer
82       */
83      private ChannelBuffer undecodedChunk;
84  
85      /**
86       * Does this request is a Multipart request
87       */
88      private boolean isMultipart;
89  
90      /**
91       * Body HttpDatas current position
92       */
93      private int bodyListHttpDataRank;
94  
95      /**
96       * If multipart, this is the boundary for the flobal multipart
97       */
98      private String multipartDataBoundary;
99  
100     /**
101      * If multipart, there could be internal multiparts (mixed) to the global multipart.
102      * Only one level is allowed.
103      */
104     private String multipartMixedBoundary;
105 
106     /**
107      * Current status
108      */
109     private MultiPartStatus currentStatus = MultiPartStatus.NOTSTARTED;
110 
111     /**
112      * Used in Multipart
113      */
114     private Map<String, Attribute> currentFieldAttributes;
115 
116     /**
117      * The current FileUpload that is currently in decode process
118      */
119     private FileUpload currentFileUpload;
120 
121     /**
122      * The current Attribute that is currently in decode process
123      */
124     private Attribute currentAttribute;
125 
126     /**
127     *
128     * @param request the request to decode
129     * @throws NullPointerException for request
130     * @throws IncompatibleDataDecoderException if the request has no body to decode
131     * @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
132     */
133     public HttpPostRequestDecoder(HttpRequest request)
134             throws ErrorDataDecoderException, IncompatibleDataDecoderException {
135         this(new DefaultHttpDataFactory(DefaultHttpDataFactory.MINSIZE),
136                 request, HttpConstants.DEFAULT_CHARSET);
137     }
138 
139     /**
140      *
141      * @param factory the factory used to create InterfaceHttpData
142      * @param request the request to decode
143      * @throws NullPointerException for request or factory
144      * @throws IncompatibleDataDecoderException if the request has no body to decode
145      * @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
146      */
147     public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request)
148             throws ErrorDataDecoderException, IncompatibleDataDecoderException {
149         this(factory, request, HttpConstants.DEFAULT_CHARSET);
150     }
151 
152     /**
153      *
154      * @param factory the factory used to create InterfaceHttpData
155      * @param request the request to decode
156      * @param charset the charset to use as default
157      * @throws NullPointerException for request or charset or factory
158      * @throws IncompatibleDataDecoderException if the request has no body to decode
159      * @throws ErrorDataDecoderException if the default charset was wrong when decoding or other errors
160      */
161     public HttpPostRequestDecoder(HttpDataFactory factory, HttpRequest request,
162             Charset charset) throws ErrorDataDecoderException,
163             IncompatibleDataDecoderException {
164         if (factory == null) {
165             throw new NullPointerException("factory");
166         }
167         if (request == null) {
168             throw new NullPointerException("request");
169         }
170         if (charset == null) {
171             throw new NullPointerException("charset");
172         }
173         this.request = request;
174         HttpMethod method = request.getMethod();
175         if (method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT) || method.equals(HttpMethod.PATCH)) {
176             bodyToDecode = true;
177         }
178         this.charset = charset;
179         this.factory = factory;
180         // Fill default values
181         if (this.request.containsHeader(HttpHeaders.Names.CONTENT_TYPE)) {
182             checkMultipart(this.request.getHeader(HttpHeaders.Names.CONTENT_TYPE));
183         } else {
184             isMultipart = false;
185         }
186         if (!bodyToDecode) {
187             throw new IncompatibleDataDecoderException("No Body to decode");
188         }
189         if (!this.request.isChunked()) {
190             undecodedChunk = this.request.getContent();
191             isLastChunk = true;
192             parseBody();
193         }
194     }
195 
196     /**
197      * states follow
198      * NOTSTARTED PREAMBLE (
199      *  (HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))*
200      *  (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE
201      *     (MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+
202      *   MIXEDCLOSEDELIMITER)*
203      * CLOSEDELIMITER)+ EPILOGUE
204      *
205      *  First status is: NOSTARTED
206 
207         Content-type: multipart/form-data, boundary=AaB03x     => PREAMBLE in Header
208 
209         --AaB03x                                               => HEADERDELIMITER
210         content-disposition: form-data; name="field1"          => DISPOSITION
211 
212         Joe Blow                                               => FIELD
213         --AaB03x                                               => HEADERDELIMITER
214         content-disposition: form-data; name="pics"            => DISPOSITION
215         Content-type: multipart/mixed, boundary=BbC04y
216 
217         --BbC04y                                               => MIXEDDELIMITER
218         Content-disposition: attachment; filename="file1.txt"  => MIXEDDISPOSITION
219         Content-Type: text/plain
220 
221         ... contents of file1.txt ...                          => MIXEDFILEUPLOAD
222         --BbC04y                                               => MIXEDDELIMITER
223         Content-disposition: file; filename="file2.gif"  => MIXEDDISPOSITION
224         Content-type: image/gif
225         Content-Transfer-Encoding: binary
226 
227           ...contents of file2.gif...                          => MIXEDFILEUPLOAD
228         --BbC04y--                                             => MIXEDCLOSEDELIMITER
229         --AaB03x--                                             => CLOSEDELIMITER
230 
231        Once CLOSEDELIMITER is found, last status is EPILOGUE
232      */
233     private enum MultiPartStatus {
234         NOTSTARTED,
235         PREAMBLE,
236         HEADERDELIMITER,
237         DISPOSITION,
238         FIELD,
239         FILEUPLOAD,
240         MIXEDPREAMBLE,
241         MIXEDDELIMITER,
242         MIXEDDISPOSITION,
243         MIXEDFILEUPLOAD,
244         MIXEDCLOSEDELIMITER,
245         CLOSEDELIMITER,
246         PREEPILOGUE,
247         EPILOGUE
248     }
249 
250     /**
251      * Check from the request ContentType if this request is a Multipart request.
252      */
253     private void checkMultipart(String contentType)
254             throws ErrorDataDecoderException {
255         // Check if Post using "multipart/form-data; boundary=--89421926422648"
256         String[] headerContentType = splitHeaderContentType(contentType);
257         if (headerContentType[0].toLowerCase().startsWith(
258                 HttpHeaders.Values.MULTIPART_FORM_DATA) &&
259                 headerContentType[1].toLowerCase().startsWith(
260                         HttpHeaders.Values.BOUNDARY)) {
261             String[] boundary = StringUtil.split(headerContentType[1], '=');
262             if (boundary.length != 2) {
263                 throw new ErrorDataDecoderException("Needs a boundary value");
264             }
265             multipartDataBoundary = "--" + boundary[1];
266             isMultipart = true;
267             currentStatus = MultiPartStatus.HEADERDELIMITER;
268         } else {
269             isMultipart = false;
270         }
271     }
272 
273     /**
274      * True if this request is a Multipart request
275      * @return True if this request is a Multipart request
276      */
277     public boolean isMultipart() {
278         return isMultipart;
279     }
280 
281     /**
282      * This method returns a List of all HttpDatas from body.<br>
283      *
284      * If chunked, all chunks must have been offered using offer() method.
285      * If not, NotEnoughDataDecoderException will be raised.
286      *
287      * @return the list of HttpDatas from Body part for POST method
288      * @throws NotEnoughDataDecoderException Need more chunks
289      */
290     public List<InterfaceHttpData> getBodyHttpDatas()
291             throws NotEnoughDataDecoderException {
292         if (!isLastChunk) {
293             throw new NotEnoughDataDecoderException();
294         }
295         return bodyListHttpData;
296     }
297 
298     /**
299      * This method returns a List of all HttpDatas with the given name from body.<br>
300      *
301      * If chunked, all chunks must have been offered using offer() method.
302      * If not, NotEnoughDataDecoderException will be raised.
303 
304      * @return All Body HttpDatas with the given name (ignore case)
305      * @throws NotEnoughDataDecoderException need more chunks
306      */
307     public List<InterfaceHttpData> getBodyHttpDatas(String name)
308             throws NotEnoughDataDecoderException {
309         if (!isLastChunk) {
310             throw new NotEnoughDataDecoderException();
311         }
312         return bodyMapHttpData.get(name);
313     }
314 
315     /**
316      * This method returns the first InterfaceHttpData with the given name from body.<br>
317      *
318      * If chunked, all chunks must have been offered using offer() method.
319      * If not, NotEnoughDataDecoderException will be raised.
320     *
321     * @return The first Body InterfaceHttpData with the given name (ignore case)
322     * @throws NotEnoughDataDecoderException need more chunks
323     */
324     public InterfaceHttpData getBodyHttpData(String name)
325             throws NotEnoughDataDecoderException {
326         if (!isLastChunk) {
327             throw new NotEnoughDataDecoderException();
328         }
329         List<InterfaceHttpData> list = bodyMapHttpData.get(name);
330         if (list != null) {
331             return list.get(0);
332         }
333         return null;
334     }
335 
336     /**
337      * Initialized the internals from a new chunk
338      * @param chunk the new received chunk
339      * @throws ErrorDataDecoderException if there is a problem with the charset decoding or
340      *          other errors
341      */
342     public void offer(HttpChunk chunk) throws ErrorDataDecoderException {
343         ChannelBuffer chunked = chunk.getContent();
344         if (undecodedChunk == null) {
345             undecodedChunk = chunked;
346         } else {
347             //undecodedChunk = ChannelBuffers.wrappedBuffer(undecodedChunk, chunk.getContent());
348             // less memory usage
349             undecodedChunk = ChannelBuffers.wrappedBuffer(
350                     undecodedChunk, chunked);
351         }
352         if (chunk.isLast()) {
353             isLastChunk = true;
354         }
355         parseBody();
356     }
357 
358     /**
359      * True if at current status, there is an available decoded InterfaceHttpData from the Body.
360      *
361      * This method works for chunked and not chunked request.
362      *
363      * @return True if at current status, there is a decoded InterfaceHttpData
364      * @throws EndOfDataDecoderException No more data will be available
365      */
366     public boolean hasNext() throws EndOfDataDecoderException {
367         if (currentStatus == MultiPartStatus.EPILOGUE) {
368             // OK except if end of list
369             if (bodyListHttpDataRank >= bodyListHttpData.size()) {
370                 throw new EndOfDataDecoderException();
371             }
372         }
373         return !bodyListHttpData.isEmpty() && bodyListHttpDataRank < bodyListHttpData.size();
374     }
375 
376     /**
377      * Returns the next available InterfaceHttpData or null if, at the time it is called, there is no more
378      * available InterfaceHttpData. A subsequent call to offer(httpChunk) could enable more data.
379      *
380      * @return the next available InterfaceHttpData or null if none
381      * @throws EndOfDataDecoderException No more data will be available
382      */
383     public InterfaceHttpData next() throws EndOfDataDecoderException {
384         if (hasNext()) {
385             return bodyListHttpData.get(bodyListHttpDataRank++);
386         }
387         return null;
388     }
389 
390     /**
391      * This method will parse as much as possible data and fill the list and map
392      * @throws ErrorDataDecoderException if there is a problem with the charset decoding or
393      *          other errors
394      */
395     private void parseBody() throws ErrorDataDecoderException {
396         if (currentStatus == MultiPartStatus.PREEPILOGUE ||
397                 currentStatus == MultiPartStatus.EPILOGUE) {
398             if (isLastChunk) {
399                 currentStatus = MultiPartStatus.EPILOGUE;
400             }
401             return;
402         }
403         if (isMultipart) {
404             parseBodyMultipart();
405         } else {
406             parseBodyAttributes();
407         }
408     }
409 
410     /**
411      * Utility function to add a new decoded data
412      */
413     private void addHttpData(InterfaceHttpData data) {
414         if (data == null) {
415             return;
416         }
417         List<InterfaceHttpData> datas = bodyMapHttpData.get(data.getName());
418         if (datas == null) {
419             datas = new ArrayList<InterfaceHttpData>(1);
420             bodyMapHttpData.put(data.getName(), datas);
421         }
422         datas.add(data);
423         bodyListHttpData.add(data);
424     }
425 
426     /**
427       * This method fill the map and list with as much Attribute as possible from Body in
428       * not Multipart mode.
429       *
430       * @throws ErrorDataDecoderException if there is a problem with the charset decoding or
431       *          other errors
432       */
433     private void parseBodyAttributesStandard() throws ErrorDataDecoderException {
434         int firstpos = undecodedChunk.readerIndex();
435         int currentpos = firstpos;
436         int equalpos;
437         int ampersandpos;
438         if (currentStatus == MultiPartStatus.NOTSTARTED) {
439             currentStatus = MultiPartStatus.DISPOSITION;
440         }
441         boolean contRead = true;
442         try {
443             while (undecodedChunk.readable() && contRead) {
444                 char read = (char) undecodedChunk.readUnsignedByte();
445                 currentpos++;
446                 switch (currentStatus) {
447                 case DISPOSITION:// search '='
448                     if (read == '=') {
449                         currentStatus = MultiPartStatus.FIELD;
450                         equalpos = currentpos - 1;
451                         String key = decodeAttribute(
452                                 undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
453                                 charset);
454                         currentAttribute = factory.createAttribute(request, key);
455                         firstpos = currentpos;
456                     } else if (read == '&') { // special empty FIELD
457                         currentStatus = MultiPartStatus.DISPOSITION;
458                         ampersandpos = currentpos - 1;
459                         String key = decodeAttribute(
460                                 undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
461                         currentAttribute = factory.createAttribute(request, key);
462                         currentAttribute.setValue(""); // empty
463                         addHttpData(currentAttribute);
464                         currentAttribute = null;
465                         firstpos = currentpos;
466                         contRead = true;
467                     }
468                     break;
469                 case FIELD:// search '&' or end of line
470                     if (read == '&') {
471                         currentStatus = MultiPartStatus.DISPOSITION;
472                         ampersandpos = currentpos - 1;
473                         setFinalBuffer(undecodedChunk.slice(firstpos, ampersandpos - firstpos));
474                         firstpos = currentpos;
475                         contRead = true;
476                     } else if (read == HttpConstants.CR) {
477                         if (undecodedChunk.readable()) {
478                             read = (char) undecodedChunk.readUnsignedByte();
479                             currentpos++;
480                             if (read == HttpConstants.LF) {
481                                 currentStatus = MultiPartStatus.PREEPILOGUE;
482                                 ampersandpos = currentpos - 2;
483                                 setFinalBuffer(
484                                         undecodedChunk.slice(firstpos, ampersandpos - firstpos));
485                                 firstpos = currentpos;
486                                 contRead = false;
487                             } else {
488                                 // Error
489                                 throw new ErrorDataDecoderException("Bad end of line");
490                             }
491                         } else {
492                             currentpos--;
493                         }
494                     } else if (read == HttpConstants.LF) {
495                         currentStatus = MultiPartStatus.PREEPILOGUE;
496                         ampersandpos = currentpos - 1;
497                         setFinalBuffer(
498                                 undecodedChunk.slice(firstpos, ampersandpos - firstpos));
499                         firstpos = currentpos;
500                         contRead = false;
501                     }
502                     break;
503                 default:
504                     // just stop
505                     contRead = false;
506                 }
507             }
508             if (isLastChunk && currentAttribute != null) {
509                 // special case
510                 ampersandpos = currentpos;
511                 if (ampersandpos > firstpos) {
512                     setFinalBuffer(
513                             undecodedChunk.slice(firstpos, ampersandpos - firstpos));
514                 } else if (! currentAttribute.isCompleted()) {
515                     setFinalBuffer(ChannelBuffers.EMPTY_BUFFER);
516                 }
517                 firstpos = currentpos;
518                 currentStatus = MultiPartStatus.EPILOGUE;
519                 return;
520             }
521             if (contRead && currentAttribute != null) {
522                 // reset index except if to continue in case of FIELD status
523                 if (currentStatus == MultiPartStatus.FIELD) {
524                     currentAttribute.addContent(
525                             undecodedChunk.slice(firstpos, currentpos - firstpos),
526                             false);
527                     firstpos = currentpos;
528                 }
529                 undecodedChunk.readerIndex(firstpos);
530             } else {
531                 // end of line so keep index
532             }
533         } catch (ErrorDataDecoderException e) {
534             // error while decoding
535             undecodedChunk.readerIndex(firstpos);
536             throw e;
537         } catch (IOException e) {
538             // error while decoding
539             undecodedChunk.readerIndex(firstpos);
540             throw new ErrorDataDecoderException(e);
541         }
542     }
543 
544     /**
545      * This method fill the map and list with as much Attribute as possible from Body in
546      * not Multipart mode.
547      *
548      * @throws ErrorDataDecoderException if there is a problem with the charset decoding or
549      * other errors
550      */
551     private void parseBodyAttributes() throws ErrorDataDecoderException {
552         SeekAheadOptimize sao;
553         try {
554             sao = new SeekAheadOptimize(undecodedChunk);
555         } catch (SeekAheadNoBackArrayException e1) {
556             parseBodyAttributesStandard();
557             return;
558         }
559         int firstpos = undecodedChunk.readerIndex();
560         int currentpos = firstpos;
561         int equalpos;
562         int ampersandpos;
563         if (currentStatus == MultiPartStatus.NOTSTARTED) {
564             currentStatus = MultiPartStatus.DISPOSITION;
565         }
566         boolean contRead = true;
567         try {
568             loop:
569             while (sao.pos < sao.limit) {
570                 char read = (char) (sao.bytes[sao.pos ++] & 0xFF);
571                 currentpos ++;
572                 switch (currentStatus) {
573                 case DISPOSITION:// search '='
574                     if (read == '=') {
575                         currentStatus = MultiPartStatus.FIELD;
576                         equalpos = currentpos - 1;
577                         String key = decodeAttribute(
578                                 undecodedChunk.toString(firstpos, equalpos - firstpos, charset),
579                                 charset);
580                         currentAttribute = factory.createAttribute(request, key);
581                         firstpos = currentpos;
582                     } else if (read == '&') { // special empty FIELD
583                         currentStatus = MultiPartStatus.DISPOSITION;
584                         ampersandpos = currentpos - 1;
585                         String key = decodeAttribute(
586                                 undecodedChunk.toString(firstpos, ampersandpos - firstpos, charset), charset);
587                         currentAttribute = factory.createAttribute(request, key);
588                         currentAttribute.setValue(""); // empty
589                         addHttpData(currentAttribute);
590                         currentAttribute = null;
591                         firstpos = currentpos;
592                         contRead = true;
593                     }
594                     break;
595                 case FIELD:// search '&' or end of line
596                     if (read == '&') {
597                         currentStatus = MultiPartStatus.DISPOSITION;
598                         ampersandpos = currentpos - 1;
599                         setFinalBuffer(undecodedChunk.slice(firstpos, ampersandpos - firstpos));
600                         firstpos = currentpos;
601                         contRead = true;
602                     } else if (read == HttpConstants.CR) {
603                         if (sao.pos < sao.limit) {
604                             read = (char) (sao.bytes[sao.pos ++] & 0xFF);
605                             currentpos++;
606                             if (read == HttpConstants.LF) {
607                                 currentStatus = MultiPartStatus.PREEPILOGUE;
608                                 ampersandpos = currentpos - 2;
609                                 sao.setReadPosition(0);
610                                 setFinalBuffer(
611                                         undecodedChunk.slice(firstpos, ampersandpos - firstpos));
612                                 firstpos = currentpos;
613                                 contRead = false;
614                                 break loop;
615                             } else {
616                                 // Error
617                                 sao.setReadPosition(0);
618                                 throw new ErrorDataDecoderException("Bad end of line");
619                             }
620                         } else {
621                             if (sao.limit > 0) {
622                                 currentpos --;
623                             }
624                         }
625                     } else if (read == HttpConstants.LF) {
626                         currentStatus = MultiPartStatus.PREEPILOGUE;
627                         ampersandpos = currentpos - 1;
628                         sao.setReadPosition(0);
629                         setFinalBuffer(
630                                 undecodedChunk.slice(firstpos, ampersandpos - firstpos));
631                         firstpos = currentpos;
632                         contRead = false;
633                         break loop;
634                     }
635                     break;
636                 default:
637                     // just stop
638                     sao.setReadPosition(0);
639                     contRead = false;
640                     break loop;
641                 }
642             }
643             if (isLastChunk && currentAttribute != null) {
644                 // special case
645                 ampersandpos = currentpos;
646                 if (ampersandpos > firstpos) {
647                     setFinalBuffer(
648                             undecodedChunk.slice(firstpos, ampersandpos - firstpos));
649                 } else if (! currentAttribute.isCompleted()) {
650                     setFinalBuffer(ChannelBuffers.EMPTY_BUFFER);
651                 }
652                 firstpos = currentpos;
653                 currentStatus = MultiPartStatus.EPILOGUE;
654                 return;
655             }
656             if (contRead && currentAttribute != null) {
657                 // reset index except if to continue in case of FIELD status
658                 if (currentStatus == MultiPartStatus.FIELD) {
659                     currentAttribute.addContent(
660                             undecodedChunk.slice(firstpos, currentpos - firstpos),
661                             false);
662                     firstpos = currentpos;
663                 }
664                 undecodedChunk.readerIndex(firstpos);
665             } else {
666                 // end of line so keep index
667             }
668         } catch (ErrorDataDecoderException e) {
669             // error while decoding
670             undecodedChunk.readerIndex(firstpos);
671             throw e;
672         } catch (IOException e) {
673             // error while decoding
674             undecodedChunk.readerIndex(firstpos);
675             throw new ErrorDataDecoderException(e);
676         }
677     }
678 
679     private void setFinalBuffer(ChannelBuffer buffer) throws ErrorDataDecoderException, IOException {
680         currentAttribute.addContent(buffer, true);
681         String value = decodeAttribute(
682                 currentAttribute.getChannelBuffer().toString(charset),
683                 charset);
684         currentAttribute.setValue(value);
685         addHttpData(currentAttribute);
686         currentAttribute = null;
687     }
688 
689     /**
690      * Decode component
691      * @return the decoded component
692      */
693     private static String decodeAttribute(String s, Charset charset)
694             throws ErrorDataDecoderException {
695         if (s == null) {
696             return "";
697         }
698         try {
699             return URLDecoder.decode(s, charset.name());
700         } catch (UnsupportedEncodingException e) {
701             throw new ErrorDataDecoderException(charset.toString(), e);
702         } catch (IllegalArgumentException e) {
703             throw new ErrorDataDecoderException("Bad string: '" + s + '\'', e);
704         }
705     }
706 
707     /**
708      * Parse the Body for multipart
709      *
710      * @throws ErrorDataDecoderException if there is a problem with the charset decoding or other errors
711      */
712     private void parseBodyMultipart() throws ErrorDataDecoderException {
713         if (undecodedChunk == null || undecodedChunk.readableBytes() == 0) {
714             // nothing to decode
715             return;
716         }
717         InterfaceHttpData data = decodeMultipart(currentStatus);
718         while (data != null) {
719             addHttpData(data);
720             if (currentStatus == MultiPartStatus.PREEPILOGUE ||
721                     currentStatus == MultiPartStatus.EPILOGUE) {
722                 break;
723             }
724             data = decodeMultipart(currentStatus);
725         }
726     }
727 
728     /**
729      * Decode a multipart request by pieces<br>
730      * <br>
731      * NOTSTARTED PREAMBLE (<br>
732      *  (HEADERDELIMITER DISPOSITION (FIELD | FILEUPLOAD))*<br>
733      *  (HEADERDELIMITER DISPOSITION MIXEDPREAMBLE<br>
734      *     (MIXEDDELIMITER MIXEDDISPOSITION MIXEDFILEUPLOAD)+<br>
735      *   MIXEDCLOSEDELIMITER)*<br>
736      * CLOSEDELIMITER)+ EPILOGUE<br>
737      *
738      * Inspired from HttpMessageDecoder
739      *
740      * @return the next decoded InterfaceHttpData or null if none until now.
741      * @throws ErrorDataDecoderException if an error occurs
742      */
743     private InterfaceHttpData decodeMultipart(MultiPartStatus state)
744             throws ErrorDataDecoderException {
745         switch (state) {
746         case NOTSTARTED:
747             throw new ErrorDataDecoderException(
748                     "Should not be called with the current status");
749         case PREAMBLE:
750             // Content-type: multipart/form-data, boundary=AaB03x
751             throw new ErrorDataDecoderException(
752                     "Should not be called with the current status");
753         case HEADERDELIMITER: {
754             // --AaB03x or --AaB03x--
755             return findMultipartDelimiter(multipartDataBoundary,
756                     MultiPartStatus.DISPOSITION, MultiPartStatus.PREEPILOGUE);
757         }
758         case DISPOSITION: {
759             //  content-disposition: form-data; name="field1"
760             //  content-disposition: form-data; name="pics"; filename="file1.txt"
761             // and other immediate values like
762             //  Content-type: image/gif
763             //  Content-Type: text/plain
764             //  Content-Type: text/plain; charset=ISO-8859-1
765             //  Content-Transfer-Encoding: binary
766             // The following line implies a change of mode (mixed mode)
767             //  Content-type: multipart/mixed, boundary=BbC04y
768             return findMultipartDisposition();
769         }
770         case FIELD: {
771             // Now get value according to Content-Type and Charset
772             Charset localCharset = null;
773             Attribute charsetAttribute = currentFieldAttributes
774                     .get(HttpHeaders.Values.CHARSET);
775             if (charsetAttribute != null) {
776                 try {
777                     localCharset = Charset.forName(charsetAttribute.getValue());
778                 } catch (IOException e) {
779                     throw new ErrorDataDecoderException(e);
780                 }
781             }
782             Attribute nameAttribute = currentFieldAttributes
783                 .get(HttpPostBodyUtil.NAME);
784             if (currentAttribute == null) {
785                 try {
786                     currentAttribute = factory.createAttribute(request,
787                             cleanString(nameAttribute.getValue()));
788                 } catch (NullPointerException e) {
789                     throw new ErrorDataDecoderException(e);
790                 } catch (IllegalArgumentException e) {
791                     throw new ErrorDataDecoderException(e);
792                 } catch (IOException e) {
793                     throw new ErrorDataDecoderException(e);
794                 }
795                 if (localCharset != null) {
796                     currentAttribute.setCharset(localCharset);
797                 }
798             }
799             // load data
800             try {
801                 loadFieldMultipart(multipartDataBoundary);
802             } catch (NotEnoughDataDecoderException e) {
803                 return null;
804             }
805             Attribute finalAttribute = currentAttribute;
806             currentAttribute = null;
807             currentFieldAttributes = null;
808             // ready to load the next one
809             currentStatus = MultiPartStatus.HEADERDELIMITER;
810             return finalAttribute;
811         }
812         case FILEUPLOAD: {
813             // eventually restart from existing FileUpload
814             return getFileUpload(multipartDataBoundary);
815         }
816         case MIXEDDELIMITER: {
817             // --AaB03x or --AaB03x--
818             // Note that currentFieldAttributes exists
819             return findMultipartDelimiter(multipartMixedBoundary,
820                     MultiPartStatus.MIXEDDISPOSITION,
821                     MultiPartStatus.HEADERDELIMITER);
822         }
823         case MIXEDDISPOSITION: {
824             return findMultipartDisposition();
825         }
826         case MIXEDFILEUPLOAD: {
827             // eventually restart from existing FileUpload
828             return getFileUpload(multipartMixedBoundary);
829         }
830         case PREEPILOGUE:
831             return null;
832         case EPILOGUE:
833             return null;
834         default:
835             throw new ErrorDataDecoderException("Shouldn't reach here.");
836         }
837     }
838 
839     /**
840      * Skip control Characters
841      * @throws NotEnoughDataDecoderException
842      */
843     void skipControlCharacters() throws NotEnoughDataDecoderException {
844         SeekAheadOptimize sao;
845         try {
846             sao = new SeekAheadOptimize(undecodedChunk);
847         } catch (SeekAheadNoBackArrayException e) {
848             try {
849                 skipControlCharactersStandard();
850             } catch (IndexOutOfBoundsException e1) {
851                 throw new NotEnoughDataDecoderException(e1);
852             }
853             return;
854         }
855 
856         while (sao.pos < sao.limit) {
857             char c = (char) (sao.bytes[sao.pos ++] & 0xFF);
858             if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
859                 sao.setReadPosition(1);
860                 return;
861             }
862         }
863         throw new NotEnoughDataDecoderException("Access out of bounds");
864     }
865     void skipControlCharactersStandard() {
866         for (;;) {
867             char c = (char) undecodedChunk.readUnsignedByte();
868             if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
869                 undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
870                 break;
871             }
872         }
873     }
874 
875     /**
876      * Find the next Multipart Delimiter
877      * @param delimiter delimiter to find
878      * @param dispositionStatus the next status if the delimiter is a start
879      * @param closeDelimiterStatus the next status if the delimiter is a close delimiter
880      * @return the next InterfaceHttpData if any
881      * @throws ErrorDataDecoderException
882      */
883     private InterfaceHttpData findMultipartDelimiter(String delimiter,
884             MultiPartStatus dispositionStatus,
885             MultiPartStatus closeDelimiterStatus)
886             throws ErrorDataDecoderException {
887         // --AaB03x or --AaB03x--
888         int readerIndex = undecodedChunk.readerIndex();
889         try {
890             skipControlCharacters();
891         } catch (NotEnoughDataDecoderException e1) {
892             undecodedChunk.readerIndex(readerIndex);
893             return null;
894         }
895         skipOneLine();
896         String newline;
897         try {
898             newline = readDelimiter(delimiter);
899         } catch (NotEnoughDataDecoderException e) {
900             undecodedChunk.readerIndex(readerIndex);
901             return null;
902         }
903         if (newline.equals(delimiter)) {
904             currentStatus = dispositionStatus;
905             return decodeMultipart(dispositionStatus);
906         }
907         if (newline.equals(delimiter + "--")) {
908             // CLOSEDELIMITER or MIXED CLOSEDELIMITER found
909             currentStatus = closeDelimiterStatus;
910             if (currentStatus == MultiPartStatus.HEADERDELIMITER) {
911                 // MIXEDCLOSEDELIMITER
912                 // end of the Mixed part
913                 currentFieldAttributes = null;
914                 return decodeMultipart(MultiPartStatus.HEADERDELIMITER);
915             }
916             return null;
917         }
918         undecodedChunk.readerIndex(readerIndex);
919         throw new ErrorDataDecoderException("No Multipart delimiter found");
920     }
921 
922     /**
923      * Find the next Disposition
924      * @return the next InterfaceHttpData if any
925      * @throws ErrorDataDecoderException
926      */
927     private InterfaceHttpData findMultipartDisposition()
928             throws ErrorDataDecoderException {
929         int readerIndex = undecodedChunk.readerIndex();
930         if (currentStatus == MultiPartStatus.DISPOSITION) {
931             currentFieldAttributes = new TreeMap<String, Attribute>(
932                     CaseIgnoringComparator.INSTANCE);
933         }
934         // read many lines until empty line with newline found! Store all data
935         while (!skipOneLine()) {
936             String newline;
937             try {
938                 skipControlCharacters();
939                 newline = readLine();
940             } catch (NotEnoughDataDecoderException e) {
941                 undecodedChunk.readerIndex(readerIndex);
942                 return null;
943             }
944             String[] contents = splitMultipartHeader(newline);
945             if (contents[0].equalsIgnoreCase(HttpPostBodyUtil.CONTENT_DISPOSITION)) {
946                 boolean checkSecondArg;
947                 if (currentStatus == MultiPartStatus.DISPOSITION) {
948                     checkSecondArg = contents[1]
949                             .equalsIgnoreCase(HttpPostBodyUtil.FORM_DATA);
950                 } else {
951                     checkSecondArg = contents[1]
952                             .equalsIgnoreCase(HttpPostBodyUtil.ATTACHMENT) ||
953                             contents[1]
954                             .equalsIgnoreCase(HttpPostBodyUtil.FILE);
955                 }
956                 if (checkSecondArg) {
957                     // read next values and store them in the map as Attribute
958                     for (int i = 2; i < contents.length; i ++) {
959                         String[] values = StringUtil.split(contents[i], '=');
960                         Attribute attribute;
961                         try {
962                             String name = cleanString(values[0]);
963                             String value = values[1];
964 
965                             // See http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
966                             if (HttpPostBodyUtil.FILENAME.equals(name)) {
967                                 // filename value is quoted string so strip them
968                                 value = value.substring(1, value.length() - 1);
969                             } else {
970                                 // otherwise we need to clean the value
971                                 value = cleanString(value);
972                             }
973                             attribute = factory.createAttribute(request, name, value);
974                         } catch (NullPointerException e) {
975                             throw new ErrorDataDecoderException(e);
976                         } catch (IllegalArgumentException e) {
977                             throw new ErrorDataDecoderException(e);
978                         }
979                         currentFieldAttributes.put(attribute.getName(),
980                                 attribute);
981                     }
982                 }
983             } else if (contents[0]
984                     .equalsIgnoreCase(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING)) {
985                 Attribute attribute;
986                 try {
987                     attribute = factory.createAttribute(request,
988                             HttpHeaders.Names.CONTENT_TRANSFER_ENCODING,
989                             cleanString(contents[1]));
990                 } catch (NullPointerException e) {
991                     throw new ErrorDataDecoderException(e);
992                 } catch (IllegalArgumentException e) {
993                     throw new ErrorDataDecoderException(e);
994                 }
995                 currentFieldAttributes.put(
996                         HttpHeaders.Names.CONTENT_TRANSFER_ENCODING, attribute);
997             } else if (contents[0]
998                     .equalsIgnoreCase(HttpHeaders.Names.CONTENT_LENGTH)) {
999                 Attribute attribute;
1000                 try {
1001                     attribute = factory.createAttribute(request,
1002                             HttpHeaders.Names.CONTENT_LENGTH,
1003                             cleanString(contents[1]));
1004                 } catch (NullPointerException e) {
1005                     throw new ErrorDataDecoderException(e);
1006                 } catch (IllegalArgumentException e) {
1007                     throw new ErrorDataDecoderException(e);
1008                 }
1009                 currentFieldAttributes.put(HttpHeaders.Names.CONTENT_LENGTH,
1010                         attribute);
1011             } else if (contents[0].equalsIgnoreCase(HttpHeaders.Names.CONTENT_TYPE)) {
1012                 // Take care of possible "multipart/mixed"
1013                 if (contents[1].equalsIgnoreCase(HttpPostBodyUtil.MULTIPART_MIXED)) {
1014                     if (currentStatus == MultiPartStatus.DISPOSITION) {
1015                         String[] values = StringUtil.split(contents[2], '=');
1016                         multipartMixedBoundary = "--" + values[1];
1017                         currentStatus = MultiPartStatus.MIXEDDELIMITER;
1018                         return decodeMultipart(MultiPartStatus.MIXEDDELIMITER);
1019                     } else {
1020                         throw new ErrorDataDecoderException(
1021                                 "Mixed Multipart found in a previous Mixed Multipart");
1022                     }
1023                 } else {
1024                     for (int i = 1; i < contents.length; i ++) {
1025                         if (contents[i].toLowerCase().startsWith(
1026                                 HttpHeaders.Values.CHARSET)) {
1027                             String[] values = StringUtil.split(contents[i], '=');
1028                             Attribute attribute;
1029                             try {
1030                                 attribute = factory.createAttribute(request,
1031                                         HttpHeaders.Values.CHARSET,
1032                                         cleanString(values[1]));
1033                             } catch (NullPointerException e) {
1034                                 throw new ErrorDataDecoderException(e);
1035                             } catch (IllegalArgumentException e) {
1036                                 throw new ErrorDataDecoderException(e);
1037                             }
1038                             currentFieldAttributes.put(HttpHeaders.Values.CHARSET,
1039                                     attribute);
1040                         } else {
1041                             Attribute attribute;
1042                             try {
1043                                 attribute = factory.createAttribute(request,
1044                                         cleanString(contents[0]), contents[i]);
1045                             } catch (NullPointerException e) {
1046                                 throw new ErrorDataDecoderException(e);
1047                             } catch (IllegalArgumentException e) {
1048                                 throw new ErrorDataDecoderException(e);
1049                             }
1050                             currentFieldAttributes.put(attribute.getName(),
1051                                     attribute);
1052                         }
1053                     }
1054                 }
1055             } else {
1056                 throw new ErrorDataDecoderException("Unknown Params: " +
1057                         newline);
1058             }
1059         }
1060         // Is it a FileUpload
1061         Attribute filenameAttribute = currentFieldAttributes
1062                 .get(HttpPostBodyUtil.FILENAME);
1063         if (currentStatus == MultiPartStatus.DISPOSITION) {
1064             if (filenameAttribute != null) {
1065                 // FileUpload
1066                 currentStatus = MultiPartStatus.FILEUPLOAD;
1067                 // do not change the buffer position
1068                 return decodeMultipart(MultiPartStatus.FILEUPLOAD);
1069             } else {
1070                 // Field
1071                 currentStatus = MultiPartStatus.FIELD;
1072                 // do not change the buffer position
1073                 return decodeMultipart(MultiPartStatus.FIELD);
1074             }
1075         } else {
1076             if (filenameAttribute != null) {
1077                 // FileUpload
1078                 currentStatus = MultiPartStatus.MIXEDFILEUPLOAD;
1079                 // do not change the buffer position
1080                 return decodeMultipart(MultiPartStatus.MIXEDFILEUPLOAD);
1081             } else {
1082                 // Field is not supported in MIXED mode
1083                 throw new ErrorDataDecoderException("Filename not found");
1084             }
1085         }
1086     }
1087 
1088     /**
1089      * Get the FileUpload (new one or current one)
1090      * @param delimiter the delimiter to use
1091      * @return the InterfaceHttpData if any
1092      * @throws ErrorDataDecoderException
1093      */
1094     private InterfaceHttpData getFileUpload(String delimiter)
1095             throws ErrorDataDecoderException {
1096         // eventually restart from existing FileUpload
1097         // Now get value according to Content-Type and Charset
1098         Attribute encoding = currentFieldAttributes
1099                 .get(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING);
1100         Charset localCharset = charset;
1101         // Default
1102         TransferEncodingMechanism mechanism = TransferEncodingMechanism.BIT7;
1103         if (encoding != null) {
1104             String code;
1105             try {
1106                 code = encoding.getValue().toLowerCase();
1107             } catch (IOException e) {
1108                 throw new ErrorDataDecoderException(e);
1109             }
1110             if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT7.value())) {
1111                 localCharset = HttpPostBodyUtil.US_ASCII;
1112             } else if (code.equals(HttpPostBodyUtil.TransferEncodingMechanism.BIT8.value())) {
1113                 localCharset = HttpPostBodyUtil.ISO_8859_1;
1114                 mechanism = TransferEncodingMechanism.BIT8;
1115             } else if (code
1116                     .equals(HttpPostBodyUtil.TransferEncodingMechanism.BINARY.value())) {
1117                 // no real charset, so let the default
1118                 mechanism = TransferEncodingMechanism.BINARY;
1119             } else {
1120                 throw new ErrorDataDecoderException(
1121                         "TransferEncoding Unknown: " + code);
1122             }
1123         }
1124         Attribute charsetAttribute = currentFieldAttributes
1125                 .get(HttpHeaders.Values.CHARSET);
1126         if (charsetAttribute != null) {
1127             try {
1128                 localCharset = Charset.forName(charsetAttribute.getValue());
1129             } catch (IOException e) {
1130                 throw new ErrorDataDecoderException(e);
1131             }
1132         }
1133         if (currentFileUpload == null) {
1134             Attribute filenameAttribute = currentFieldAttributes
1135                     .get(HttpPostBodyUtil.FILENAME);
1136             Attribute nameAttribute = currentFieldAttributes
1137                     .get(HttpPostBodyUtil.NAME);
1138             Attribute contentTypeAttribute = currentFieldAttributes
1139                     .get(HttpHeaders.Names.CONTENT_TYPE);
1140             if (contentTypeAttribute == null) {
1141                 throw new ErrorDataDecoderException(
1142                         "Content-Type is absent but required");
1143             }
1144             Attribute lengthAttribute = currentFieldAttributes
1145                     .get(HttpHeaders.Names.CONTENT_LENGTH);
1146             long size;
1147             try {
1148                 size = lengthAttribute != null? Long.parseLong(lengthAttribute
1149                         .getValue()) : 0L;
1150             } catch (IOException e) {
1151                 throw new ErrorDataDecoderException(e);
1152             } catch (NumberFormatException e) {
1153                 size = 0;
1154             }
1155             try {
1156                 currentFileUpload = factory.createFileUpload(
1157                         request,
1158                         cleanString(nameAttribute.getValue()), cleanString(filenameAttribute.getValue()),
1159                         contentTypeAttribute.getValue(), mechanism.value(),
1160                         localCharset, size);
1161             } catch (NullPointerException e) {
1162                 throw new ErrorDataDecoderException(e);
1163             } catch (IllegalArgumentException e) {
1164                 throw new ErrorDataDecoderException(e);
1165             } catch (IOException e) {
1166                 throw new ErrorDataDecoderException(e);
1167             }
1168         }
1169         // load data as much as possible
1170         try {
1171             readFileUploadByteMultipart(delimiter);
1172         } catch (NotEnoughDataDecoderException e) {
1173             // do not change the buffer position
1174             // since some can be already saved into FileUpload
1175             // So do not change the currentStatus
1176             return null;
1177         }
1178         if (currentFileUpload.isCompleted()) {
1179             // ready to load the next one
1180             if (currentStatus == MultiPartStatus.FILEUPLOAD) {
1181                 currentStatus = MultiPartStatus.HEADERDELIMITER;
1182                 currentFieldAttributes = null;
1183             } else {
1184                 currentStatus = MultiPartStatus.MIXEDDELIMITER;
1185                 cleanMixedAttributes();
1186             }
1187             FileUpload fileUpload = currentFileUpload;
1188             currentFileUpload = null;
1189             return fileUpload;
1190         }
1191         // do not change the buffer position
1192         // since some can be already saved into FileUpload
1193         // So do not change the currentStatus
1194         return null;
1195     }
1196 
1197     /**
1198      * Clean all HttpDatas (on Disk) for the current request.
1199      */
1200     public void cleanFiles() {
1201         factory.cleanRequestHttpDatas(request);
1202     }
1203 
1204     /**
1205      * Remove the given FileUpload from the list of FileUploads to clean
1206      */
1207     public void removeHttpDataFromClean(InterfaceHttpData data) {
1208         factory.removeHttpDataFromClean(request, data);
1209     }
1210 
1211     /**
1212      * Remove all Attributes that should be cleaned between two FileUpload in Mixed mode
1213      */
1214     private void cleanMixedAttributes() {
1215         currentFieldAttributes.remove(HttpHeaders.Values.CHARSET);
1216         currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_LENGTH);
1217         currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_TRANSFER_ENCODING);
1218         currentFieldAttributes.remove(HttpHeaders.Names.CONTENT_TYPE);
1219         currentFieldAttributes.remove(HttpPostBodyUtil.FILENAME);
1220     }
1221 
1222     /**
1223      * Read one line up to the CRLF or LF
1224      * @return the String from one line
1225      * @throws NotEnoughDataDecoderException Need more chunks and
1226      *   reset the readerInder to the previous value
1227      */
1228     private String readLineStandard() throws NotEnoughDataDecoderException {
1229         int readerIndex = undecodedChunk.readerIndex();
1230         try {
1231             ChannelBuffer line = ChannelBuffers.dynamicBuffer(64);
1232 
1233             while (undecodedChunk.readable()) {
1234                 byte nextByte = undecodedChunk.readByte();
1235                 if (nextByte == HttpConstants.CR) {
1236                     nextByte = undecodedChunk.readByte();
1237                     if (nextByte == HttpConstants.LF) {
1238                         return line.toString(charset);
1239                     }
1240                 } else if (nextByte == HttpConstants.LF) {
1241                     return line.toString(charset);
1242                 } else {
1243                     line.writeByte(nextByte);
1244                 }
1245             }
1246         } catch (IndexOutOfBoundsException e) {
1247             undecodedChunk.readerIndex(readerIndex);
1248             throw new NotEnoughDataDecoderException(e);
1249         }
1250         undecodedChunk.readerIndex(readerIndex);
1251         throw new NotEnoughDataDecoderException();
1252     }
1253     /**
1254      * Read one line up to the CRLF or LF
1255      * @return the String from one line
1256      * @throws NotEnoughDataDecoderException Need more chunks and
1257      * reset the readerInder to the previous value
1258      */
1259     private String readLine() throws NotEnoughDataDecoderException {
1260         SeekAheadOptimize sao;
1261         try {
1262             sao = new SeekAheadOptimize(undecodedChunk);
1263         } catch (SeekAheadNoBackArrayException e1) {
1264             return readLineStandard();
1265         }
1266         int readerIndex = undecodedChunk.readerIndex();
1267         try {
1268             ChannelBuffer line = ChannelBuffers.dynamicBuffer(64);
1269             while (sao.pos < sao.limit) {
1270                 byte nextByte = sao.bytes[sao.pos ++];
1271                 if (nextByte == HttpConstants.CR) {
1272                     if (sao.pos < sao.limit) {
1273                         nextByte = sao.bytes[sao.pos ++];
1274                         if (nextByte == HttpConstants.LF) {
1275                             sao.setReadPosition(0);
1276                             return line.toString(charset);
1277                         }
1278                     } else {
1279                         line.writeByte(nextByte);
1280                     }
1281                 } else if (nextByte == HttpConstants.LF) {
1282                     sao.setReadPosition(0);
1283                     return line.toString(charset);
1284                 } else {
1285                     line.writeByte(nextByte);
1286                 }
1287             }
1288         } catch (IndexOutOfBoundsException e) {
1289             undecodedChunk.readerIndex(readerIndex);
1290             throw new NotEnoughDataDecoderException(e);
1291         }
1292         undecodedChunk.readerIndex(readerIndex);
1293         throw new NotEnoughDataDecoderException();
1294     }
1295 
1296     /**
1297      * Read one line up to --delimiter or --delimiter-- and if existing the CRLF or LF
1298      * Read one line up to --delimiter or --delimiter-- and if existing the CRLF or LF.
1299      * Note that CRLF or LF are mandatory for opening delimiter (--delimiter) but not for
1300      * closing delimiter (--delimiter--) since some clients does not include CRLF in this case.
1301      *
1302      * @param delimiter of the form --string, such that '--' is already included
1303      * @return the String from one line as the delimiter searched (opening or closing)
1304      * @throws NotEnoughDataDecoderException Need more chunks and
1305      *   reset the readerInder to the previous value
1306      */
1307     private String readDelimiterStandard(String delimiter) throws NotEnoughDataDecoderException {
1308         int readerIndex = undecodedChunk.readerIndex();
1309         try {
1310             StringBuilder sb = new StringBuilder(64);
1311             int delimiterPos = 0;
1312             int len = delimiter.length();
1313             while (undecodedChunk.readable() && delimiterPos < len) {
1314                 byte nextByte = undecodedChunk.readByte();
1315                 if (nextByte == delimiter.charAt(delimiterPos)) {
1316                     delimiterPos++;
1317                     sb.append((char) nextByte);
1318                 } else {
1319                     // delimiter not found so break here !
1320                     undecodedChunk.readerIndex(readerIndex);
1321                     throw new NotEnoughDataDecoderException();
1322                 }
1323             }
1324             // Now check if either opening delimiter or closing delimiter
1325             if (undecodedChunk.readable()) {
1326                 byte nextByte = undecodedChunk.readByte();
1327                 // first check for opening delimiter
1328                 if (nextByte == HttpConstants.CR) {
1329                     nextByte = undecodedChunk.readByte();
1330                     if (nextByte == HttpConstants.LF) {
1331                         return sb.toString();
1332                     } else {
1333                         // error since CR must be followed by LF
1334                         // delimiter not found so break here !
1335                         undecodedChunk.readerIndex(readerIndex);
1336                         throw new NotEnoughDataDecoderException();
1337                     }
1338                 } else if (nextByte == HttpConstants.LF) {
1339                     return sb.toString();
1340                 } else if (nextByte == '-') {
1341                     sb.append('-');
1342                     // second check for closing delimiter
1343                     nextByte = undecodedChunk.readByte();
1344                     if (nextByte == '-') {
1345                         sb.append('-');
1346                         // now try to find if CRLF or LF there
1347                         if (undecodedChunk.readable()) {
1348                             nextByte = undecodedChunk.readByte();
1349                             if (nextByte == HttpConstants.CR) {
1350                                 nextByte = undecodedChunk.readByte();
1351                                 if (nextByte == HttpConstants.LF) {
1352                                     return sb.toString();
1353                                 } else {
1354                                     // error CR without LF
1355                                     // delimiter not found so break here !
1356                                    undecodedChunk.readerIndex(readerIndex);
1357                                     throw new NotEnoughDataDecoderException();
1358                                 }
1359                             } else if (nextByte == HttpConstants.LF) {
1360                                 return sb.toString();
1361                             } else {
1362                                 // No CRLF but ok however (Adobe Flash uploader)
1363                                 // minus 1 since we read one char ahead but should not
1364                                 undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
1365                                 return sb.toString();
1366                             }
1367                         }
1368                         // FIXME what do we do here?
1369                         // either considering it is fine, either waiting for more data to come?
1370                         // lets try considering it is fine...
1371                         return sb.toString();
1372                     }
1373                     // only one '-' => not enough
1374                     // whatever now => error since incomplete
1375                 }
1376             }
1377         } catch (IndexOutOfBoundsException e) {
1378             undecodedChunk.readerIndex(readerIndex);
1379             throw new NotEnoughDataDecoderException(e);
1380         }
1381         undecodedChunk.readerIndex(readerIndex);
1382         throw new NotEnoughDataDecoderException();
1383     }
1384 
1385     /**
1386      * Read one line up to --delimiter or --delimiter-- and if existing the CRLF or LF.
1387      * Note that CRLF or LF are mandatory for opening delimiter (--delimiter) but not for
1388      * closing delimiter (--delimiter--) since some clients does not include CRLF in this case.
1389      *
1390      * @param delimiter of the form --string, such that '--' is already included
1391      * @return the String from one line as the delimiter searched (opening or closing)
1392      * @throws NotEnoughDataDecoderException Need more chunks and
1393      * reset the readerInder to the previous value
1394      */
1395     private String readDelimiter(String delimiter) throws NotEnoughDataDecoderException {
1396         SeekAheadOptimize sao;
1397         try {
1398             sao = new SeekAheadOptimize(undecodedChunk);
1399         } catch (SeekAheadNoBackArrayException e1) {
1400             return readDelimiterStandard(delimiter);
1401         }
1402         int readerIndex = undecodedChunk.readerIndex();
1403         int delimiterPos = 0;
1404         int len = delimiter.length();
1405         try {
1406             StringBuilder sb = new StringBuilder(64);
1407             // check conformity with delimiter
1408             while (sao.pos < sao.limit && delimiterPos < len) {
1409                 byte nextByte = sao.bytes[sao.pos ++];
1410                 if (nextByte == delimiter.charAt(delimiterPos)) {
1411                     delimiterPos++;
1412                     sb.append((char) nextByte);
1413                 } else {
1414                     // delimiter not found so break here !
1415                     undecodedChunk.readerIndex(readerIndex);
1416                     throw new NotEnoughDataDecoderException();
1417                 }
1418             }
1419             // Now check if either opening delimiter or closing delimiter
1420             if (sao.pos < sao.limit) {
1421                 byte nextByte = sao.bytes[sao.pos ++];
1422                 if (nextByte == HttpConstants.CR) {
1423                     // first check for opening delimiter
1424                     if (sao.pos < sao.limit) {
1425                         nextByte = sao.bytes[sao.pos ++];
1426                         if (nextByte == HttpConstants.LF) {
1427                             sao.setReadPosition(0);
1428                             return sb.toString();
1429                         }
1430                     } else {
1431                         // error since CR must be followed by LF
1432                         // delimiter not found so break here !
1433                         undecodedChunk.readerIndex(readerIndex);
1434                         throw new NotEnoughDataDecoderException();
1435                     }
1436                 } else if (nextByte == HttpConstants.LF) {
1437                     // same first check for opening delimiter where LF used with no CR
1438                     sao.setReadPosition(0);
1439                     return sb.toString();
1440                 } else if (nextByte == '-') {
1441                     sb.append('-');
1442                     // second check for closing delimiter
1443                     if (sao.pos < sao.limit) {
1444                         nextByte = sao.bytes[sao.pos ++];
1445                         if (nextByte == '-') {
1446                             sb.append('-');
1447                             // now try to find if CRLF or LF there
1448                             if (sao.pos < sao.limit) {
1449                                 nextByte = sao.bytes[sao.pos++];
1450                                 if (nextByte == HttpConstants.CR) {
1451                                     if (sao.pos < sao.limit) {
1452                                         nextByte = sao.bytes[sao.pos++];
1453                                         if (nextByte == HttpConstants.LF) {
1454                                             sao.setReadPosition(0);
1455                                             return sb.toString();
1456                                         }
1457                                     } else {
1458                                         // error CR without LF
1459                                         // delimiter not found so break here !
1460                                         undecodedChunk.readerIndex(readerIndex);
1461                                         throw new NotEnoughDataDecoderException();
1462                                     }
1463                                 } else if (nextByte == HttpConstants.LF) {
1464                                     sao.setReadPosition(0);
1465                                     return sb.toString();
1466                                 } else {
1467                                     // No CRLF but ok however (Adobe Flash uploader)
1468                                     // minus 1 since we read one char ahead but should not
1469                                     sao.setReadPosition(1);
1470                                     return sb.toString();
1471                                 }
1472                             }
1473                             // FIXME what do we do here?
1474                             // either considering it is fine, either waiting for more data to come?
1475                             // lets try considering it is fine...
1476                             sao.setReadPosition(0);
1477                             return sb.toString();
1478                         }
1479                         // whatever now => error since incomplete
1480                         // only one '-' => not enough or whatever not enough element
1481                     }
1482                 }
1483             }
1484         } catch (IndexOutOfBoundsException e) {
1485             undecodedChunk.readerIndex(readerIndex);
1486             throw new NotEnoughDataDecoderException(e);
1487         }
1488         undecodedChunk.readerIndex(readerIndex);
1489         throw new NotEnoughDataDecoderException();
1490     }
1491 
1492     /**
1493      * Read a FileUpload data as Byte (Binary) and add the bytes directly to the
1494      * FileUpload. If the delimiter is found, the FileUpload is completed.
1495      * @throws NotEnoughDataDecoderException Need more chunks but
1496      *   do not reset the readerInder since some values will be already added to the FileOutput
1497      * @throws ErrorDataDecoderException write IO error occurs with the FileUpload
1498      */
1499     private void readFileUploadByteMultipartStandard(String delimiter)
1500             throws NotEnoughDataDecoderException, ErrorDataDecoderException {
1501         int readerIndex = undecodedChunk.readerIndex();
1502         // found the decoder limit
1503         boolean newLine = true;
1504         int index = 0;
1505         int lastPosition = undecodedChunk.readerIndex();
1506         boolean found = false;
1507         while (undecodedChunk.readable()) {
1508             byte nextByte = undecodedChunk.readByte();
1509             if (newLine) {
1510                 // Check the delimiter
1511                 if (nextByte == delimiter.codePointAt(index)) {
1512                     index ++;
1513                     if (delimiter.length() == index) {
1514                         found = true;
1515                         break;
1516                     }
1517                     continue;
1518                 } else {
1519                     newLine = false;
1520                     index = 0;
1521                     // continue until end of line
1522                     if (nextByte == HttpConstants.CR) {
1523                         if (undecodedChunk.readable()) {
1524                             nextByte = undecodedChunk.readByte();
1525                             if (nextByte == HttpConstants.LF) {
1526                                 newLine = true;
1527                                 index = 0;
1528                                 lastPosition = undecodedChunk.readerIndex() - 2;
1529                             } else {
1530                                 // save last valid position
1531                                 lastPosition = undecodedChunk.readerIndex() - 1;
1532 
1533                                 // Unread next byte.
1534                                 undecodedChunk.readerIndex(lastPosition);
1535                             }
1536                         }
1537                     } else if (nextByte == HttpConstants.LF) {
1538                         newLine = true;
1539                         index = 0;
1540                         lastPosition = undecodedChunk.readerIndex() - 1;
1541                     } else {
1542                         // save last valid position
1543                         lastPosition = undecodedChunk.readerIndex();
1544                     }
1545                 }
1546             } else {
1547                 // continue until end of line
1548                 if (nextByte == HttpConstants.CR) {
1549                     if (undecodedChunk.readable()) {
1550                         nextByte = undecodedChunk.readByte();
1551                         if (nextByte == HttpConstants.LF) {
1552                             newLine = true;
1553                             index = 0;
1554                             lastPosition = undecodedChunk.readerIndex() - 2;
1555                         } else {
1556                             // save last valid position
1557                             lastPosition = undecodedChunk.readerIndex() - 1;
1558 
1559                             // Unread next byte.
1560                             undecodedChunk.readerIndex(lastPosition);
1561                         }
1562                     }
1563                 } else if (nextByte == HttpConstants.LF) {
1564                     newLine = true;
1565                     index = 0;
1566                     lastPosition = undecodedChunk.readerIndex() - 1;
1567                 } else {
1568                     // save last valid position
1569                     lastPosition = undecodedChunk.readerIndex();
1570                 }
1571             }
1572         }
1573         ChannelBuffer buffer = undecodedChunk.slice(readerIndex, lastPosition -
1574                 readerIndex);
1575         if (found) {
1576             // found so lastPosition is correct and final
1577             try {
1578                 currentFileUpload.addContent(buffer, true);
1579                 // just before the CRLF and delimiter
1580                 undecodedChunk.readerIndex(lastPosition);
1581             } catch (IOException e) {
1582                 throw new ErrorDataDecoderException(e);
1583             }
1584         } else {
1585             // possibly the delimiter is partially found but still the last position is OK
1586             try {
1587                 currentFileUpload.addContent(buffer, false);
1588                 // last valid char (not CR, not LF, not beginning of delimiter)
1589                 undecodedChunk.readerIndex(lastPosition);
1590                 throw new NotEnoughDataDecoderException();
1591             } catch (IOException e) {
1592                 throw new ErrorDataDecoderException(e);
1593             }
1594         }
1595     }
1596 
1597     /**
1598      * Read a FileUpload data as Byte (Binary) and add the bytes directly to the
1599      * FileUpload. If the delimiter is found, the FileUpload is completed.
1600      * @throws NotEnoughDataDecoderException Need more chunks but
1601      * do not reset the readerInder since some values will be already added to the FileOutput
1602      * @throws ErrorDataDecoderException write IO error occurs with the FileUpload
1603      */
1604     private void readFileUploadByteMultipart(String delimiter)
1605             throws NotEnoughDataDecoderException, ErrorDataDecoderException {
1606         SeekAheadOptimize sao;
1607         try {
1608             sao = new SeekAheadOptimize(undecodedChunk);
1609         } catch (SeekAheadNoBackArrayException e1) {
1610             readFileUploadByteMultipartStandard(delimiter);
1611             return;
1612         }
1613         int readerIndex = undecodedChunk.readerIndex();
1614         // found the decoder limit
1615         boolean newLine = true;
1616         int index = 0;
1617         int lastrealpos = sao.pos;
1618         int lastPosition;
1619         boolean found = false;
1620 
1621         while (sao.pos < sao.limit) {
1622             byte nextByte = sao.bytes[sao.pos ++];
1623             if (newLine) {
1624                 // Check the delimiter
1625                 if (nextByte == delimiter.codePointAt(index)) {
1626                     index ++;
1627                     if (delimiter.length() == index) {
1628                         found = true;
1629                         break;
1630                     }
1631                     continue;
1632                 } else {
1633                     newLine = false;
1634                     index = 0;
1635                     // continue until end of line
1636                     if (nextByte == HttpConstants.CR) {
1637                         if (sao.pos < sao.limit) {
1638                             nextByte = sao.bytes[sao.pos ++];
1639                             if (nextByte == HttpConstants.LF) {
1640                                 newLine = true;
1641                                 index = 0;
1642                                 lastrealpos = sao.pos - 2;
1643                             } else {
1644                                 // unread next byte
1645                                 sao.pos--;
1646 
1647                                 // save last valid position
1648                                 lastrealpos = sao.pos;
1649                             }
1650                         }
1651                     } else if (nextByte == HttpConstants.LF) {
1652                         newLine = true;
1653                         index = 0;
1654                         lastrealpos = sao.pos - 1;
1655                     } else {
1656                         // save last valid position
1657                         lastrealpos = sao.pos;
1658                     }
1659                 }
1660             } else {
1661                 // continue until end of line
1662                 if (nextByte == HttpConstants.CR) {
1663                     if (sao.pos < sao.limit) {
1664                         nextByte = sao.bytes[sao.pos ++];
1665                         if (nextByte == HttpConstants.LF) {
1666                             newLine = true;
1667                             index = 0;
1668                             lastrealpos = sao.pos - 2;
1669                         } else {
1670                             // unread next byte
1671                             sao.pos--;
1672 
1673                             // save last valid position
1674                             lastrealpos = sao.pos;
1675                         }
1676                     }
1677                 } else if (nextByte == HttpConstants.LF) {
1678                     newLine = true;
1679                     index = 0;
1680                     lastrealpos = sao.pos - 1;
1681                 } else {
1682                     // save last valid position
1683                     lastrealpos = sao.pos;
1684                 }
1685             }
1686         }
1687         lastPosition = sao.getReadPosition(lastrealpos);
1688         ChannelBuffer buffer = undecodedChunk.slice(readerIndex, lastPosition - readerIndex);
1689         if (found) {
1690             // found so lastPosition is correct and final
1691             try {
1692                 currentFileUpload.addContent(buffer, true);
1693                 // just before the CRLF and delimiter
1694                 undecodedChunk.readerIndex(lastPosition);
1695             } catch (IOException e) {
1696                 throw new ErrorDataDecoderException(e);
1697             }
1698         } else {
1699             // possibly the delimiter is partially found but still the last position is OK
1700             try {
1701                 currentFileUpload.addContent(buffer, false);
1702                 // last valid char (not CR, not LF, not beginning of delimiter)
1703                 undecodedChunk.readerIndex(lastPosition);
1704                 throw new NotEnoughDataDecoderException();
1705             } catch (IOException e) {
1706                 throw new ErrorDataDecoderException(e);
1707             }
1708         }
1709     }
1710 
1711     /**
1712      * Load the field value from a Multipart request
1713      * @throws NotEnoughDataDecoderException Need more chunks
1714      * @throws ErrorDataDecoderException
1715      */
1716     private void loadFieldMultipartStandard(String delimiter)
1717             throws NotEnoughDataDecoderException, ErrorDataDecoderException {
1718         int readerIndex = undecodedChunk.readerIndex();
1719         try {
1720             // found the decoder limit
1721             boolean newLine = true;
1722             int index = 0;
1723             int lastPosition = undecodedChunk.readerIndex();
1724             boolean found = false;
1725             while (undecodedChunk.readable()) {
1726                 byte nextByte = undecodedChunk.readByte();
1727                 if (newLine) {
1728                     // Check the delimiter
1729                     if (nextByte == delimiter.codePointAt(index)) {
1730                         index ++;
1731                         if (delimiter.length() == index) {
1732                             found = true;
1733                             break;
1734                         }
1735                         continue;
1736                     } else {
1737                         newLine = false;
1738                         index = 0;
1739                         // continue until end of line
1740                         if (nextByte == HttpConstants.CR) {
1741                             if (undecodedChunk.readable()) {
1742                                 nextByte = undecodedChunk.readByte();
1743                                 if (nextByte == HttpConstants.LF) {
1744                                     newLine = true;
1745                                     index = 0;
1746                                     lastPosition = undecodedChunk.readerIndex() - 2;
1747                                 }
1748                             }
1749                         } else if (nextByte == HttpConstants.LF) {
1750                             newLine = true;
1751                             index = 0;
1752                             lastPosition = undecodedChunk.readerIndex() - 1;
1753                         } else {
1754                             lastPosition = undecodedChunk.readerIndex();
1755                         }
1756                     }
1757                 } else {
1758                     // continue until end of line
1759                     if (nextByte == HttpConstants.CR) {
1760                         if (undecodedChunk.readable()) {
1761                             nextByte = undecodedChunk.readByte();
1762                             if (nextByte == HttpConstants.LF) {
1763                                 newLine = true;
1764                                 index = 0;
1765                                 lastPosition = undecodedChunk.readerIndex() - 2;
1766                             }
1767                         }
1768                     } else if (nextByte == HttpConstants.LF) {
1769                         newLine = true;
1770                         index = 0;
1771                         lastPosition = undecodedChunk.readerIndex() - 1;
1772                     } else {
1773                         lastPosition = undecodedChunk.readerIndex();
1774                     }
1775                 }
1776             }
1777             if (found) {
1778                 // found so lastPosition is correct
1779                 // but position is just after the delimiter (either close delimiter or simple one)
1780                 // so go back of delimiter size
1781                 try {
1782                     currentAttribute.addContent(
1783                             undecodedChunk.slice(readerIndex, lastPosition - readerIndex),
1784                             true);
1785                 } catch (IOException e) {
1786                     throw new ErrorDataDecoderException(e);
1787                 }
1788                 undecodedChunk.readerIndex(lastPosition);
1789             } else {
1790                 try {
1791                     currentAttribute.addContent(
1792                             undecodedChunk.slice(readerIndex, lastPosition - readerIndex),
1793                             false);
1794                 } catch (IOException e) {
1795                     throw new ErrorDataDecoderException(e);
1796                 }
1797                 undecodedChunk.readerIndex(lastPosition);
1798                 throw new NotEnoughDataDecoderException();
1799             }
1800         } catch (IndexOutOfBoundsException e) {
1801             undecodedChunk.readerIndex(readerIndex);
1802             throw new NotEnoughDataDecoderException(e);
1803         }
1804     }
1805 
1806     /**
1807      * Load the field value from a Multipart request
1808      * @throws NotEnoughDataDecoderException Need more chunks
1809      * @throws ErrorDataDecoderException
1810      */
1811     private void loadFieldMultipart(String delimiter)
1812             throws NotEnoughDataDecoderException, ErrorDataDecoderException {
1813         SeekAheadOptimize sao;
1814         try {
1815             sao = new SeekAheadOptimize(undecodedChunk);
1816         } catch (SeekAheadNoBackArrayException e1) {
1817             loadFieldMultipartStandard(delimiter);
1818             return;
1819         }
1820         int readerIndex = undecodedChunk.readerIndex();
1821         try {
1822             // found the decoder limit
1823             boolean newLine = true;
1824             int index = 0;
1825             int lastPosition;
1826             int lastrealpos = sao.pos;
1827             boolean found = false;
1828 
1829             while (sao.pos < sao.limit) {
1830                 byte nextByte = sao.bytes[sao.pos ++];
1831                 if (newLine) {
1832                     // Check the delimiter
1833                     if (nextByte == delimiter.codePointAt(index)) {
1834                         index ++;
1835                         if (delimiter.length() == index) {
1836                             found = true;
1837                             break;
1838                         }
1839                         continue;
1840                     } else {
1841                         newLine = false;
1842                         index = 0;
1843                         // continue until end of line
1844                         if (nextByte == HttpConstants.CR) {
1845                             if (sao.pos < sao.limit) {
1846                                 nextByte = sao.bytes[sao.pos ++];
1847                                 if (nextByte == HttpConstants.LF) {
1848                                     newLine = true;
1849                                     index = 0;
1850                                     lastrealpos = sao.pos - 2;
1851                                 }
1852                             }
1853                         } else if (nextByte == HttpConstants.LF) {
1854                             newLine = true;
1855                             index = 0;
1856                             lastrealpos = sao.pos - 1;
1857                         } else {
1858                             lastrealpos = sao.pos;
1859                         }
1860                     }
1861                 } else {
1862                     // continue until end of line
1863                     if (nextByte == HttpConstants.CR) {
1864                         if (sao.pos < sao.limit) {
1865                             nextByte = sao.bytes[sao.pos ++];
1866                             if (nextByte == HttpConstants.LF) {
1867                                 newLine = true;
1868                                 index = 0;
1869                                 lastrealpos = sao.pos - 2;
1870                             }
1871                         }
1872                     } else if (nextByte == HttpConstants.LF) {
1873                         newLine = true;
1874                         index = 0;
1875                         lastrealpos = sao.pos - 1;
1876                     } else {
1877                         lastrealpos = sao.pos;
1878                     }
1879                 }
1880             }
1881             lastPosition = sao.getReadPosition(lastrealpos);
1882             if (found) {
1883                 // found so lastPosition is correct
1884                 // but position is just after the delimiter (either close delimiter or simple one)
1885                 // so go back of delimiter size
1886                 try {
1887                     currentAttribute.addContent(
1888                             undecodedChunk.slice(readerIndex, lastPosition - readerIndex), true);
1889                 } catch (IOException e) {
1890                     throw new ErrorDataDecoderException(e);
1891                 }
1892                 undecodedChunk.readerIndex(lastPosition);
1893             } else {
1894                 try {
1895                     currentAttribute.addContent(
1896                             undecodedChunk.slice(readerIndex, lastPosition - readerIndex), false);
1897                 } catch (IOException e) {
1898                     throw new ErrorDataDecoderException(e);
1899                 }
1900                 undecodedChunk.readerIndex(lastPosition);
1901                 throw new NotEnoughDataDecoderException();
1902             }
1903         } catch (IndexOutOfBoundsException e) {
1904             undecodedChunk.readerIndex(readerIndex);
1905             throw new NotEnoughDataDecoderException(e);
1906         }
1907     }
1908 
1909     /**
1910      * Clean the String from any unallowed character
1911      * @return the cleaned String
1912      */
1913     private static String cleanString(String field) {
1914         StringBuilder sb = new StringBuilder(field.length());
1915         for (int i = 0; i < field.length(); i ++) {
1916             char nextChar = field.charAt(i);
1917             if (nextChar == HttpConstants.COLON) {
1918                 sb.append(HttpConstants.SP);
1919             } else if (nextChar == HttpConstants.COMMA) {
1920                 sb.append(HttpConstants.SP);
1921             } else if (nextChar == HttpConstants.EQUALS) {
1922                 sb.append(HttpConstants.SP);
1923             } else if (nextChar == HttpConstants.SEMICOLON) {
1924                 sb.append(HttpConstants.SP);
1925             } else if (nextChar == HttpConstants.HT) {
1926                 sb.append(HttpConstants.SP);
1927             } else if (nextChar == HttpConstants.DOUBLE_QUOTE) {
1928                 // nothing added, just removes it
1929             } else {
1930                 sb.append(nextChar);
1931             }
1932         }
1933         return sb.toString().trim();
1934     }
1935 
1936     /**
1937      * Skip one empty line
1938      * @return True if one empty line was skipped
1939      */
1940     private boolean skipOneLine() {
1941         if (!undecodedChunk.readable()) {
1942             return false;
1943         }
1944         byte nextByte = undecodedChunk.readByte();
1945         if (nextByte == HttpConstants.CR) {
1946             if (!undecodedChunk.readable()) {
1947                 undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
1948                 return false;
1949             }
1950             nextByte = undecodedChunk.readByte();
1951             if (nextByte == HttpConstants.LF) {
1952                 return true;
1953             }
1954             undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 2);
1955             return false;
1956         }
1957         if (nextByte == HttpConstants.LF) {
1958             return true;
1959         }
1960         undecodedChunk.readerIndex(undecodedChunk.readerIndex() - 1);
1961         return false;
1962     }
1963 
1964     /**
1965      * Split the very first line (Content-Type value) in 2 Strings
1966      * @return the array of 2 Strings
1967      */
1968     private static String[] splitHeaderContentType(String sb) {
1969         int size = sb.length();
1970         int aStart;
1971         int aEnd;
1972         int bStart;
1973         int bEnd;
1974         aStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
1975         aEnd = HttpPostBodyUtil.findWhitespace(sb, aStart);
1976         if (aEnd >= size) {
1977             return new String[] { sb, "" };
1978         }
1979         if (sb.charAt(aEnd) == ';') {
1980             aEnd --;
1981         }
1982         bStart = HttpPostBodyUtil.findNonWhitespace(sb, aEnd);
1983         bEnd = HttpPostBodyUtil.findEndOfString(sb);
1984         return new String[] { sb.substring(aStart, aEnd),
1985                 sb.substring(bStart, bEnd) };
1986     }
1987 
1988     /**
1989      * Split one header in Multipart
1990      * @return an array of String where rank 0 is the name of the header, follows by several
1991      *  values that were separated by ';' or ','
1992      */
1993     private static String[] splitMultipartHeader(String sb) {
1994         ArrayList<String> headers = new ArrayList<String>(1);
1995         int nameStart;
1996         int nameEnd;
1997         int colonEnd;
1998         int valueStart;
1999         int valueEnd;
2000         nameStart = HttpPostBodyUtil.findNonWhitespace(sb, 0);
2001         for (nameEnd = nameStart; nameEnd < sb.length(); nameEnd ++) {
2002             char ch = sb.charAt(nameEnd);
2003             if (ch == ':' || Character.isWhitespace(ch)) {
2004                 break;
2005             }
2006         }
2007         for (colonEnd = nameEnd; colonEnd < sb.length(); colonEnd ++) {
2008             if (sb.charAt(colonEnd) == ':') {
2009                 colonEnd ++;
2010                 break;
2011             }
2012         }
2013         valueStart = HttpPostBodyUtil.findNonWhitespace(sb, colonEnd);
2014         valueEnd = HttpPostBodyUtil.findEndOfString(sb);
2015         headers.add(sb.substring(nameStart, nameEnd));
2016         String svalue = sb.substring(valueStart, valueEnd);
2017         String[] values;
2018         if (svalue.indexOf(';') >= 0) {
2019             values = StringUtil.split(svalue, ';');
2020         } else {
2021             values = StringUtil.split(svalue, ',');
2022         }
2023         for (String value: values) {
2024             headers.add(value.trim());
2025         }
2026         String[] array = new String[headers.size()];
2027         for (int i = 0; i < headers.size(); i ++) {
2028             array[i] = headers.get(i);
2029         }
2030         return array;
2031     }
2032 
2033     /**
2034      * Exception when try reading data from request in chunked format, and not enough
2035      * data are available (need more chunks)
2036      */
2037     public static class NotEnoughDataDecoderException extends Exception {
2038         private static final long serialVersionUID = -7846841864603865638L;
2039 
2040         public NotEnoughDataDecoderException() {
2041         }
2042 
2043         public NotEnoughDataDecoderException(String msg) {
2044             super(msg);
2045         }
2046 
2047         public NotEnoughDataDecoderException(Throwable cause) {
2048             super(cause);
2049         }
2050 
2051         public NotEnoughDataDecoderException(String msg, Throwable cause) {
2052             super(msg, cause);
2053         }
2054     }
2055 
2056     /**
2057      * Exception when the body is fully decoded, even if there is still data
2058      */
2059     public static class EndOfDataDecoderException extends Exception {
2060         private static final long serialVersionUID = 1336267941020800769L;
2061     }
2062 
2063     /**
2064      * Exception when an error occurs while decoding
2065      */
2066     public static class ErrorDataDecoderException extends Exception {
2067         private static final long serialVersionUID = 5020247425493164465L;
2068 
2069         public ErrorDataDecoderException() {
2070         }
2071 
2072         public ErrorDataDecoderException(String msg) {
2073             super(msg);
2074         }
2075 
2076         public ErrorDataDecoderException(Throwable cause) {
2077             super(cause);
2078         }
2079 
2080         public ErrorDataDecoderException(String msg, Throwable cause) {
2081             super(msg, cause);
2082         }
2083     }
2084 
2085     /**
2086      * Exception when an unappropriated method was called on a request
2087      */
2088     public static class IncompatibleDataDecoderException extends Exception {
2089         private static final long serialVersionUID = -953268047926250267L;
2090 
2091         public IncompatibleDataDecoderException() {
2092         }
2093 
2094         public IncompatibleDataDecoderException(String msg) {
2095             super(msg);
2096         }
2097 
2098         public IncompatibleDataDecoderException(Throwable cause) {
2099             super(cause);
2100         }
2101 
2102         public IncompatibleDataDecoderException(String msg, Throwable cause) {
2103             super(msg, cause);
2104         }
2105     }
2106 }