View Javadoc

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