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