View Javadoc

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