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