View Javadoc

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