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