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.io.UnsupportedEncodingException;
28  import java.util.Iterator;
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  public class UrlEncoded extends MultiMap implements Cloneable
55  {
56      private static final Logger LOG = Log.getLogger(UrlEncoded.class);
57  
58      public static final String ENCODING = System.getProperty("org.eclipse.jetty.util.UrlEncoding.charset",StringUtil.__UTF8);
59  
60      /* ----------------------------------------------------------------- */
61      public UrlEncoded(UrlEncoded url)
62      {
63          super(url);
64      }
65      
66      /* ----------------------------------------------------------------- */
67      public UrlEncoded()
68      {
69          super(6);
70      }
71      
72      /* ----------------------------------------------------------------- */
73      public UrlEncoded(String s)
74      {
75          super(6);
76          decode(s,ENCODING);
77      }
78      
79      /* ----------------------------------------------------------------- */
80      public UrlEncoded(String s, String charset)
81      {
82          super(6);
83          decode(s,charset);
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,String 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(String 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(String 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 map, String charset, boolean equalsForNullValue)
130     {
131         if (charset==null)
132             charset=ENCODING;
133 
134         StringBuilder result = new StringBuilder(128);
135 
136         Iterator iter = map.entrySet().iterator();
137         while(iter.hasNext())
138         {
139             Map.Entry entry = (Map.Entry)iter.next();
140 
141             String key = entry.getKey().toString();
142             Object list = entry.getValue();
143             int s=LazyList.size(list);
144 
145             if (s==0)
146             {
147                 result.append(encodeString(key,charset));
148                 if(equalsForNullValue)
149                     result.append('=');
150             }
151             else
152             {
153                 for (int i=0;i<s;i++)
154                 {
155                     if (i>0)
156                         result.append('&');
157                     Object val=LazyList.get(list,i);
158                     result.append(encodeString(key,charset));
159 
160                     if (val!=null)
161                     {
162                         String str=val.toString();
163                         if (str.length()>0)
164                         {
165                             result.append('=');
166                             result.append(encodeString(str,charset));
167                         }
168                         else if (equalsForNullValue)
169                             result.append('=');
170                     }
171                     else if (equalsForNullValue)
172                         result.append('=');
173                 }
174             }
175             if (iter.hasNext())
176                 result.append('&');
177         }
178         return result.toString();
179     }
180 
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 map, String charset)
188     {
189         decodeTo(content,map,charset,-1);
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 map, String 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      */
273     public static void decodeUtf8To(byte[] raw,int offset, int length, MultiMap map)
274     {
275         decodeUtf8To(raw,offset,length,map,new Utf8StringBuilder());
276     }
277 
278     /* -------------------------------------------------------------- */
279     /** Decoded parameters to Map.
280      * @param raw the byte[] containing the encoded parameters
281      * @param offset the offset within raw to decode from
282      * @param length the length of the section to decode
283      * @param map the {@link MultiMap} to populate
284      * @param buffer the buffer to decode into
285      */
286     public static void decodeUtf8To(byte[] raw,int offset, int length, MultiMap map,Utf8StringBuilder buffer)
287     {
288         synchronized(map)
289         {
290             String key = null;
291             String value = null;
292 
293             // TODO cache of parameter names ???
294             int end=offset+length;
295             for (int i=offset;i<end;i++)
296             {
297                 byte b=raw[i];
298                 try
299                 {
300                     switch ((char)(0xff&b))
301                     {
302                         case '&':
303                             value = buffer.length()==0?"":buffer.toString();
304                             buffer.reset();
305                             if (key != null)
306                             {
307                                 map.add(key,value);
308                             }
309                             else if (value!=null&&value.length()>0)
310                             {
311                                 map.add(value,"");
312                             }
313                             key = null;
314                             value=null;
315                             break;
316 
317                         case '=':
318                             if (key!=null)
319                             {
320                                 buffer.append(b);
321                                 break;
322                             }
323                             key = buffer.toString();
324                             buffer.reset();
325                             break;
326 
327                         case '+':
328                             buffer.append((byte)' ');
329                             break;
330 
331                         case '%':
332                             if (i+2<end)
333                             {
334                                 if ('u'==raw[i+1])
335                                 {
336                                     i++;
337                                     if (i+4<end)
338                                         buffer.getStringBuilder().append(Character.toChars((convertHexDigit(raw[++i])<<12) +(convertHexDigit(raw[++i])<<8) + (convertHexDigit(raw[++i])<<4) +convertHexDigit(raw[++i])));
339                                     else
340                                     {
341                                         buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
342                                         i=end;
343                                     }
344                                 }
345                                 else
346                                     buffer.append((byte)((convertHexDigit(raw[++i])<<4) + convertHexDigit(raw[++i])));
347                             }
348                             else
349                             {
350                                 buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
351                                 i=end;
352                             }
353                             break;
354                             
355                         default:
356                             buffer.append(b);
357                             break;
358                     }
359                 }
360                 catch(NotUtf8Exception e)
361                 {
362                     LOG.warn(e.toString());
363                     LOG.debug(e);
364                 }
365             }
366             
367             if (key != null)
368             {
369                 value = buffer.length()==0?"":buffer.toReplacedString();
370                 buffer.reset();
371                 map.add(key,value);
372             }
373             else if (buffer.length()>0)
374             {
375                 map.add(buffer.toReplacedString(),"");
376             }
377         }
378     }
379 
380     /* -------------------------------------------------------------- */
381     /** Decoded parameters to Map.
382      * @param in InputSteam to read
383      * @param map MultiMap to add parameters to
384      * @param maxLength maximum number of keys to read or -1 for no limit
385      */
386     public static void decode88591To(InputStream in, MultiMap map, int maxLength, int maxKeys)
387     throws IOException
388     {
389         synchronized(map)
390         {
391             StringBuffer buffer = new StringBuffer();
392             String key = null;
393             String value = null;
394             
395             int b;
396 
397             // TODO cache of parameter names ???
398             int totalLength=0;
399             while ((b=in.read())>=0)
400             {
401                 switch ((char) b)
402                 {
403                     case '&':
404                         value = buffer.length()==0?"":buffer.toString();
405                         buffer.setLength(0);
406                         if (key != null)
407                         {
408                             map.add(key,value);
409                         }
410                         else if (value!=null&&value.length()>0)
411                         {
412                             map.add(value,"");
413                         }
414                         key = null;
415                         value=null;
416                         if (maxKeys>0 && map.size()>maxKeys)
417                             throw new IllegalStateException("Form too many keys");
418                         break;
419                         
420                     case '=':
421                         if (key!=null)
422                         {
423                             buffer.append((char)b);
424                             break;
425                         }
426                         key = buffer.toString();
427                         buffer.setLength(0);
428                         break;
429                         
430                     case '+':
431                         buffer.append(' ');
432                         break;
433                         
434                     case '%':
435                         int code0=in.read();
436                         if ('u'==code0)
437                         {
438                             int code1=in.read();
439                             if (code1>=0)
440                             {
441                                 int code2=in.read();
442                                 if (code2>=0)
443                                 {
444                                     int code3=in.read();
445                                     if (code3>=0)
446                                         buffer.append(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3)));
447                                 }
448                             }
449                         }
450                         else if (code0>=0)
451                         {
452                             int code1=in.read();
453                             if (code1>=0)
454                                 buffer.append((char)((convertHexDigit(code0)<<4)+convertHexDigit(code1)));
455                         }
456                         break;
457                      
458                     default:
459                         buffer.append((char)b);
460                     break;
461                 }
462                 if (maxLength>=0 && (++totalLength > maxLength))
463                     throw new IllegalStateException("Form too large");
464             }
465             
466             if (key != null)
467             {
468                 value = buffer.length()==0?"":buffer.toString();
469                 buffer.setLength(0);
470                 map.add(key,value);
471             }
472             else if (buffer.length()>0)
473             {
474                 map.add(buffer.toString(), "");
475             }
476         }
477     }
478     
479     /* -------------------------------------------------------------- */
480     /** Decoded parameters to Map.
481      * @param in InputSteam to read
482      * @param map MultiMap to add parameters to
483      * @param maxLength maximum number of keys to read or -1 for no limit
484      */
485     public static void decodeUtf8To(InputStream in, MultiMap map, int maxLength, int maxKeys)
486     throws IOException
487     {
488         synchronized(map)
489         {
490             Utf8StringBuilder buffer = new Utf8StringBuilder();
491             String key = null;
492             String value = null;
493             
494             int b;
495             
496             // TODO cache of parameter names ???
497             int totalLength=0;
498             while ((b=in.read())>=0)
499             {
500                 try
501                 {
502                     switch ((char) b)
503                     {
504                         case '&':
505                             value = buffer.length()==0?"":buffer.toString();
506                             buffer.reset();
507                             if (key != null)
508                             {
509                                 map.add(key,value);
510                             }
511                             else if (value!=null&&value.length()>0)
512                             {
513                                 map.add(value,"");
514                             }
515                             key = null;
516                             value=null;
517                             if (maxKeys>0 && map.size()>maxKeys)
518                                 throw new IllegalStateException("Form too many keys");
519                             break;
520 
521                         case '=':
522                             if (key!=null)
523                             {
524                                 buffer.append((byte)b);
525                                 break;
526                             }
527                             key = buffer.toString();
528                             buffer.reset();
529                             break;
530 
531                         case '+':
532                             buffer.append((byte)' ');
533                             break;
534 
535                         case '%':
536                             int code0=in.read();
537                             if ('u'==code0)
538                             {
539                                 int code1=in.read();
540                                 if (code1>=0)
541                                 {
542                                     int code2=in.read();
543                                     if (code2>=0)
544                                     {
545                                         int code3=in.read();
546                                         if (code3>=0)
547                                             buffer.getStringBuilder().append(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3)));
548                                     }
549                                 }
550                             }
551                             else if (code0>=0)
552                             {
553                                 int code1=in.read();
554                                 if (code1>=0)
555                                     buffer.append((byte)((convertHexDigit(code0)<<4)+convertHexDigit(code1)));
556                             }
557                             break;
558                           
559                         default:
560                             buffer.append((byte)b);
561                             break;
562                     }
563                 }
564                 catch(NotUtf8Exception e)
565                 {
566                     LOG.warn(e.toString());
567                     LOG.debug(e);
568                 }
569                 if (maxLength>=0 && (++totalLength > maxLength))
570                     throw new IllegalStateException("Form too large");
571             }
572             
573             if (key != null)
574             {
575                 value = buffer.length()==0?"":buffer.toString();
576                 buffer.reset();
577                 map.add(key,value);
578             }
579             else if (buffer.length()>0)
580             {
581                 map.add(buffer.toString(), "");
582             }
583         }
584     }
585     
586     /* -------------------------------------------------------------- */
587     public static void decodeUtf16To(InputStream in, MultiMap map, int maxLength, int maxKeys) throws IOException
588     {
589         InputStreamReader input = new InputStreamReader(in,StringUtil.__UTF16);
590         StringWriter buf = new StringWriter(8192);
591         IO.copy(input,buf,maxLength);
592         
593         decodeTo(buf.getBuffer().toString(),map,StringUtil.__UTF16,maxKeys);
594     }
595     
596     /* -------------------------------------------------------------- */
597     /** Decoded parameters to Map.
598      * @param in the stream containing the encoded parameters
599      */
600     public static void decodeTo(InputStream in, MultiMap map, String charset, int maxLength, int maxKeys)
601     throws IOException
602     {
603         //no charset present, use the configured default
604         if (charset==null) 
605         {
606            charset=ENCODING;
607         }
608             
609         if (StringUtil.__UTF8.equalsIgnoreCase(charset))
610         {
611             decodeUtf8To(in,map,maxLength,maxKeys);
612             return;
613         }
614         
615         if (StringUtil.__ISO_8859_1.equals(charset))
616         {
617             decode88591To(in,map,maxLength,maxKeys);
618             return;
619         }
620 
621         if (StringUtil.__UTF16.equalsIgnoreCase(charset)) // Should be all 2 byte encodings
622         {
623             decodeUtf16To(in,map,maxLength,maxKeys);
624             return;
625         }
626         
627 
628         synchronized(map)
629         {
630             String key = null;
631             String value = null;
632             
633             int c;
634             
635             int totalLength = 0;
636             ByteArrayOutputStream2 output = new ByteArrayOutputStream2();
637             
638             int size=0;
639             
640             while ((c=in.read())>0)
641             {
642                 switch ((char) c)
643                 {
644                     case '&':
645                         size=output.size();
646                         value = size==0?"":output.toString(charset);
647                         output.setCount(0);
648                         if (key != null)
649                         {
650                             map.add(key,value);
651                         }
652                         else if (value!=null&&value.length()>0)
653                         {
654                             map.add(value,"");
655                         }
656                         key = null;
657                         value=null;
658                         if (maxKeys>0 && map.size()>maxKeys)
659                             throw new IllegalStateException("Form too many keys");
660                         break;
661                     case '=':
662                         if (key!=null)
663                         {
664                             output.write(c);
665                             break;
666                         }
667                         size=output.size();
668                         key = size==0?"":output.toString(charset);
669                         output.setCount(0);
670                         break;
671                     case '+':
672                         output.write(' ');
673                         break;
674                     case '%':
675                         int code0=in.read();
676                         if ('u'==code0)
677                         {
678                             int code1=in.read();
679                             if (code1>=0)
680                             {
681                                 int code2=in.read();
682                                 if (code2>=0)
683                                 {
684                                     int code3=in.read();
685                                     if (code3>=0)
686                                         output.write(new String(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3))).getBytes(charset));
687                                 }
688                             }
689                             
690                         }
691                         else if (code0>=0)
692                         {
693                             int code1=in.read();
694                             if (code1>=0)
695                                 output.write((convertHexDigit(code0)<<4)+convertHexDigit(code1));
696                         }
697                         break;
698                     default:
699                         output.write(c);
700                     break;
701                 }
702                 
703                 totalLength++;
704                 if (maxLength>=0 && totalLength > maxLength)
705                     throw new IllegalStateException("Form too large");
706             }
707 
708             size=output.size();
709             if (key != null)
710             {
711                 value = size==0?"":output.toString(charset);
712                 output.setCount(0);
713                 map.add(key,value);
714             }
715             else if (size>0)
716                 map.add(output.toString(charset),"");
717         }
718     }
719     
720     /* -------------------------------------------------------------- */
721     /** Decode String with % encoding.
722      * This method makes the assumption that the majority of calls
723      * will need no decoding.
724      */
725     public static String decodeString(String encoded,int offset,int length,String charset)
726     {
727         if (charset==null || StringUtil.isUTF8(charset))
728         {
729             Utf8StringBuffer buffer=null;
730 
731             for (int i=0;i<length;i++)
732             {
733                 char c = encoded.charAt(offset+i);
734                 if (c<0||c>0xff)
735                 {
736                     if (buffer==null)
737                     {
738                         buffer=new Utf8StringBuffer(length);
739                         buffer.getStringBuffer().append(encoded,offset,offset+i+1);
740                     }
741                     else
742                         buffer.getStringBuffer().append(c);
743                 }
744                 else if (c=='+')
745                 {
746                     if (buffer==null)
747                     {
748                         buffer=new Utf8StringBuffer(length);
749                         buffer.getStringBuffer().append(encoded,offset,offset+i);
750                     }
751                     
752                     buffer.getStringBuffer().append(' ');
753                 }
754                 else if (c=='%')
755                 {
756                     if (buffer==null)
757                     {
758                         buffer=new Utf8StringBuffer(length);
759                         buffer.getStringBuffer().append(encoded,offset,offset+i);
760                     }
761                     
762                     if ((i+2)<length)
763                     {
764                         try
765                         {
766                             if ('u'==encoded.charAt(offset+i+1))
767                             {
768                                 if((i+5)<length)
769                                 {
770                                     int o=offset+i+2;
771                                     i+=5;
772                                     String unicode = new String(Character.toChars(TypeUtil.parseInt(encoded,o,4,16)));
773                                     buffer.getStringBuffer().append(unicode); 
774                                 }
775                                 else
776                                 {
777                                     i=length;
778                                     buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT); 
779                                 }
780                             }
781                             else
782                             {
783                                 int o=offset+i+1;
784                                 i+=2;
785                                 byte b=(byte)TypeUtil.parseInt(encoded,o,2,16);
786                                 buffer.append(b);
787                             }
788                         }
789                         catch(NotUtf8Exception e)
790                         {
791                             LOG.warn(e.toString());
792                             LOG.debug(e);
793                         }
794                         catch(NumberFormatException nfe)
795                         {
796                             LOG.debug(nfe);
797                             buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT);  
798                         }
799                     }
800                     else
801                     {
802                         buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT); 
803                         i=length;
804                     }
805                 }
806                 else if (buffer!=null)
807                     buffer.getStringBuffer().append(c);
808             }
809 
810             if (buffer==null)
811             {
812                 if (offset==0 && encoded.length()==length)
813                     return encoded;
814                 return encoded.substring(offset,offset+length);
815             }
816 
817             return buffer.toReplacedString();
818         }
819         else
820         {
821             StringBuffer buffer=null;
822 
823             try
824             {
825                 for (int i=0;i<length;i++)
826                 {
827                     char c = encoded.charAt(offset+i);
828                     if (c<0||c>0xff)
829                     {
830                         if (buffer==null)
831                         {
832                             buffer=new StringBuffer(length);
833                             buffer.append(encoded,offset,offset+i+1);
834                         }
835                         else
836                             buffer.append(c);
837                     }
838                     else if (c=='+')
839                     {
840                         if (buffer==null)
841                         {
842                             buffer=new StringBuffer(length);
843                             buffer.append(encoded,offset,offset+i);
844                         }
845                         
846                         buffer.append(' ');
847                     }
848                     else if (c=='%')
849                     {
850                         if (buffer==null)
851                         {
852                             buffer=new StringBuffer(length);
853                             buffer.append(encoded,offset,offset+i);
854                         }
855 
856                         byte[] ba=new byte[length];
857                         int n=0;
858                         while(c>=0 && c<=0xff)
859                         {
860                             if (c=='%')
861                             {   
862                                 if(i+2<length)
863                                 {
864                                     try
865                                     {
866                                         if ('u'==encoded.charAt(offset+i+1))
867                                         {
868                                             if (i+6<length)
869                                             {
870                                                 int o=offset+i+2;
871                                                 i+=6;
872                                                 String unicode = new String(Character.toChars(TypeUtil.parseInt(encoded,o,4,16)));
873                                                 byte[] reencoded = unicode.getBytes(charset);
874                                                 System.arraycopy(reencoded,0,ba,n,reencoded.length);
875                                                 n+=reencoded.length;
876                                             }
877                                             else
878                                             {
879                                                 ba[n++] = (byte)'?';
880                                                 i=length;
881                                             }
882                                         }
883                                         else
884                                         {
885                                             int o=offset+i+1;
886                                             i+=3;
887                                             ba[n]=(byte)TypeUtil.parseInt(encoded,o,2,16);
888                                             n++;
889                                         }
890                                     }
891                                     catch(NumberFormatException nfe)
892                                     {   
893                                         LOG.ignore(nfe);
894                                         ba[n++] = (byte)'?';
895                                     }
896                                 }
897                                 else
898                                 {
899                                     ba[n++] = (byte)'?';
900                                     i=length;
901                                 }
902                             }
903                             else if (c=='+')
904                             {
905                                 ba[n++]=(byte)' ';
906                                 i++;
907                             }
908                             else
909                             {
910                                 ba[n++]=(byte)c;
911                                 i++;
912                             }
913                             
914                             if (i>=length)
915                                 break;
916                             c = encoded.charAt(offset+i);
917                         }
918 
919                         i--;
920                         buffer.append(new String(ba,0,n,charset));
921 
922                     }
923                     else if (buffer!=null)
924                         buffer.append(c);
925                 }
926 
927                 if (buffer==null)
928                 {
929                     if (offset==0 && encoded.length()==length)
930                         return encoded;
931                     return encoded.substring(offset,offset+length);
932                 }
933 
934                 return buffer.toString();
935             }
936             catch (UnsupportedEncodingException e)
937             {
938                 throw new RuntimeException(e);
939             }
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,String charset)
960     {
961         if (charset==null)
962             charset=ENCODING;
963         byte[] bytes=null;
964         try
965         {
966             bytes=string.getBytes(charset);
967         }
968         catch(UnsupportedEncodingException e)
969         {
970             // LOG.warn(LogSupport.EXCEPTION,e);
971             bytes=string.getBytes();
972         }
973         
974         int len=bytes.length;
975         byte[] encoded= new byte[bytes.length*3];
976         int n=0;
977         boolean noEncode=true;
978         
979         for (int i=0;i<len;i++)
980         {
981             byte b = bytes[i];
982             
983             if (b==' ')
984             {
985                 noEncode=false;
986                 encoded[n++]=(byte)'+';
987             }
988             else if (b>='a' && b<='z' ||
989                      b>='A' && b<='Z' ||
990                      b>='0' && b<='9')
991             {
992                 encoded[n++]=b;
993             }
994             else
995             {
996                 noEncode=false;
997                 encoded[n++]=(byte)'%';
998                 byte nibble= (byte) ((b&0xf0)>>4);
999                 if (nibble>=10)
1000                     encoded[n++]=(byte)('A'+nibble-10);
1001                 else
1002                     encoded[n++]=(byte)('0'+nibble);
1003                 nibble= (byte) (b&0xf);
1004                 if (nibble>=10)
1005                     encoded[n++]=(byte)('A'+nibble-10);
1006                 else
1007                     encoded[n++]=(byte)('0'+nibble);
1008             }
1009         }
1010 
1011         if (noEncode)
1012             return string;
1013         
1014         try
1015         {    
1016             return new String(encoded,0,n,charset);
1017         }
1018         catch(UnsupportedEncodingException e)
1019         {
1020             // LOG.warn(LogSupport.EXCEPTION,e);
1021             return new String(encoded,0,n);
1022         }
1023     }
1024 
1025 
1026     /* ------------------------------------------------------------ */
1027     /** 
1028      */
1029     @Override
1030     public Object clone()
1031     {
1032         return new UrlEncoded(this);
1033     }
1034 }