View Javadoc

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