View Javadoc

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