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