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                 if (key != null && key.length() > 0)
235                 {
236                     map.add(key,"");
237                 }
238             }
239         }
240     }
241 
242     /* -------------------------------------------------------------- */
243     /** Decoded parameters to Map.
244      * @param raw the byte[] containing the encoded parameters
245      * @param offset the offset within raw to decode from
246      * @param length the length of the section to decode
247      * @param map the {@link MultiMap} to populate
248      */
249     public static void decodeUtf8To(byte[] raw,int offset, int length, MultiMap map)
250     {
251         decodeUtf8To(raw,offset,length,map,new Utf8StringBuilder());
252     }
253 
254     /* -------------------------------------------------------------- */
255     /** Decoded parameters to Map.
256      * @param raw the byte[] containing the encoded parameters
257      * @param offset the offset within raw to decode from
258      * @param length the length of the section to decode
259      * @param map the {@link MultiMap} to populate
260      * @param buffer the buffer to decode into
261      */
262     public static void decodeUtf8To(byte[] raw,int offset, int length, MultiMap map,Utf8StringBuilder buffer)
263     {
264         synchronized(map)
265         {
266             String key = null;
267             String value = null;
268             
269             // TODO cache of parameter names ???
270             int end=offset+length;
271             for (int i=offset;i<end;i++)
272             {
273                 byte b=raw[i];
274                 switch ((char)(0xff&b))
275                 {
276                     case '&':
277                         value = buffer.length()==0?"":buffer.toString();
278                         buffer.reset();
279                         if (key != null)
280                         {
281                             map.add(key,value);
282                         }
283                         else if (value!=null&&value.length()>0)
284                         {
285                             map.add(value,"");
286                         }
287                         key = null;
288                         value=null;
289                         break;
290                         
291                     case '=':
292                         if (key!=null)
293                         {
294                             buffer.append(b);
295                             break;
296                         }
297                         key = buffer.toString();
298                         buffer.reset();
299                         break;
300                         
301                     case '+':
302                         buffer.append((byte)' ');
303                         break;
304                         
305                     case '%':
306                         if (i+2<end)
307                             buffer.append((byte)((TypeUtil.convertHexDigit(raw[++i])<<4) + TypeUtil.convertHexDigit(raw[++i])));
308                         break;
309                     default:
310                         buffer.append(b);
311                     break;
312                 }
313             }
314             
315             if (key != null)
316             {
317                 value = buffer.length()==0?"":buffer.toString();
318                 buffer.reset();
319                 map.add(key,value);
320             }
321             else if (buffer.length()>0)
322             {
323                 map.add(buffer.toString(),"");
324             }
325         }
326     }
327 
328     /* -------------------------------------------------------------- */
329     /** Decoded parameters to Map.
330      * @param in InputSteam to read
331      * @param map MultiMap to add parameters to
332      * @param maxLength maximum length of content to read 0r -1 for no limit
333      */
334     public static void decode88591To(InputStream in, MultiMap map, int maxLength)
335     throws IOException
336     {
337         synchronized(map)
338         {
339             StringBuffer buffer = new StringBuffer();
340             String key = null;
341             String value = null;
342             
343             int b;
344 
345             // TODO cache of parameter names ???
346             int totalLength=0;
347             while ((b=in.read())>=0)
348             {
349                 switch ((char) b)
350                 {
351                     case '&':
352                         value = buffer.length()==0?"":buffer.toString();
353                         buffer.setLength(0);
354                         if (key != null)
355                         {
356                             map.add(key,value);
357                         }
358                         else if (value!=null&&value.length()>0)
359                         {
360                             map.add(value,"");
361                         }
362                         key = null;
363                         value=null;
364                         break;
365                         
366                     case '=':
367                         if (key!=null)
368                         {
369                             buffer.append((char)b);
370                             break;
371                         }
372                         key = buffer.toString();
373                         buffer.setLength(0);
374                         break;
375                         
376                     case '+':
377                         buffer.append(' ');
378                         break;
379                         
380                     case '%':
381                         int dh=in.read();
382                         int dl=in.read();
383                         if (dh<0||dl<0)
384                             break;
385                         buffer.append((char)((TypeUtil.convertHexDigit((byte)dh)<<4) + TypeUtil.convertHexDigit((byte)dl)));
386                         break;
387                     default:
388                         buffer.append((char)b);
389                     break;
390                 }
391                 if (maxLength>=0 && (++totalLength > maxLength))
392                     throw new IllegalStateException("Form too large");
393             }
394             
395             if (key != null)
396             {
397                 value = buffer.length()==0?"":buffer.toString();
398                 buffer.setLength(0);
399                 map.add(key,value);
400             }
401             else if (buffer.length()>0)
402             {
403                 map.add(buffer.toString(), "");
404             }
405         }
406     }
407     
408     /* -------------------------------------------------------------- */
409     /** Decoded parameters to Map.
410      * @param in InputSteam to read
411      * @param map MultiMap to add parameters to
412      * @param maxLength maximum length of content to read 0r -1 for no limit
413      */
414     public static void decodeUtf8To(InputStream in, MultiMap map, int maxLength)
415     throws IOException
416     {
417         synchronized(map)
418         {
419             Utf8StringBuilder buffer = new Utf8StringBuilder();
420             String key = null;
421             String value = null;
422             
423             int b;
424             
425             // TODO cache of parameter names ???
426             int totalLength=0;
427             while ((b=in.read())>=0)
428             {
429                 switch ((char) b)
430                 {
431                     case '&':
432                         value = buffer.length()==0?"":buffer.toString();
433                         buffer.reset();
434                         if (key != null)
435                         {
436                             map.add(key,value);
437                         }
438                         else if (value!=null&&value.length()>0)
439                         {
440                             map.add(value,"");
441                         }
442                         key = null;
443                         value=null;
444                         break;
445                         
446                     case '=':
447                         if (key!=null)
448                         {
449                             buffer.append((byte)b);
450                             break;
451                         }
452                         key = buffer.toString();
453                         buffer.reset();
454                         break;
455                         
456                     case '+':
457                         buffer.append((byte)' ');
458                         break;
459                         
460                     case '%':
461                         int dh=in.read();
462                         int dl=in.read();
463                         if (dh<0||dl<0)
464                             break;
465                         buffer.append((byte)((TypeUtil.convertHexDigit((byte)dh)<<4) + TypeUtil.convertHexDigit((byte)dl)));
466                         break;
467                     default:
468                         buffer.append((byte)b);
469                     break;
470                 }
471                 if (maxLength>=0 && (++totalLength > maxLength))
472                     throw new IllegalStateException("Form too large");
473             }
474             
475             if (key != null)
476             {
477                 value = buffer.length()==0?"":buffer.toString();
478                 buffer.reset();
479                 map.add(key,value);
480             }
481             else if (buffer.length()>0)
482             {
483                 map.add(buffer.toString(), "");
484             }
485         }
486     }
487     
488     /* -------------------------------------------------------------- */
489     public static void decodeUtf16To(InputStream in, MultiMap map, int maxLength) throws IOException
490     {
491         InputStreamReader input = new InputStreamReader(in,StringUtil.__UTF16);
492         StringBuffer buf = new StringBuffer();
493 
494         int c;
495         int length=0;
496         if (maxLength<0)
497             maxLength=Integer.MAX_VALUE;
498         while ((c=input.read())>0 && length++<maxLength)
499             buf.append((char)c);
500         decodeTo(buf.toString(),map,ENCODING);
501     }
502     
503     /* -------------------------------------------------------------- */
504     /** Decoded parameters to Map.
505      * @param in the stream containing the encoded parameters
506      */
507     public static void decodeTo(InputStream in, MultiMap map, String charset, int maxLength)
508     throws IOException
509     {
510         //no charset present, use the configured default
511         if (charset==null) 
512         {
513            charset=ENCODING;
514         }
515             
516             
517         if (StringUtil.__UTF8.equalsIgnoreCase(charset))
518         {
519             decodeUtf8To(in,map,maxLength);
520             return;
521         }
522         
523         if (StringUtil.__ISO_8859_1.equals(charset))
524         {
525             decode88591To(in,map,maxLength);
526             return;
527         }
528 
529         if (StringUtil.__UTF16.equalsIgnoreCase(charset)) // Should be all 2 byte encodings
530         {
531             decodeUtf16To(in,map,maxLength);
532             return;
533         }
534         
535 
536         synchronized(map)
537         {
538             String key = null;
539             String value = null;
540             
541             int c;
542             int digit=0;
543             int digits=0;
544             
545             int totalLength = 0;
546             ByteArrayOutputStream2 output = new ByteArrayOutputStream2();
547             
548             int size=0;
549             
550             while ((c=in.read())>0)
551             {
552                 switch ((char) c)
553                 {
554                     case '&':
555                         size=output.size();
556                         value = size==0?"":output.toString(charset);
557                         output.setCount(0);
558                         if (key != null)
559                         {
560                             map.add(key,value);
561                         }
562                         else if (value!=null&&value.length()>0)
563                         {
564                             map.add(value,"");
565                         }
566                         key = null;
567                         value=null;
568                         break;
569                     case '=':
570                         if (key!=null)
571                         {
572                             output.write(c);
573                             break;
574                         }
575                         size=output.size();
576                         key = size==0?"":output.toString(charset);
577                         output.setCount(0);
578                         break;
579                     case '+':
580                         output.write(' ');
581                         break;
582                     case '%':
583                         digits=2;
584                         break;
585                     default:
586                         if (digits==2)
587                         {
588                             digit=TypeUtil.convertHexDigit((byte)c);
589                             digits=1;
590                         }
591                         else if (digits==1)
592                         {
593                             output.write((digit<<4) + TypeUtil.convertHexDigit((byte)c));
594                             digits=0;
595                         }
596                         else
597                             output.write(c);
598                     break;
599                 }
600                 
601                 totalLength++;
602                 if (maxLength>=0 && totalLength > maxLength)
603                     throw new IllegalStateException("Form too large");
604             }
605 
606             size=output.size();
607             if (key != null)
608             {
609                 value = size==0?"":output.toString(charset);
610                 output.setCount(0);
611                 map.add(key,value);
612             }
613             else if (size>0)
614                 map.add(output.toString(charset),"");
615         }
616     }
617     
618     /* -------------------------------------------------------------- */
619     /** Decode String with % encoding.
620      * This method makes the assumption that the majority of calls
621      * will need no decoding.
622      */
623     public static String decodeString(String encoded,int offset,int length,String charset)
624     {
625         if (charset==null || StringUtil.isUTF8(charset))
626         {
627             Utf8StringBuffer buffer=null;
628 
629             for (int i=0;i<length;i++)
630             {
631                 char c = encoded.charAt(offset+i);
632                 if (c<0||c>0xff)
633                 {
634                     if (buffer==null)
635                     {
636                         buffer=new Utf8StringBuffer(length);
637                         buffer.getStringBuffer().append(encoded,offset,offset+i+1);
638                     }
639                     else
640                         buffer.getStringBuffer().append(c);
641                 }
642                 else if (c=='+')
643                 {
644                     if (buffer==null)
645                     {
646                         buffer=new Utf8StringBuffer(length);
647                         buffer.getStringBuffer().append(encoded,offset,offset+i);
648                     }
649                     
650                     buffer.getStringBuffer().append(' ');
651                 }
652                 else if (c=='%' && (i+2)<length)
653                 {
654                     if (buffer==null)
655                     {
656                         buffer=new Utf8StringBuffer(length);
657                         buffer.getStringBuffer().append(encoded,offset,offset+i);
658                     }
659 
660                     try
661                     {
662                         byte b=(byte)TypeUtil.parseInt(encoded,offset+i+1,2,16);
663                         buffer.append(b);
664                         i+=2;
665                     }
666                     catch(NumberFormatException nfe)
667                     {
668                         buffer.getStringBuffer().append('%');  
669                     }
670                 }
671                 else if (buffer!=null)
672                     buffer.getStringBuffer().append(c);
673             }
674 
675             if (buffer==null)
676             {
677                 if (offset==0 && encoded.length()==length)
678                     return encoded;
679                 return encoded.substring(offset,offset+length);
680             }
681 
682             return buffer.toString();
683         }
684         else
685         {
686             StringBuffer buffer=null;
687 
688             try
689             {
690                 for (int i=0;i<length;i++)
691                 {
692                     char c = encoded.charAt(offset+i);
693                     if (c<0||c>0xff)
694                     {
695                         if (buffer==null)
696                         {
697                             buffer=new StringBuffer(length);
698                             buffer.append(encoded,offset,offset+i+1);
699                         }
700                         else
701                             buffer.append(c);
702                     }
703                     else if (c=='+')
704                     {
705                         if (buffer==null)
706                         {
707                             buffer=new StringBuffer(length);
708                             buffer.append(encoded,offset,offset+i);
709                         }
710                         
711                         buffer.append(' ');
712                     }
713                     else if (c=='%' && (i+2)<length)
714                     {
715                         if (buffer==null)
716                         {
717                             buffer=new StringBuffer(length);
718                             buffer.append(encoded,offset,offset+i);
719                         }
720 
721                         byte[] ba=new byte[length];
722                         int n=0;
723                         while(c>=0 && c<=0xff)
724                         {
725                             if (c=='%')
726                             {   
727                                 if(i+2<length)
728                                 {
729                                     try
730                                     {
731                                         ba[n++]=(byte)TypeUtil.parseInt(encoded,offset+i+1,2,16);
732                                         i+=3;
733                                     }
734                                     catch(NumberFormatException nfe)
735                                     {                                        
736                                         ba[n-1] = (byte)'%';                                    
737                                         for(char next; ((next=encoded.charAt(++i+offset))!='%');)
738                                             ba[n++] = (byte)(next=='+' ? ' ' : next);
739                                     }
740                                 }
741                                 else
742                                 {
743                                     ba[n++] = (byte)'%';
744                                     i++;
745                                 }
746                             }
747                             else if (c=='+')
748                             {
749                                 ba[n++]=(byte)' ';
750                                 i++;
751                             }
752                             else
753                             {
754                                 ba[n++]=(byte)c;
755                                 i++;
756                             }
757                             
758                             if (i>=length)
759                                 break;
760                             c = encoded.charAt(offset+i);
761                         }
762 
763                         i--;
764                         buffer.append(new String(ba,0,n,charset));
765 
766                     }
767                     else if (buffer!=null)
768                         buffer.append(c);
769                 }
770 
771                 if (buffer==null)
772                 {
773                     if (offset==0 && encoded.length()==length)
774                         return encoded;
775                     return encoded.substring(offset,offset+length);
776                 }
777 
778                 return buffer.toString();
779             }
780             catch (UnsupportedEncodingException e)
781             {
782                 throw new RuntimeException(e);
783             }
784         }
785         
786     }
787     
788     /* ------------------------------------------------------------ */
789     /** Perform URL encoding.
790      * @param string 
791      * @return encoded string.
792      */
793     public static String encodeString(String string)
794     {
795         return encodeString(string,ENCODING);
796     }
797     
798     /* ------------------------------------------------------------ */
799     /** Perform URL encoding.
800      * @param string 
801      * @return encoded string.
802      */
803     public static String encodeString(String string,String charset)
804     {
805         if (charset==null)
806             charset=ENCODING;
807         byte[] bytes=null;
808         try
809         {
810             bytes=string.getBytes(charset);
811         }
812         catch(UnsupportedEncodingException e)
813         {
814             // Log.warn(LogSupport.EXCEPTION,e);
815             bytes=string.getBytes();
816         }
817         
818         int len=bytes.length;
819         byte[] encoded= new byte[bytes.length*3];
820         int n=0;
821         boolean noEncode=true;
822         
823         for (int i=0;i<len;i++)
824         {
825             byte b = bytes[i];
826             
827             if (b==' ')
828             {
829                 noEncode=false;
830                 encoded[n++]=(byte)'+';
831             }
832             else if (b>='a' && b<='z' ||
833                      b>='A' && b<='Z' ||
834                      b>='0' && b<='9')
835             {
836                 encoded[n++]=b;
837             }
838             else
839             {
840                 noEncode=false;
841                 encoded[n++]=(byte)'%';
842                 byte nibble= (byte) ((b&0xf0)>>4);
843                 if (nibble>=10)
844                     encoded[n++]=(byte)('A'+nibble-10);
845                 else
846                     encoded[n++]=(byte)('0'+nibble);
847                 nibble= (byte) (b&0xf);
848                 if (nibble>=10)
849                     encoded[n++]=(byte)('A'+nibble-10);
850                 else
851                     encoded[n++]=(byte)('0'+nibble);
852             }
853         }
854 
855         if (noEncode)
856             return string;
857         
858         try
859         {    
860             return new String(encoded,0,n,charset);
861         }
862         catch(UnsupportedEncodingException e)
863         {
864             // Log.warn(LogSupport.EXCEPTION,e);
865             return new String(encoded,0,n);
866         }
867     }
868 
869 
870     /* ------------------------------------------------------------ */
871     /** 
872      */
873     @Override
874     public Object clone()
875     {
876         return new UrlEncoded(this);
877     }
878 }