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