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 length of content to read or -1 for no limit
362      * @param maxLength maximum number of keys to read or -1 for no limit
363      */
364     public static void decode88591To(InputStream in, MultiMap map, int maxLength, int maxKeys)
365     throws IOException
366     {
367         synchronized(map)
368         {
369             StringBuffer buffer = new StringBuffer();
370             String key = null;
371             String value = null;
372             
373             int b;
374 
375             // TODO cache of parameter names ???
376             int totalLength=0;
377             while ((b=in.read())>=0)
378             {
379                 switch ((char) b)
380                 {
381                     case '&':
382                         value = buffer.length()==0?"":buffer.toString();
383                         buffer.setLength(0);
384                         if (key != null)
385                         {
386                             map.add(key,value);
387                         }
388                         else if (value!=null&&value.length()>0)
389                         {
390                             map.add(value,"");
391                         }
392                         key = null;
393                         value=null;
394                         if (maxKeys>0 && map.size()>maxKeys)
395                         {
396                             LOG.warn("maxFormKeys limit exceeded keys>{}",maxKeys);
397                             return;
398                         }
399                         break;
400                         
401                     case '=':
402                         if (key!=null)
403                         {
404                             buffer.append((char)b);
405                             break;
406                         }
407                         key = buffer.toString();
408                         buffer.setLength(0);
409                         break;
410                         
411                     case '+':
412                         buffer.append(' ');
413                         break;
414                         
415                     case '%':
416                         int dh=in.read();
417                         int dl=in.read();
418                         if (dh<0||dl<0)
419                             break;
420                         buffer.append((char)((TypeUtil.convertHexDigit((byte)dh)<<4) + TypeUtil.convertHexDigit((byte)dl)));
421                         break;
422                     default:
423                         buffer.append((char)b);
424                     break;
425                 }
426                 if (maxLength>=0 && (++totalLength > maxLength))
427                     throw new IllegalStateException("Form too large");
428             }
429             
430             if (key != null)
431             {
432                 value = buffer.length()==0?"":buffer.toString();
433                 buffer.setLength(0);
434                 map.add(key,value);
435             }
436             else if (buffer.length()>0)
437             {
438                 map.add(buffer.toString(), "");
439             }
440         }
441     }
442     
443     /* -------------------------------------------------------------- */
444     /** Decoded parameters to Map.
445      * @param in InputSteam to read
446      * @param map MultiMap to add parameters to
447      * @param maxLength maximum length of content to read or -1 for no limit
448      * @param maxLength maximum number of keys to read or -1 for no limit
449      */
450     public static void decodeUtf8To(InputStream in, MultiMap map, int maxLength, int maxKeys)
451     throws IOException
452     {
453         synchronized(map)
454         {
455             Utf8StringBuilder buffer = new Utf8StringBuilder();
456             String key = null;
457             String value = null;
458             
459             int b;
460             
461             // TODO cache of parameter names ???
462             int totalLength=0;
463             while ((b=in.read())>=0)
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                 if (maxLength>=0 && (++totalLength > maxLength))
513                     throw new IllegalStateException("Form too large");
514             }
515             
516             if (key != null)
517             {
518                 value = buffer.length()==0?"":buffer.toString();
519                 buffer.reset();
520                 map.add(key,value);
521             }
522             else if (buffer.length()>0)
523             {
524                 map.add(buffer.toString(), "");
525             }
526         }
527     }
528     
529     /* -------------------------------------------------------------- */
530     public static void decodeUtf16To(InputStream in, MultiMap map, int maxLength, int maxKeys) throws IOException
531     {
532         InputStreamReader input = new InputStreamReader(in,StringUtil.__UTF16);
533         StringWriter buf = new StringWriter(8192);
534         IO.copy(input,buf,maxLength);
535         
536         decodeTo(buf.getBuffer().toString(),map,ENCODING,maxKeys);
537     }
538     
539     /* -------------------------------------------------------------- */
540     /** Decoded parameters to Map.
541      * @param in the stream containing the encoded parameters
542      */
543     public static void decodeTo(InputStream in, MultiMap map, String charset, int maxLength, int maxKeys)
544     throws IOException
545     {
546         //no charset present, use the configured default
547         if (charset==null) 
548         {
549            charset=ENCODING;
550         }
551             
552         if (StringUtil.__UTF8.equalsIgnoreCase(charset))
553         {
554             decodeUtf8To(in,map,maxLength,maxKeys);
555             return;
556         }
557         
558         if (StringUtil.__ISO_8859_1.equals(charset))
559         {
560             decode88591To(in,map,maxLength,maxKeys);
561             return;
562         }
563 
564         if (StringUtil.__UTF16.equalsIgnoreCase(charset)) // Should be all 2 byte encodings
565         {
566             decodeUtf16To(in,map,maxLength,maxKeys);
567             return;
568         }
569         
570 
571         synchronized(map)
572         {
573             String key = null;
574             String value = null;
575             
576             int c;
577             int digit=0;
578             int digits=0;
579             
580             int totalLength = 0;
581             ByteArrayOutputStream2 output = new ByteArrayOutputStream2();
582             
583             int size=0;
584             
585             while ((c=in.read())>0)
586             {
587                 switch ((char) c)
588                 {
589                     case '&':
590                         size=output.size();
591                         value = size==0?"":output.toString(charset);
592                         output.setCount(0);
593                         if (key != null)
594                         {
595                             map.add(key,value);
596                         }
597                         else if (value!=null&&value.length()>0)
598                         {
599                             map.add(value,"");
600                         }
601                         key = null;
602                         value=null;
603                         break;
604                     case '=':
605                         if (key!=null)
606                         {
607                             output.write(c);
608                             break;
609                         }
610                         size=output.size();
611                         key = size==0?"":output.toString(charset);
612                         output.setCount(0);
613                         break;
614                     case '+':
615                         output.write(' ');
616                         break;
617                     case '%':
618                         digits=2;
619                         break;
620                     default:
621                         if (digits==2)
622                         {
623                             digit=TypeUtil.convertHexDigit((byte)c);
624                             digits=1;
625                         }
626                         else if (digits==1)
627                         {
628                             output.write((digit<<4) + TypeUtil.convertHexDigit((byte)c));
629                             digits=0;
630                         }
631                         else
632                             output.write(c);
633                     break;
634                 }
635                 
636                 totalLength++;
637                 if (maxLength>=0 && totalLength > maxLength)
638                     throw new IllegalStateException("Form too large");
639             }
640 
641             size=output.size();
642             if (key != null)
643             {
644                 value = size==0?"":output.toString(charset);
645                 output.setCount(0);
646                 map.add(key,value);
647             }
648             else if (size>0)
649                 map.add(output.toString(charset),"");
650         }
651     }
652     
653     /* -------------------------------------------------------------- */
654     /** Decode String with % encoding.
655      * This method makes the assumption that the majority of calls
656      * will need no decoding.
657      */
658     public static String decodeString(String encoded,int offset,int length,String charset)
659     {
660         if (charset==null || StringUtil.isUTF8(charset))
661         {
662             Utf8StringBuffer buffer=null;
663 
664             for (int i=0;i<length;i++)
665             {
666                 char c = encoded.charAt(offset+i);
667                 if (c<0||c>0xff)
668                 {
669                     if (buffer==null)
670                     {
671                         buffer=new Utf8StringBuffer(length);
672                         buffer.getStringBuffer().append(encoded,offset,offset+i+1);
673                     }
674                     else
675                         buffer.getStringBuffer().append(c);
676                 }
677                 else if (c=='+')
678                 {
679                     if (buffer==null)
680                     {
681                         buffer=new Utf8StringBuffer(length);
682                         buffer.getStringBuffer().append(encoded,offset,offset+i);
683                     }
684                     
685                     buffer.getStringBuffer().append(' ');
686                 }
687                 else if (c=='%' && (i+2)<length)
688                 {
689                     if (buffer==null)
690                     {
691                         buffer=new Utf8StringBuffer(length);
692                         buffer.getStringBuffer().append(encoded,offset,offset+i);
693                     }
694 
695                     try
696                     {
697                         byte b=(byte)TypeUtil.parseInt(encoded,offset+i+1,2,16);
698                         buffer.append(b);
699                         i+=2;
700                     }
701                     catch(NumberFormatException nfe)
702                     {
703                         buffer.getStringBuffer().append('%');  
704                     }
705                 }
706                 else if (buffer!=null)
707                     buffer.getStringBuffer().append(c);
708             }
709 
710             if (buffer==null)
711             {
712                 if (offset==0 && encoded.length()==length)
713                     return encoded;
714                 return encoded.substring(offset,offset+length);
715             }
716 
717             return buffer.toString();
718         }
719         else
720         {
721             StringBuffer buffer=null;
722 
723             try
724             {
725                 for (int i=0;i<length;i++)
726                 {
727                     char c = encoded.charAt(offset+i);
728                     if (c<0||c>0xff)
729                     {
730                         if (buffer==null)
731                         {
732                             buffer=new StringBuffer(length);
733                             buffer.append(encoded,offset,offset+i+1);
734                         }
735                         else
736                             buffer.append(c);
737                     }
738                     else if (c=='+')
739                     {
740                         if (buffer==null)
741                         {
742                             buffer=new StringBuffer(length);
743                             buffer.append(encoded,offset,offset+i);
744                         }
745                         
746                         buffer.append(' ');
747                     }
748                     else if (c=='%' && (i+2)<length)
749                     {
750                         if (buffer==null)
751                         {
752                             buffer=new StringBuffer(length);
753                             buffer.append(encoded,offset,offset+i);
754                         }
755 
756                         byte[] ba=new byte[length];
757                         int n=0;
758                         while(c>=0 && c<=0xff)
759                         {
760                             if (c=='%')
761                             {   
762                                 if(i+2<length)
763                                 {
764                                     try
765                                     {
766                                         ba[n++]=(byte)TypeUtil.parseInt(encoded,offset+i+1,2,16);
767                                         i+=3;
768                                     }
769                                     catch(NumberFormatException nfe)
770                                     {                                        
771                                         ba[n-1] = (byte)'%';                                    
772                                         for(char next; ((next=encoded.charAt(++i+offset))!='%');)
773                                             ba[n++] = (byte)(next=='+' ? ' ' : next);
774                                     }
775                                 }
776                                 else
777                                 {
778                                     ba[n++] = (byte)'%';
779                                     i++;
780                                 }
781                             }
782                             else if (c=='+')
783                             {
784                                 ba[n++]=(byte)' ';
785                                 i++;
786                             }
787                             else
788                             {
789                                 ba[n++]=(byte)c;
790                                 i++;
791                             }
792                             
793                             if (i>=length)
794                                 break;
795                             c = encoded.charAt(offset+i);
796                         }
797 
798                         i--;
799                         buffer.append(new String(ba,0,n,charset));
800 
801                     }
802                     else if (buffer!=null)
803                         buffer.append(c);
804                 }
805 
806                 if (buffer==null)
807                 {
808                     if (offset==0 && encoded.length()==length)
809                         return encoded;
810                     return encoded.substring(offset,offset+length);
811                 }
812 
813                 return buffer.toString();
814             }
815             catch (UnsupportedEncodingException e)
816             {
817                 throw new RuntimeException(e);
818             }
819         }
820         
821     }
822     
823     /* ------------------------------------------------------------ */
824     /** Perform URL encoding.
825      * @param string 
826      * @return encoded string.
827      */
828     public static String encodeString(String string)
829     {
830         return encodeString(string,ENCODING);
831     }
832     
833     /* ------------------------------------------------------------ */
834     /** Perform URL encoding.
835      * @param string 
836      * @return encoded string.
837      */
838     public static String encodeString(String string,String charset)
839     {
840         if (charset==null)
841             charset=ENCODING;
842         byte[] bytes=null;
843         try
844         {
845             bytes=string.getBytes(charset);
846         }
847         catch(UnsupportedEncodingException e)
848         {
849             // LOG.warn(LogSupport.EXCEPTION,e);
850             bytes=string.getBytes();
851         }
852         
853         int len=bytes.length;
854         byte[] encoded= new byte[bytes.length*3];
855         int n=0;
856         boolean noEncode=true;
857         
858         for (int i=0;i<len;i++)
859         {
860             byte b = bytes[i];
861             
862             if (b==' ')
863             {
864                 noEncode=false;
865                 encoded[n++]=(byte)'+';
866             }
867             else if (b>='a' && b<='z' ||
868                      b>='A' && b<='Z' ||
869                      b>='0' && b<='9')
870             {
871                 encoded[n++]=b;
872             }
873             else
874             {
875                 noEncode=false;
876                 encoded[n++]=(byte)'%';
877                 byte nibble= (byte) ((b&0xf0)>>4);
878                 if (nibble>=10)
879                     encoded[n++]=(byte)('A'+nibble-10);
880                 else
881                     encoded[n++]=(byte)('0'+nibble);
882                 nibble= (byte) (b&0xf);
883                 if (nibble>=10)
884                     encoded[n++]=(byte)('A'+nibble-10);
885                 else
886                     encoded[n++]=(byte)('0'+nibble);
887             }
888         }
889 
890         if (noEncode)
891             return string;
892         
893         try
894         {    
895             return new String(encoded,0,n,charset);
896         }
897         catch(UnsupportedEncodingException e)
898         {
899             // LOG.warn(LogSupport.EXCEPTION,e);
900             return new String(encoded,0,n);
901         }
902     }
903 
904 
905     /* ------------------------------------------------------------ */
906     /** 
907      */
908     @Override
909     public Object clone()
910     {
911         return new UrlEncoded(this);
912     }
913 }