View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.util;
20  
21  import static org.eclipse.jetty.util.TypeUtil.convertHexDigit;
22  
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InputStreamReader;
26  import java.io.StringWriter;
27  import java.nio.charset.Charset;
28  import java.nio.charset.StandardCharsets;
29  import java.util.List;
30  import java.util.Map;
31  
32  import org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception;
33  import org.eclipse.jetty.util.log.Log;
34  import org.eclipse.jetty.util.log.Logger;
35  
36  /* ------------------------------------------------------------ */
37  /** Handles coding of MIME  "x-www-form-urlencoded".
38   * <p>
39   * This class handles the encoding and decoding for either
40   * the query string of a URL or the _content of a POST HTTP request.
41   *
42   * <h4>Notes</h4>
43   * The UTF-8 charset is assumed, unless otherwise defined by either
44   * passing a parameter or setting the "org.eclipse.jetty.util.UrlEncoding.charset"
45   * System property.
46   * <p>
47   * The hashtable either contains String single values, vectors
48   * of String or arrays of Strings.
49   * <p>
50   * This class is only partially synchronised.  In particular, simple
51   * get operations are not protected from concurrent updates.
52   *
53   * @see java.net.URLEncoder
54   */
55  @SuppressWarnings("serial")
56  public class UrlEncoded extends MultiMap<String> implements Cloneable
57  {
58      static final Logger LOG = Log.getLogger(UrlEncoded.class);
59  
60      public static final Charset ENCODING;
61      static
62      {
63          Charset encoding;
64          try
65          {
66              String charset = System.getProperty("org.eclipse.jetty.util.UrlEncoding.charset");
67              encoding = charset == null ? StandardCharsets.UTF_8 : Charset.forName(charset);
68          }
69          catch(Exception e)
70          {
71              LOG.warn(e);
72              encoding=StandardCharsets.UTF_8;
73          }
74          ENCODING=encoding;
75      }
76      
77      /* ----------------------------------------------------------------- */
78      public UrlEncoded(UrlEncoded url)
79      {
80          super(url);
81      }
82      
83      /* ----------------------------------------------------------------- */
84      public UrlEncoded()
85      {
86      }
87      
88      /* ----------------------------------------------------------------- */
89      public void decode(String query)
90      {
91          decodeTo(query,this,ENCODING,-1);
92      }
93      
94      /* ----------------------------------------------------------------- */
95      public void decode(String query,Charset charset)
96      {
97          decodeTo(query,this,charset,-1);
98      }
99      
100     /* -------------------------------------------------------------- */
101     /** Encode Hashtable with % encoding.
102      */
103     public String encode()
104     {
105         return encode(ENCODING,false);
106     }
107     
108     /* -------------------------------------------------------------- */
109     /** Encode Hashtable with % encoding.
110      */
111     public String encode(Charset charset)
112     {
113         return encode(charset,false);
114     }
115     
116     /* -------------------------------------------------------------- */
117     /** Encode Hashtable with % encoding.
118      * @param equalsForNullValue if True, then an '=' is always used, even
119      * for parameters without a value. e.g. "blah?a=&b=&c=".
120      */
121     public synchronized String encode(Charset charset, boolean equalsForNullValue)
122     {
123         return encode(this,charset,equalsForNullValue);
124     }
125     
126     /* -------------------------------------------------------------- */
127     /** Encode Hashtable with % encoding.
128      * @param equalsForNullValue if True, then an '=' is always used, even
129      * for parameters without a value. e.g. "blah?a=&b=&c=".
130      */
131     public static String encode(MultiMap<String> map, Charset charset, boolean equalsForNullValue)
132     {
133         if (charset==null)
134             charset=ENCODING;
135 
136         StringBuilder result = new StringBuilder(128);
137 
138         boolean delim = false;
139         for(Map.Entry<String, List<String>> entry: map.entrySet())
140         {
141             String key = entry.getKey().toString();
142             List<String> list = entry.getValue();
143             int s=list.size();
144             
145             if (delim)
146             {
147                 result.append('&');
148             }
149 
150             if (s==0)
151             {
152                 result.append(encodeString(key,charset));
153                 if(equalsForNullValue)
154                     result.append('=');
155             }
156             else
157             {
158                 for (int i=0;i<s;i++)
159                 {
160                     if (i>0)
161                         result.append('&');
162                     String val=list.get(i);
163                     result.append(encodeString(key,charset));
164 
165                     if (val!=null)
166                     {
167                         String str=val.toString();
168                         if (str.length()>0)
169                         {
170                             result.append('=');
171                             result.append(encodeString(str,charset));
172                         }
173                         else if (equalsForNullValue)
174                             result.append('=');
175                     }
176                     else if (equalsForNullValue)
177                         result.append('=');
178                 }
179             }
180             delim = true;
181         }
182         return result.toString();
183     }
184 
185     /* -------------------------------------------------------------- */
186     /** Decoded parameters to Map.
187      * @param content the string containing the encoded parameters
188      */
189     public static void decodeTo(String content, MultiMap<String> map, String charset, int maxKeys)
190     {
191         decodeTo(content,map,charset==null?null:Charset.forName(charset),maxKeys);
192     }
193     
194     /* -------------------------------------------------------------- */
195     /** Decoded parameters to Map.
196      * @param content the string containing the encoded parameters
197      */
198     public static void decodeTo(String content, MultiMap<String> map, Charset charset, int maxKeys)
199     {
200         if (charset==null)
201             charset=ENCODING;
202 
203         synchronized(map)
204         {
205             String key = null;
206             String value = null;
207             int mark=-1;
208             boolean encoded=false;
209             for (int i=0;i<content.length();i++)
210             {
211                 char c = content.charAt(i);
212                 switch (c)
213                 {
214                   case '&':
215                       int l=i-mark-1;
216                       value = l==0?"":
217                           (encoded?decodeString(content,mark+1,l,charset):content.substring(mark+1,i));
218                       mark=i;
219                       encoded=false;
220                       if (key != null)
221                       {
222                           map.add(key,value);
223                       }
224                       else if (value!=null&&value.length()>0)
225                       {
226                           map.add(value,"");
227                       }
228                       key = null;
229                       value=null;
230                       if (maxKeys>0 && map.size()>maxKeys)
231                           throw new IllegalStateException("Form too many keys");
232                       break;
233                   case '=':
234                       if (key!=null)
235                           break;
236                       key = encoded?decodeString(content,mark+1,i-mark-1,charset):content.substring(mark+1,i);
237                       mark=i;
238                       encoded=false;
239                       break;
240                   case '+':
241                       encoded=true;
242                       break;
243                   case '%':
244                       encoded=true;
245                       break;
246                 }                
247             }
248             
249             if (key != null)
250             {
251                 int l=content.length()-mark-1;
252                 value = l==0?"":(encoded?decodeString(content,mark+1,l,charset):content.substring(mark+1));
253                 map.add(key,value);
254             }
255             else if (mark<content.length())
256             {
257                 key = encoded
258                     ?decodeString(content,mark+1,content.length()-mark-1,charset)
259                     :content.substring(mark+1);
260                 if (key != null && key.length() > 0)
261                 {
262                     map.add(key,"");
263                 }
264             }
265         }
266     }
267 
268     /* -------------------------------------------------------------- */
269     /** Decoded parameters to Map.
270      * @param raw the byte[] containing the encoded parameters
271      * @param offset the offset within raw to decode from
272      * @param length the length of the section to decode
273      * @param map the {@link MultiMap} to populate
274      */
275     public static void decodeUtf8To(byte[] raw,int offset, int length, MultiMap<String> map)
276     {
277         Utf8StringBuilder buffer = new Utf8StringBuilder();
278         synchronized(map)
279         {
280             String key = null;
281             String value = null;
282 
283             int end=offset+length;
284             for (int i=offset;i<end;i++)
285             {
286                 byte b=raw[i];
287                 try
288                 {
289                     switch ((char)(0xff&b))
290                     {
291                         case '&':
292                             value = buffer.length()==0?"":buffer.toString();
293                             buffer.reset();
294                             if (key != null)
295                             {
296                                 map.add(key,value);
297                             }
298                             else if (value!=null&&value.length()>0)
299                             {
300                                 map.add(value,"");
301                             }
302                             key = null;
303                             value=null;
304                             break;
305 
306                         case '=':
307                             if (key!=null)
308                             {
309                                 buffer.append(b);
310                                 break;
311                             }
312                             key = buffer.toString();
313                             buffer.reset();
314                             break;
315 
316                         case '+':
317                             buffer.append((byte)' ');
318                             break;
319 
320                         case '%':
321                             if (i+2<end)
322                             {
323                                 if ('u'==raw[i+1])
324                                 {
325                                     i++;
326                                     if (i+4<end)
327                                     {
328                                         byte top=raw[++i];
329                                         byte hi=raw[++i];
330                                         byte lo=raw[++i];
331                                         byte bot=raw[++i];
332                                         buffer.getStringBuilder().append(Character.toChars((convertHexDigit(top)<<12) +(convertHexDigit(hi)<<8) + (convertHexDigit(lo)<<4) +convertHexDigit(bot)));
333                                     }
334                                     else
335                                     {
336                                         buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
337                                         i=end;
338                                     }
339                                 }
340                                 else
341                                 {
342                                     byte hi=raw[++i];
343                                     byte lo=raw[++i];
344                                     buffer.append((byte)((convertHexDigit(hi)<<4) + convertHexDigit(lo)));
345                                 }
346                             }
347                             else
348                             {
349                                 buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
350                                 i=end;
351                             }
352                             break;
353                             
354                         default:
355                             buffer.append(b);
356                             break;
357                     }
358                 }
359                 catch(NotUtf8Exception e)
360                 {
361                     LOG.warn(e.toString());
362                     LOG.debug(e);
363                 }
364                 catch(NumberFormatException e)
365                 {
366                     buffer.append(Utf8Appendable.REPLACEMENT_UTF8,0,3);
367                     LOG.warn(e.toString());
368                     LOG.debug(e);
369                 }
370             }
371             
372             if (key != null)
373             {
374                 value = buffer.length()==0?"":buffer.toReplacedString();
375                 buffer.reset();
376                 map.add(key,value);
377             }
378             else if (buffer.length()>0)
379             {
380                 map.add(buffer.toReplacedString(),"");
381             }
382         }
383     }
384 
385     /* -------------------------------------------------------------- */
386     /** Decoded parameters to Map.
387      * @param in InputSteam to read
388      * @param map MultiMap to add parameters to
389      * @param maxLength maximum number of keys to read or -1 for no limit
390      */
391     public static void decode88591To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
392     throws IOException
393     {
394         synchronized(map)
395         {
396             StringBuffer buffer = new StringBuffer();
397             String key = null;
398             String value = null;
399             
400             int b;
401 
402             int totalLength=0;
403             while ((b=in.read())>=0)
404             {
405                 switch ((char) b)
406                 {
407                     case '&':
408                         value = buffer.length()==0?"":buffer.toString();
409                         buffer.setLength(0);
410                         if (key != null)
411                         {
412                             map.add(key,value);
413                         }
414                         else if (value!=null&&value.length()>0)
415                         {
416                             map.add(value,"");
417                         }
418                         key = null;
419                         value=null;
420                         if (maxKeys>0 && map.size()>maxKeys)
421                             throw new IllegalStateException("Form too many keys");
422                         break;
423                         
424                     case '=':
425                         if (key!=null)
426                         {
427                             buffer.append((char)b);
428                             break;
429                         }
430                         key = buffer.toString();
431                         buffer.setLength(0);
432                         break;
433                         
434                     case '+':
435                         buffer.append(' ');
436                         break;
437                         
438                     case '%':
439                         int code0=in.read();
440                         if ('u'==code0)
441                         {
442                             int code1=in.read();
443                             if (code1>=0)
444                             {
445                                 int code2=in.read();
446                                 if (code2>=0)
447                                 {
448                                     int code3=in.read();
449                                     if (code3>=0)
450                                         buffer.append(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3)));
451                                 }
452                             }
453                         }
454                         else if (code0>=0)
455                         {
456                             int code1=in.read();
457                             if (code1>=0)
458                                 buffer.append((char)((convertHexDigit(code0)<<4)+convertHexDigit(code1)));
459                         }
460                         break;
461                      
462                     default:
463                         buffer.append((char)b);
464                     break;
465                 }
466                 if (maxLength>=0 && (++totalLength > maxLength))
467                     throw new IllegalStateException("Form too large");
468             }
469             
470             if (key != null)
471             {
472                 value = buffer.length()==0?"":buffer.toString();
473                 buffer.setLength(0);
474                 map.add(key,value);
475             }
476             else if (buffer.length()>0)
477             {
478                 map.add(buffer.toString(), "");
479             }
480         }
481     }
482     
483     /* -------------------------------------------------------------- */
484     /** Decoded parameters to Map.
485      * @param in InputSteam to read
486      * @param map MultiMap to add parameters to
487      * @param maxLength maximum number of keys to read or -1 for no limit
488      */
489     public static void decodeUtf8To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
490     throws IOException
491     {
492         synchronized(map)
493         {
494             Utf8StringBuilder buffer = new Utf8StringBuilder();
495             String key = null;
496             String value = null;
497             
498             int b;
499             
500             int totalLength=0;
501             while ((b=in.read())>=0)
502             {
503                 try
504                 {
505                     switch ((char) b)
506                     {
507                         case '&':
508                             value = buffer.length()==0?"":buffer.toString();
509                             buffer.reset();
510                             if (key != null)
511                             {
512                                 map.add(key,value);
513                             }
514                             else if (value!=null&&value.length()>0)
515                             {
516                                 map.add(value,"");
517                             }
518                             key = null;
519                             value=null;
520                             if (maxKeys>0 && map.size()>maxKeys)
521                                 throw new IllegalStateException("Form too many keys");
522                             break;
523 
524                         case '=':
525                             if (key!=null)
526                             {
527                                 buffer.append((byte)b);
528                                 break;
529                             }
530                             key = buffer.toString();
531                             buffer.reset();
532                             break;
533 
534                         case '+':
535                             buffer.append((byte)' ');
536                             break;
537 
538                         case '%':
539                             int code0=in.read();
540                             if ('u'==code0)
541                             {
542                                 int code1=in.read();
543                                 if (code1>=0)
544                                 {
545                                     int code2=in.read();
546                                     if (code2>=0)
547                                     {
548                                         int code3=in.read();
549                                         if (code3>=0)
550                                             buffer.getStringBuilder().append(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3)));
551                                     }
552                                 }
553                             }
554                             else if (code0>=0)
555                             {
556                                 int code1=in.read();
557                                 if (code1>=0)
558                                     buffer.append((byte)((convertHexDigit(code0)<<4)+convertHexDigit(code1)));
559                             }
560                             break;
561                           
562                         default:
563                             buffer.append((byte)b);
564                             break;
565                     }
566                 }
567                 catch(NotUtf8Exception e)
568                 {
569                     LOG.warn(e.toString());
570                     LOG.debug(e);
571                 }
572                 catch(NumberFormatException e)
573                 {
574                     buffer.append(Utf8Appendable.REPLACEMENT_UTF8,0,3);
575                     LOG.warn(e.toString());
576                     LOG.debug(e);
577                 }
578                 if (maxLength>=0 && (++totalLength > maxLength))
579                     throw new IllegalStateException("Form too large");
580             }
581             
582             if (key != null)
583             {
584                 value = buffer.length()==0?"":buffer.toString();
585                 buffer.reset();
586                 map.add(key,value);
587             }
588             else if (buffer.length()>0)
589             {
590                 map.add(buffer.toString(), "");
591             }
592         }
593     }
594     
595     /* -------------------------------------------------------------- */
596     public static void decodeUtf16To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys) throws IOException
597     {
598         InputStreamReader input = new InputStreamReader(in,StandardCharsets.UTF_16);
599         StringWriter buf = new StringWriter(8192);
600         IO.copy(input,buf,maxLength);
601         
602         decodeTo(buf.getBuffer().toString(),map,StandardCharsets.UTF_16,maxKeys);
603     }
604 
605     /* -------------------------------------------------------------- */
606     /** Decoded parameters to Map.
607      * @param in the stream containing the encoded parameters
608      */
609     public static void decodeTo(InputStream in, MultiMap<String> map, String charset, int maxLength, int maxKeys)
610     throws IOException
611     {
612         if (charset==null)
613         {
614             if (ENCODING.equals(StandardCharsets.UTF_8))
615                 decodeUtf8To(in,map,maxLength,maxKeys);
616             else
617                 decodeTo(in,map,ENCODING,maxLength,maxKeys);
618         }
619         else if (StringUtil.__UTF8.equalsIgnoreCase(charset))
620             decodeUtf8To(in,map,maxLength,maxKeys);
621         else if (StringUtil.__ISO_8859_1.equalsIgnoreCase(charset))
622             decode88591To(in,map,maxLength,maxKeys);
623         else if (StringUtil.__UTF16.equalsIgnoreCase(charset))
624             decodeUtf16To(in,map,maxLength,maxKeys);
625         else
626             decodeTo(in,map,Charset.forName(charset),maxLength,maxKeys);
627     }
628     
629     /* -------------------------------------------------------------- */
630     /** Decoded parameters to Map.
631      * @param in the stream containing the encoded parameters
632      */
633     public static void decodeTo(InputStream in, MultiMap<String> map, Charset charset, int maxLength, int maxKeys)
634     throws IOException
635     {
636         //no charset present, use the configured default
637         if (charset==null) 
638            charset=ENCODING;
639             
640         if (StandardCharsets.UTF_8.equals(charset))
641         {
642             decodeUtf8To(in,map,maxLength,maxKeys);
643             return;
644         }
645         
646         if (StandardCharsets.ISO_8859_1.equals(charset))
647         {
648             decode88591To(in,map,maxLength,maxKeys);
649             return;
650         }
651 
652         if (StandardCharsets.UTF_16.equals(charset)) // Should be all 2 byte encodings
653         {
654             decodeUtf16To(in,map,maxLength,maxKeys);
655             return;
656         }
657 
658         synchronized(map)
659         {
660             String key = null;
661             String value = null;
662             
663             int c;
664             
665             int totalLength = 0;
666             ByteArrayOutputStream2 output = new ByteArrayOutputStream2();
667             
668             int size=0;
669             
670             while ((c=in.read())>0)
671             {
672                 switch ((char) c)
673                 {
674                     case '&':
675                         size=output.size();
676                         value = size==0?"":output.toString(charset);
677                         output.setCount(0);
678                         if (key != null)
679                         {
680                             map.add(key,value);
681                         }
682                         else if (value!=null&&value.length()>0)
683                         {
684                             map.add(value,"");
685                         }
686                         key = null;
687                         value=null;
688                         if (maxKeys>0 && map.size()>maxKeys)
689                             throw new IllegalStateException("Form too many keys");
690                         break;
691                     case '=':
692                         if (key!=null)
693                         {
694                             output.write(c);
695                             break;
696                         }
697                         size=output.size();
698                         key = size==0?"":output.toString(charset);
699                         output.setCount(0);
700                         break;
701                     case '+':
702                         output.write(' ');
703                         break;
704                     case '%':
705                         int code0=in.read();
706                         if ('u'==code0)
707                         {
708                             int code1=in.read();
709                             if (code1>=0)
710                             {
711                                 int code2=in.read();
712                                 if (code2>=0)
713                                 {
714                                     int code3=in.read();
715                                     if (code3>=0)
716                                         output.write(new String(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3))).getBytes(charset));
717                                 }
718                             }
719                             
720                         }
721                         else if (code0>=0)
722                         {
723                             int code1=in.read();
724                             if (code1>=0)
725                                 output.write((convertHexDigit(code0)<<4)+convertHexDigit(code1));
726                         }
727                         break;
728                     default:
729                         output.write(c);
730                     break;
731                 }
732                 
733                 totalLength++;
734                 if (maxLength>=0 && totalLength > maxLength)
735                     throw new IllegalStateException("Form too large");
736             }
737 
738             size=output.size();
739             if (key != null)
740             {
741                 value = size==0?"":output.toString(charset);
742                 output.setCount(0);
743                 map.add(key,value);
744             }
745             else if (size>0)
746                 map.add(output.toString(charset),"");
747         }
748     }
749     
750     /* -------------------------------------------------------------- */
751     /** Decode String with % encoding.
752      * This method makes the assumption that the majority of calls
753      * will need no decoding.
754      */
755     public static String decodeString(String encoded,int offset,int length,Charset charset)
756     {
757         if (charset==null || StandardCharsets.UTF_8.equals(charset))
758         {
759             Utf8StringBuffer buffer=null;
760 
761             for (int i=0;i<length;i++)
762             {
763                 char c = encoded.charAt(offset+i);
764                 if (c<0||c>0xff)
765                 {
766                     if (buffer==null)
767                     {
768                         buffer=new Utf8StringBuffer(length);
769                         buffer.getStringBuffer().append(encoded,offset,offset+i+1);
770                     }
771                     else
772                         buffer.getStringBuffer().append(c);
773                 }
774                 else if (c=='+')
775                 {
776                     if (buffer==null)
777                     {
778                         buffer=new Utf8StringBuffer(length);
779                         buffer.getStringBuffer().append(encoded,offset,offset+i);
780                     }
781                     
782                     buffer.getStringBuffer().append(' ');
783                 }
784                 else if (c=='%')
785                 {
786                     if (buffer==null)
787                     {
788                         buffer=new Utf8StringBuffer(length);
789                         buffer.getStringBuffer().append(encoded,offset,offset+i);
790                     }
791                     
792                     if ((i+2)<length)
793                     {
794                         try
795                         {
796                             if ('u'==encoded.charAt(offset+i+1))
797                             {
798                                 if((i+5)<length)
799                                 {
800                                     int o=offset+i+2;
801                                     i+=5;
802                                     String unicode = new String(Character.toChars(TypeUtil.parseInt(encoded,o,4,16)));
803                                     buffer.getStringBuffer().append(unicode); 
804                                 }
805                                 else
806                                 {
807                                     i=length;
808                                     buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT); 
809                                 }
810                             }
811                             else
812                             {
813                                 int o=offset+i+1;
814                                 i+=2;
815                                 byte b=(byte)TypeUtil.parseInt(encoded,o,2,16);
816                                 buffer.append(b);
817                             }
818                         }
819                         catch(NotUtf8Exception e)
820                         {
821                             LOG.warn(e.toString());
822                             LOG.debug(e);
823                         }
824                         catch(NumberFormatException e)
825                         {
826                             LOG.warn(e.toString());
827                             LOG.debug(e);
828                             buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT);  
829                         }
830                     }
831                     else
832                     {
833                         buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT); 
834                         i=length;
835                     }
836                 }
837                 else if (buffer!=null)
838                     buffer.getStringBuffer().append(c);
839             }
840 
841             if (buffer==null)
842             {
843                 if (offset==0 && encoded.length()==length)
844                     return encoded;
845                 return encoded.substring(offset,offset+length);
846             }
847 
848             return buffer.toReplacedString();
849         }
850         else
851         {
852             StringBuffer buffer=null;
853 
854             for (int i=0;i<length;i++)
855             {
856                 char c = encoded.charAt(offset+i);
857                 if (c<0||c>0xff)
858                 {
859                     if (buffer==null)
860                     {
861                         buffer=new StringBuffer(length);
862                         buffer.append(encoded,offset,offset+i+1);
863                     }
864                     else
865                         buffer.append(c);
866                 }
867                 else if (c=='+')
868                 {
869                     if (buffer==null)
870                     {
871                         buffer=new StringBuffer(length);
872                         buffer.append(encoded,offset,offset+i);
873                     }
874 
875                     buffer.append(' ');
876                 }
877                 else if (c=='%')
878                 {
879                     if (buffer==null)
880                     {
881                         buffer=new StringBuffer(length);
882                         buffer.append(encoded,offset,offset+i);
883                     }
884 
885                     byte[] ba=new byte[length];
886                     int n=0;
887                     while(c>=0 && c<=0xff)
888                     {
889                         if (c=='%')
890                         {   
891                             if(i+2<length)
892                             {
893                                 try
894                                 {
895                                     if ('u'==encoded.charAt(offset+i+1))
896                                     {
897                                         if (i+6<length)
898                                         {
899                                             int o=offset+i+2;
900                                             i+=6;
901                                             String unicode = new String(Character.toChars(TypeUtil.parseInt(encoded,o,4,16)));
902                                             byte[] reencoded = unicode.getBytes(charset);
903                                             System.arraycopy(reencoded,0,ba,n,reencoded.length);
904                                             n+=reencoded.length;
905                                         }
906                                         else
907                                         {
908                                             ba[n++] = (byte)'?';
909                                             i=length;
910                                         }
911                                     }
912                                     else
913                                     {
914                                         int o=offset+i+1;
915                                         i+=3;
916                                         ba[n]=(byte)TypeUtil.parseInt(encoded,o,2,16);
917                                         n++;
918                                     }
919                                 }
920                                 catch(Exception e)
921                                 {   
922                                     LOG.warn(e.toString());
923                                     LOG.debug(e);
924                                     ba[n++] = (byte)'?';
925                                 }
926                             }
927                             else
928                             {
929                                     ba[n++] = (byte)'?';
930                                     i=length;
931                             }
932                         }
933                         else if (c=='+')
934                         {
935                             ba[n++]=(byte)' ';
936                             i++;
937                         }
938                         else
939                         {
940                             ba[n++]=(byte)c;
941                             i++;
942                         }
943 
944                         if (i>=length)
945                             break;
946                         c = encoded.charAt(offset+i);
947                     }
948 
949                     i--;
950                     buffer.append(new String(ba,0,n,charset));
951 
952                 }
953                 else if (buffer!=null)
954                     buffer.append(c);
955             }
956 
957             if (buffer==null)
958             {
959                 if (offset==0 && encoded.length()==length)
960                     return encoded;
961                 return encoded.substring(offset,offset+length);
962             }
963 
964             return buffer.toString();
965         }
966 
967     }
968     
969     /* ------------------------------------------------------------ */
970     /** Perform URL encoding.
971      * @param string 
972      * @return encoded string.
973      */
974     public static String encodeString(String string)
975     {
976         return encodeString(string,ENCODING);
977     }
978     
979     /* ------------------------------------------------------------ */
980     /** Perform URL encoding.
981      * @param string 
982      * @return encoded string.
983      */
984     public static String encodeString(String string,Charset charset)
985     {
986         if (charset==null)
987             charset=ENCODING;
988         byte[] bytes=null;
989         bytes=string.getBytes(charset);
990         
991         int len=bytes.length;
992         byte[] encoded= new byte[bytes.length*3];
993         int n=0;
994         boolean noEncode=true;
995         
996         for (int i=0;i<len;i++)
997         {
998             byte b = bytes[i];
999             
1000             if (b==' ')
1001             {
1002                 noEncode=false;
1003                 encoded[n++]=(byte)'+';
1004             }
1005             else if (b>='a' && b<='z' ||
1006                      b>='A' && b<='Z' ||
1007                      b>='0' && b<='9')
1008             {
1009                 encoded[n++]=b;
1010             }
1011             else
1012             {
1013                 noEncode=false;
1014                 encoded[n++]=(byte)'%';
1015                 byte nibble= (byte) ((b&0xf0)>>4);
1016                 if (nibble>=10)
1017                     encoded[n++]=(byte)('A'+nibble-10);
1018                 else
1019                     encoded[n++]=(byte)('0'+nibble);
1020                 nibble= (byte) (b&0xf);
1021                 if (nibble>=10)
1022                     encoded[n++]=(byte)('A'+nibble-10);
1023                 else
1024                     encoded[n++]=(byte)('0'+nibble);
1025             }
1026         }
1027 
1028         if (noEncode)
1029             return string;
1030         
1031         return new String(encoded,0,n,charset);
1032     }
1033 
1034 
1035     /* ------------------------------------------------------------ */
1036     /** 
1037      */
1038     @Override
1039     public Object clone()
1040     {
1041         return new UrlEncoded(this);
1042     }
1043 }