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                                         buffer.getStringBuilder().append(Character.toChars((convertHexDigit(raw[++i])<<12) +(convertHexDigit(raw[++i])<<8) + (convertHexDigit(raw[++i])<<4) +convertHexDigit(raw[++i])));
327                                     else
328                                     {
329                                         buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
330                                         i=end;
331                                     }
332                                 }
333                                 else
334                                     buffer.append((byte)((convertHexDigit(raw[++i])<<4) + convertHexDigit(raw[++i])));
335                             }
336                             else
337                             {
338                                 buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
339                                 i=end;
340                             }
341                             break;
342                             
343                         default:
344                             buffer.append(b);
345                             break;
346                     }
347                 }
348                 catch(NotUtf8Exception e)
349                 {
350                     LOG.warn(e.toString());
351                     LOG.debug(e);
352                 }
353             }
354             
355             if (key != null)
356             {
357                 value = buffer.length()==0?"":buffer.toReplacedString();
358                 buffer.reset();
359                 map.add(key,value);
360             }
361             else if (buffer.length()>0)
362             {
363                 map.add(buffer.toReplacedString(),"");
364             }
365         }
366     }
367 
368     /* -------------------------------------------------------------- */
369     /** Decoded parameters to Map.
370      * @param in InputSteam to read
371      * @param map MultiMap to add parameters to
372      * @param maxLength maximum number of keys to read or -1 for no limit
373      */
374     public static void decode88591To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
375     throws IOException
376     {
377         synchronized(map)
378         {
379             StringBuffer buffer = new StringBuffer();
380             String key = null;
381             String value = null;
382             
383             int b;
384 
385             int totalLength=0;
386             while ((b=in.read())>=0)
387             {
388                 switch ((char) b)
389                 {
390                     case '&':
391                         value = buffer.length()==0?"":buffer.toString();
392                         buffer.setLength(0);
393                         if (key != null)
394                         {
395                             map.add(key,value);
396                         }
397                         else if (value!=null&&value.length()>0)
398                         {
399                             map.add(value,"");
400                         }
401                         key = null;
402                         value=null;
403                         if (maxKeys>0 && map.size()>maxKeys)
404                             throw new IllegalStateException("Form too many keys");
405                         break;
406                         
407                     case '=':
408                         if (key!=null)
409                         {
410                             buffer.append((char)b);
411                             break;
412                         }
413                         key = buffer.toString();
414                         buffer.setLength(0);
415                         break;
416                         
417                     case '+':
418                         buffer.append(' ');
419                         break;
420                         
421                     case '%':
422                         int code0=in.read();
423                         if ('u'==code0)
424                         {
425                             int code1=in.read();
426                             if (code1>=0)
427                             {
428                                 int code2=in.read();
429                                 if (code2>=0)
430                                 {
431                                     int code3=in.read();
432                                     if (code3>=0)
433                                         buffer.append(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3)));
434                                 }
435                             }
436                         }
437                         else if (code0>=0)
438                         {
439                             int code1=in.read();
440                             if (code1>=0)
441                                 buffer.append((char)((convertHexDigit(code0)<<4)+convertHexDigit(code1)));
442                         }
443                         break;
444                      
445                     default:
446                         buffer.append((char)b);
447                     break;
448                 }
449                 if (maxLength>=0 && (++totalLength > maxLength))
450                     throw new IllegalStateException("Form too large");
451             }
452             
453             if (key != null)
454             {
455                 value = buffer.length()==0?"":buffer.toString();
456                 buffer.setLength(0);
457                 map.add(key,value);
458             }
459             else if (buffer.length()>0)
460             {
461                 map.add(buffer.toString(), "");
462             }
463         }
464     }
465     
466     /* -------------------------------------------------------------- */
467     /** Decoded parameters to Map.
468      * @param in InputSteam to read
469      * @param map MultiMap to add parameters to
470      * @param maxLength maximum number of keys to read or -1 for no limit
471      */
472     public static void decodeUtf8To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
473     throws IOException
474     {
475         synchronized(map)
476         {
477             Utf8StringBuilder buffer = new Utf8StringBuilder();
478             String key = null;
479             String value = null;
480             
481             int b;
482             
483             int totalLength=0;
484             while ((b=in.read())>=0)
485             {
486                 try
487                 {
488                     switch ((char) b)
489                     {
490                         case '&':
491                             value = buffer.length()==0?"":buffer.toString();
492                             buffer.reset();
493                             if (key != null)
494                             {
495                                 map.add(key,value);
496                             }
497                             else if (value!=null&&value.length()>0)
498                             {
499                                 map.add(value,"");
500                             }
501                             key = null;
502                             value=null;
503                             if (maxKeys>0 && map.size()>maxKeys)
504                                 throw new IllegalStateException("Form too many keys");
505                             break;
506 
507                         case '=':
508                             if (key!=null)
509                             {
510                                 buffer.append((byte)b);
511                                 break;
512                             }
513                             key = buffer.toString();
514                             buffer.reset();
515                             break;
516 
517                         case '+':
518                             buffer.append((byte)' ');
519                             break;
520 
521                         case '%':
522                             int code0=in.read();
523                             if ('u'==code0)
524                             {
525                                 int code1=in.read();
526                                 if (code1>=0)
527                                 {
528                                     int code2=in.read();
529                                     if (code2>=0)
530                                     {
531                                         int code3=in.read();
532                                         if (code3>=0)
533                                             buffer.getStringBuilder().append(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3)));
534                                     }
535                                 }
536                             }
537                             else if (code0>=0)
538                             {
539                                 int code1=in.read();
540                                 if (code1>=0)
541                                     buffer.append((byte)((convertHexDigit(code0)<<4)+convertHexDigit(code1)));
542                             }
543                             break;
544                           
545                         default:
546                             buffer.append((byte)b);
547                             break;
548                     }
549                 }
550                 catch(NotUtf8Exception e)
551                 {
552                     LOG.warn(e.toString());
553                     LOG.debug(e);
554                 }
555                 if (maxLength>=0 && (++totalLength > maxLength))
556                     throw new IllegalStateException("Form too large");
557             }
558             
559             if (key != null)
560             {
561                 value = buffer.length()==0?"":buffer.toString();
562                 buffer.reset();
563                 map.add(key,value);
564             }
565             else if (buffer.length()>0)
566             {
567                 map.add(buffer.toString(), "");
568             }
569         }
570     }
571     
572     /* -------------------------------------------------------------- */
573     public static void decodeUtf16To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys) throws IOException
574     {
575         InputStreamReader input = new InputStreamReader(in,StringUtil.__UTF16);
576         StringWriter buf = new StringWriter(8192);
577         IO.copy(input,buf,maxLength);
578         
579         decodeTo(buf.getBuffer().toString(),map,StringUtil.__UTF16,maxKeys);
580     }
581 
582     /* -------------------------------------------------------------- */
583     /** Decoded parameters to Map.
584      * @param in the stream containing the encoded parameters
585      */
586     public static void decodeTo(InputStream in, MultiMap<String> map, String charset, int maxLength, int maxKeys)
587     throws IOException
588     {
589         if (charset==null)
590         {
591             if (ENCODING==StringUtil.__UTF8_CHARSET)
592                 decodeUtf8To(in,map,maxLength,maxKeys);
593             else
594                 decodeTo(in,map,ENCODING,maxLength,maxKeys);
595         }
596         else if (StringUtil.__UTF8.equalsIgnoreCase(charset))
597             decodeUtf8To(in,map,maxLength,maxKeys);
598         else if (StringUtil.__ISO_8859_1.equalsIgnoreCase(charset))
599             decode88591To(in,map,maxLength,maxKeys);
600         else if (StringUtil.__UTF16.equalsIgnoreCase(charset))
601             decodeUtf16To(in,map,maxLength,maxKeys);
602         else
603             decodeTo(in,map,Charset.forName(charset),maxLength,maxKeys);
604     }
605     
606     /* -------------------------------------------------------------- */
607     /** Decoded parameters to Map.
608      * @param in the stream containing the encoded parameters
609      */
610     public static void decodeTo(InputStream in, MultiMap<String> map, Charset charset, int maxLength, int maxKeys)
611     throws IOException
612     {
613         //no charset present, use the configured default
614         if (charset==null) 
615            charset=ENCODING;
616             
617         if (StringUtil.__UTF8_CHARSET.equals(charset))
618         {
619             decodeUtf8To(in,map,maxLength,maxKeys);
620             return;
621         }
622         
623         if (StringUtil.__ISO_8859_1_CHARSET.equals(charset))
624         {
625             decode88591To(in,map,maxLength,maxKeys);
626             return;
627         }
628 
629         if (StringUtil.__UTF16_CHARSET.equals(charset)) // Should be all 2 byte encodings
630         {
631             decodeUtf16To(in,map,maxLength,maxKeys);
632             return;
633         }
634 
635         synchronized(map)
636         {
637             String key = null;
638             String value = null;
639             
640             int c;
641             
642             int totalLength = 0;
643             ByteArrayOutputStream2 output = new ByteArrayOutputStream2();
644             
645             int size=0;
646             
647             while ((c=in.read())>0)
648             {
649                 switch ((char) c)
650                 {
651                     case '&':
652                         size=output.size();
653                         value = size==0?"":output.toString(charset);
654                         output.setCount(0);
655                         if (key != null)
656                         {
657                             map.add(key,value);
658                         }
659                         else if (value!=null&&value.length()>0)
660                         {
661                             map.add(value,"");
662                         }
663                         key = null;
664                         value=null;
665                         if (maxKeys>0 && map.size()>maxKeys)
666                             throw new IllegalStateException("Form too many keys");
667                         break;
668                     case '=':
669                         if (key!=null)
670                         {
671                             output.write(c);
672                             break;
673                         }
674                         size=output.size();
675                         key = size==0?"":output.toString(charset);
676                         output.setCount(0);
677                         break;
678                     case '+':
679                         output.write(' ');
680                         break;
681                     case '%':
682                         int code0=in.read();
683                         if ('u'==code0)
684                         {
685                             int code1=in.read();
686                             if (code1>=0)
687                             {
688                                 int code2=in.read();
689                                 if (code2>=0)
690                                 {
691                                     int code3=in.read();
692                                     if (code3>=0)
693                                         output.write(new String(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3))).getBytes(charset));
694                                 }
695                             }
696                             
697                         }
698                         else if (code0>=0)
699                         {
700                             int code1=in.read();
701                             if (code1>=0)
702                                 output.write((convertHexDigit(code0)<<4)+convertHexDigit(code1));
703                         }
704                         break;
705                     default:
706                         output.write(c);
707                     break;
708                 }
709                 
710                 totalLength++;
711                 if (maxLength>=0 && totalLength > maxLength)
712                     throw new IllegalStateException("Form too large");
713             }
714 
715             size=output.size();
716             if (key != null)
717             {
718                 value = size==0?"":output.toString(charset);
719                 output.setCount(0);
720                 map.add(key,value);
721             }
722             else if (size>0)
723                 map.add(output.toString(charset),"");
724         }
725     }
726     
727     /* -------------------------------------------------------------- */
728     /** Decode String with % encoding.
729      * This method makes the assumption that the majority of calls
730      * will need no decoding.
731      */
732     public static String decodeString(String encoded,int offset,int length,Charset charset)
733     {
734         if (charset==null || StringUtil.__UTF8_CHARSET.equals(charset))
735         {
736             Utf8StringBuffer buffer=null;
737 
738             for (int i=0;i<length;i++)
739             {
740                 char c = encoded.charAt(offset+i);
741                 if (c<0||c>0xff)
742                 {
743                     if (buffer==null)
744                     {
745                         buffer=new Utf8StringBuffer(length);
746                         buffer.getStringBuffer().append(encoded,offset,offset+i+1);
747                     }
748                     else
749                         buffer.getStringBuffer().append(c);
750                 }
751                 else if (c=='+')
752                 {
753                     if (buffer==null)
754                     {
755                         buffer=new Utf8StringBuffer(length);
756                         buffer.getStringBuffer().append(encoded,offset,offset+i);
757                     }
758                     
759                     buffer.getStringBuffer().append(' ');
760                 }
761                 else if (c=='%')
762                 {
763                     if (buffer==null)
764                     {
765                         buffer=new Utf8StringBuffer(length);
766                         buffer.getStringBuffer().append(encoded,offset,offset+i);
767                     }
768                     
769                     if ((i+2)<length)
770                     {
771                         try
772                         {
773                             if ('u'==encoded.charAt(offset+i+1))
774                             {
775                                 if((i+5)<length)
776                                 {
777                                     int o=offset+i+2;
778                                     i+=5;
779                                     String unicode = new String(Character.toChars(TypeUtil.parseInt(encoded,o,4,16)));
780                                     buffer.getStringBuffer().append(unicode); 
781                                 }
782                                 else
783                                 {
784                                     i=length;
785                                     buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT); 
786                                 }
787                             }
788                             else
789                             {
790                                 int o=offset+i+1;
791                                 i+=2;
792                                 byte b=(byte)TypeUtil.parseInt(encoded,o,2,16);
793                                 buffer.append(b);
794                             }
795                         }
796                         catch(NotUtf8Exception e)
797                         {
798                             LOG.warn(e.toString());
799                             LOG.debug(e);
800                         }
801                         catch(NumberFormatException nfe)
802                         {
803                             LOG.debug(nfe);
804                             buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT);  
805                         }
806                     }
807                     else
808                     {
809                         buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT); 
810                         i=length;
811                     }
812                 }
813                 else if (buffer!=null)
814                     buffer.getStringBuffer().append(c);
815             }
816 
817             if (buffer==null)
818             {
819                 if (offset==0 && encoded.length()==length)
820                     return encoded;
821                 return encoded.substring(offset,offset+length);
822             }
823 
824             return buffer.toReplacedString();
825         }
826         else
827         {
828             StringBuffer buffer=null;
829 
830             for (int i=0;i<length;i++)
831             {
832                 char c = encoded.charAt(offset+i);
833                 if (c<0||c>0xff)
834                 {
835                     if (buffer==null)
836                     {
837                         buffer=new StringBuffer(length);
838                         buffer.append(encoded,offset,offset+i+1);
839                     }
840                     else
841                         buffer.append(c);
842                 }
843                 else if (c=='+')
844                 {
845                     if (buffer==null)
846                     {
847                         buffer=new StringBuffer(length);
848                         buffer.append(encoded,offset,offset+i);
849                     }
850 
851                     buffer.append(' ');
852                 }
853                 else if (c=='%')
854                 {
855                     if (buffer==null)
856                     {
857                         buffer=new StringBuffer(length);
858                         buffer.append(encoded,offset,offset+i);
859                     }
860 
861                     byte[] ba=new byte[length];
862                     int n=0;
863                     while(c>=0 && c<=0xff)
864                     {
865                         if (c=='%')
866                         {   
867                             if(i+2<length)
868                             {
869                                 try
870                                 {
871                                     if ('u'==encoded.charAt(offset+i+1))
872                                     {
873                                             if (i+6<length)
874                                             {
875                                         int o=offset+i+2;
876                                         i+=6;
877                                         String unicode = new String(Character.toChars(TypeUtil.parseInt(encoded,o,4,16)));
878                                         byte[] reencoded = unicode.getBytes(charset);
879                                         System.arraycopy(reencoded,0,ba,n,reencoded.length);
880                                         n+=reencoded.length;
881                                     }
882                                     else
883                                     {
884                                                 ba[n++] = (byte)'?';
885                                                 i=length;
886                                             }
887                                         }
888                                         else
889                                         {
890                                         int o=offset+i+1;
891                                         i+=3;
892                                         ba[n]=(byte)TypeUtil.parseInt(encoded,o,2,16);
893                                         n++;
894                                     }
895                                 }
896                                 catch(NumberFormatException nfe)
897                                 {   
898                                     LOG.ignore(nfe);
899                                     ba[n++] = (byte)'?';
900                                 }
901                             }
902                             else
903                             {
904                                     ba[n++] = (byte)'?';
905                                     i=length;
906                             }
907                         }
908                         else if (c=='+')
909                         {
910                             ba[n++]=(byte)' ';
911                             i++;
912                         }
913                         else
914                         {
915                             ba[n++]=(byte)c;
916                             i++;
917                         }
918 
919                         if (i>=length)
920                             break;
921                         c = encoded.charAt(offset+i);
922                     }
923 
924                     i--;
925                     buffer.append(new String(ba,0,n,charset));
926 
927                 }
928                 else if (buffer!=null)
929                     buffer.append(c);
930             }
931 
932             if (buffer==null)
933             {
934                 if (offset==0 && encoded.length()==length)
935                     return encoded;
936                 return encoded.substring(offset,offset+length);
937             }
938 
939             return buffer.toString();
940         }
941 
942     }
943     
944     /* ------------------------------------------------------------ */
945     /** Perform URL encoding.
946      * @param string 
947      * @return encoded string.
948      */
949     public static String encodeString(String string)
950     {
951         return encodeString(string,ENCODING);
952     }
953     
954     /* ------------------------------------------------------------ */
955     /** Perform URL encoding.
956      * @param string 
957      * @return encoded string.
958      */
959     public static String encodeString(String string,Charset charset)
960     {
961         if (charset==null)
962             charset=ENCODING;
963         byte[] bytes=null;
964         bytes=string.getBytes(charset);
965         
966         int len=bytes.length;
967         byte[] encoded= new byte[bytes.length*3];
968         int n=0;
969         boolean noEncode=true;
970         
971         for (int i=0;i<len;i++)
972         {
973             byte b = bytes[i];
974             
975             if (b==' ')
976             {
977                 noEncode=false;
978                 encoded[n++]=(byte)'+';
979             }
980             else if (b>='a' && b<='z' ||
981                      b>='A' && b<='Z' ||
982                      b>='0' && b<='9')
983             {
984                 encoded[n++]=b;
985             }
986             else
987             {
988                 noEncode=false;
989                 encoded[n++]=(byte)'%';
990                 byte nibble= (byte) ((b&0xf0)>>4);
991                 if (nibble>=10)
992                     encoded[n++]=(byte)('A'+nibble-10);
993                 else
994                     encoded[n++]=(byte)('0'+nibble);
995                 nibble= (byte) (b&0xf);
996                 if (nibble>=10)
997                     encoded[n++]=(byte)('A'+nibble-10);
998                 else
999                     encoded[n++]=(byte)('0'+nibble);
1000             }
1001         }
1002 
1003         if (noEncode)
1004             return string;
1005         
1006         return new String(encoded,0,n,charset);
1007     }
1008 
1009 
1010     /* ------------------------------------------------------------ */
1011     /** 
1012      */
1013     @Override
1014     public Object clone()
1015     {
1016         return new UrlEncoded(this);
1017     }
1018 }