View Javadoc

1   // ========================================================================
2   // Copyright 2004-2005 Mort Bay Consulting Pty. Ltd.
3   // ------------------------------------------------------------------------
4   // Licensed under the Apache License, Version 2.0 (the "License");
5   // you may not use this file except in compliance with the License.
6   // You may obtain a copy of the License at 
7   // http://www.apache.org/licenses/LICENSE-2.0
8   // Unless required by applicable law or agreed to in writing, software
9   // distributed under the License is distributed on an "AS IS" BASIS,
10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11  // See the License for the specific language governing permissions and
12  // limitations under the License.
13  // ========================================================================
14  
15  package org.mortbay.util;
16  
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.io.InputStreamReader;
20  import java.io.StringWriter;
21  import java.io.UnsupportedEncodingException;
22  import java.util.Iterator;
23  import java.util.Map;
24  
25  import org.mortbay.log.Log;
26  
27  
28  /* ------------------------------------------------------------ */
29  /** Handles coding of MIME  "x-www-form-urlencoded".
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   * <p><h4>Notes</h4>
34   * The hashtable either contains String single values, vectors
35   * of String or arrays of Strings.
36   *
37   * This class is only partially synchronised.  In particular, simple
38   * get operations are not protected from concurrent updates.
39   *
40   * @see java.net.URLEncoder
41   * @author Greg Wilkins (gregw)
42   */
43  public class UrlEncoded extends MultiMap
44  {
45  
46      /* ----------------------------------------------------------------- */
47      public UrlEncoded(UrlEncoded url)
48      {
49          super(url);
50      }
51      
52      /* ----------------------------------------------------------------- */
53      public UrlEncoded()
54      {
55          super(6);
56      }
57      
58      /* ----------------------------------------------------------------- */
59      public UrlEncoded(String s)
60      {
61          super(6);
62          decode(s,StringUtil.__UTF8);
63      }
64      
65      /* ----------------------------------------------------------------- */
66      public UrlEncoded(String s, String charset)
67      {
68          super(6);
69          decode(s,charset);
70      }
71      
72      /* ----------------------------------------------------------------- */
73      public void decode(String query)
74      {
75          decodeTo(query,this,StringUtil.__UTF8);
76      }
77      
78      /* ----------------------------------------------------------------- */
79      public void decode(String query,String charset)
80      {
81          decodeTo(query,this,charset);
82      }
83      
84      /* -------------------------------------------------------------- */
85      /** Encode Hashtable with % encoding.
86       */
87      public String encode()
88      {
89          return encode(StringUtil.__UTF8,false);
90      }
91      
92      /* -------------------------------------------------------------- */
93      /** Encode Hashtable with % encoding.
94       */
95      public String encode(String charset)
96      {
97          return encode(charset,false);
98      }
99      
100     /* -------------------------------------------------------------- */
101     /** Encode Hashtable with % encoding.
102      * @param equalsForNullValue if True, then an '=' is always used, even
103      * for parameters without a value. e.g. "blah?a=&b=&c=".
104      */
105     public synchronized String encode(String charset, boolean equalsForNullValue)
106     {
107         return encode(this,charset,equalsForNullValue);
108     }
109     
110     /* -------------------------------------------------------------- */
111     /** Encode Hashtable with % encoding.
112      * @param equalsForNullValue if True, then an '=' is always used, even
113      * for parameters without a value. e.g. "blah?a=&b=&c=".
114      */
115     public static String encode(MultiMap map, String charset, boolean equalsForNullValue)
116     {
117         if (charset==null)
118             charset=StringUtil.__UTF8;
119         
120         StringBuffer result = new StringBuffer(128);
121         synchronized(result)
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=StringUtil.__UTF8;
178 
179         synchronized(map)
180         {
181             String key = null;
182             String value = null;
183             int mark=-1;
184             boolean encoded=false;
185             for (int i=0;i<content.length();i++)
186             {
187                 char c = content.charAt(i);
188                 switch (c)
189                 {
190                   case '&':
191                       int l=i-mark-1;
192                       value = l==0?"":
193                           (encoded?decodeString(content,mark+1,l,charset):content.substring(mark+1,i));
194                       mark=i;
195                       encoded=false;
196                       if (key != null)
197                       {
198                           map.add(key,value);
199                       }
200                       else if (value!=null&&value.length()>0)
201                       {
202                           map.add(value,"");
203                       }
204                       key = null;
205                       value=null;
206                       break;
207                   case '=':
208                       if (key!=null)
209                           break;
210                       key = encoded?decodeString(content,mark+1,i-mark-1,charset):content.substring(mark+1,i);
211                       mark=i;
212                       encoded=false;
213                       break;
214                   case '+':
215                       encoded=true;
216                       break;
217                   case '%':
218                       encoded=true;
219                       break;
220                 }                
221             }
222             
223             if (key != null)
224             {
225                 int l=content.length()-mark-1;
226                 value = l==0?"":(encoded?decodeString(content,mark+1,l,charset):content.substring(mark+1));
227                 map.add(key,value);
228             }
229             else if (mark<content.length())
230             {
231                 key = encoded
232                     ?decodeString(content,mark+1,content.length()-mark-1,charset)
233                     :content.substring(mark+1);
234                 map.add(key,"");
235             }
236         }
237     }
238 
239     /* -------------------------------------------------------------- */
240     /** Decoded parameters to Map.
241      * @param data the byte[] containing the encoded parameters
242      */
243     public static void decodeUtf8To(byte[] raw,int offset, int length, MultiMap map)
244     {
245         decodeUtf8To(raw,offset,length,map,new Utf8StringBuffer());
246     }
247 
248     /* -------------------------------------------------------------- */
249     /** Decoded parameters to Map.
250      * @param data the byte[] containing the encoded parameters
251      */
252     public static void decodeUtf8To(byte[] raw,int offset, int length, MultiMap map,Utf8StringBuffer buffer)
253     {
254         synchronized(map)
255         {
256             String key = null;
257             String value = null;
258             
259             // TODO cache of parameter names ???
260             int end=offset+length;
261             for (int i=offset;i<end;i++)
262             {
263                 byte b=raw[i];
264                 switch ((char)(0xff&b))
265                 {
266                     case '&':
267                         value = buffer.length()==0?"":buffer.toString();
268                         buffer.reset();
269                         if (key != null)
270                         {
271                             map.add(key,value);
272                         }
273                         else if (value!=null&&value.length()>0)
274                         {
275                             map.add(value,"");
276                         }
277                         key = null;
278                         value=null;
279                         break;
280                         
281                     case '=':
282                         if (key!=null)
283                         {
284                             buffer.append(b);
285                             break;
286                         }
287                         key = buffer.toString();
288                         buffer.reset();
289                         break;
290                         
291                     case '+':
292                         buffer.append((byte)' ');
293                         break;
294                         
295                     case '%':
296                         if (i+2<end)
297                             buffer.append((byte)((TypeUtil.convertHexDigit(raw[++i])<<4) + TypeUtil.convertHexDigit(raw[++i])));
298                         break;
299                     default:
300                         buffer.append(b);
301                     break;
302                 }
303             }
304             
305             if (key != null)
306             {
307                 value = buffer.length()==0?"":buffer.toString();
308                 buffer.reset();
309                 map.add(key,value);
310             }
311             else if (buffer.length()>0)
312             {
313                 map.add(buffer.toString(),"");
314             }
315         }
316     }
317 
318     /* -------------------------------------------------------------- */
319     /** Decoded parameters to Map.
320      * @param in InputSteam to read
321      * @param map MultiMap to add parameters to
322      * @param maxLength maximum length of content to read 0r -1 for no limit
323      */
324     public static void decode88591To(InputStream in, MultiMap map, int maxLength)
325     throws IOException
326     {
327         synchronized(map)
328         {
329             StringBuffer buffer = new StringBuffer();
330             String key = null;
331             String value = null;
332             
333             int b;
334 
335             // TODO cache of parameter names ???
336             int totalLength=0;
337             while ((b=in.read())>=0)
338             {
339                 switch ((char) b)
340                 {
341                     case '&':
342                         value = buffer.length()==0?"":buffer.toString();
343                         buffer.setLength(0);
344                         if (key != null)
345                         {
346                             map.add(key,value);
347                         }
348                         else if (value!=null&&value.length()>0)
349                         {
350                             map.add(value,"");
351                         }
352                         key = null;
353                         value=null;
354                         break;
355                         
356                     case '=':
357                         if (key!=null)
358                         {
359                             buffer.append((char)b);
360                             break;
361                         }
362                         key = buffer.toString();
363                         buffer.setLength(0);
364                         break;
365                         
366                     case '+':
367                         buffer.append((char)' ');
368                         break;
369                         
370                     case '%':
371                         int dh=in.read();
372                         int dl=in.read();
373                         if (dh<0||dl<0)
374                             break;
375                         buffer.append((char)((TypeUtil.convertHexDigit((byte)dh)<<4) + TypeUtil.convertHexDigit((byte)dl)));
376                         break;
377                     default:
378                         buffer.append((char)b);
379                     break;
380                 }
381                 if (maxLength>=0 && (++totalLength > maxLength))
382                     throw new IllegalStateException("Form too large");
383             }
384             
385             if (key != null)
386             {
387                 value = buffer.length()==0?"":buffer.toString();
388                 buffer.setLength(0);
389                 map.add(key,value);
390             }
391             else if (buffer.length()>0)
392             {
393                 map.add(buffer.toString(), "");
394             }
395         }
396     }
397     
398     /* -------------------------------------------------------------- */
399     /** Decoded parameters to Map.
400      * @param in InputSteam to read
401      * @param map MultiMap to add parameters to
402      * @param maxLength maximum length of conent to read 0r -1 for no limit
403      */
404     public static void decodeUtf8To(InputStream in, MultiMap map, int maxLength)
405     throws IOException
406     {
407         synchronized(map)
408         {
409             Utf8StringBuffer buffer = new Utf8StringBuffer();
410             String key = null;
411             String value = null;
412             
413             int b;
414             
415             // TODO cache of parameter names ???
416             int totalLength=0;
417             while ((b=in.read())>=0)
418             {
419                 switch ((char) b)
420                 {
421                     case '&':
422                         value = buffer.length()==0?"":buffer.toString();
423                         buffer.reset();
424                         if (key != null)
425                         {
426                             map.add(key,value);
427                         }
428                         else if (value!=null&&value.length()>0)
429                         {
430                             map.add(value,"");
431                         }
432                         key = null;
433                         value=null;
434                         break;
435                         
436                     case '=':
437                         if (key!=null)
438                         {
439                             buffer.append((byte)b);
440                             break;
441                         }
442                         key = buffer.toString();
443                         buffer.reset();
444                         break;
445                         
446                     case '+':
447                         buffer.append((byte)' ');
448                         break;
449                         
450                     case '%':
451                         int dh=in.read();
452                         int dl=in.read();
453                         if (dh<0||dl<0)
454                             break;
455                         buffer.append((byte)((TypeUtil.convertHexDigit((byte)dh)<<4) + TypeUtil.convertHexDigit((byte)dl)));
456                         break;
457                     default:
458                         buffer.append((byte)b);
459                     break;
460                 }
461                 if (maxLength>=0 && (++totalLength > maxLength))
462                     throw new IllegalStateException("Form too large");
463             }
464             
465             if (key != null)
466             {
467                 value = buffer.length()==0?"":buffer.toString();
468                 buffer.reset();
469                 map.add(key,value);
470             }
471             else if (buffer.length()>0)
472             {
473                 map.add(buffer.toString(), "");
474             }
475         }
476     }
477     
478     /* -------------------------------------------------------------- */
479     public static void decodeUtf16To(InputStream in, MultiMap map, int maxLength) throws IOException
480     {
481         InputStreamReader input = new InputStreamReader(in,StringUtil.__UTF16);
482         StringBuffer buf = new StringBuffer();
483 
484         int c;
485         int length=0;
486         if (maxLength<0)
487             maxLength=Integer.MAX_VALUE;
488         while ((c=input.read())>0 && length++<maxLength)
489             buf.append((char)c);
490         decodeTo(buf.toString(),map,StringUtil.__UTF8);
491     }
492     
493     /* -------------------------------------------------------------- */
494     /** Decoded parameters to Map.
495      * @param in the stream containing the encoded parameters
496      */
497     public static void decodeTo(InputStream in, MultiMap map, String charset, int maxLength)
498     throws IOException
499     {
500         if (charset==null || StringUtil.__ISO_8859_1.equals(charset))
501         {
502             decode88591To(in,map,maxLength);
503             return;
504         }
505 
506         if (StringUtil.__UTF8.equalsIgnoreCase(charset))
507         {
508             decodeUtf8To(in,map,maxLength);
509             return;
510         }
511 
512         if (StringUtil.__UTF16.equalsIgnoreCase(charset)) // Should be all 2 byte encodings
513         {
514             decodeUtf16To(in,map,maxLength);
515             return;
516         }
517         
518 
519         synchronized(map)
520         {
521             String key = null;
522             String value = null;
523             
524             int c;
525             int digit=0;
526             int digits=0;
527             
528             int totalLength = 0;
529             ByteArrayOutputStream2 output = new ByteArrayOutputStream2();
530             
531             int size=0;
532             
533             while ((c=in.read())>0)
534             {
535                 switch ((char) c)
536                 {
537                     case '&':
538                         size=output.size();
539                         value = size==0?"":output.toString(charset);
540                         output.setCount(0);
541                         if (key != null)
542                         {
543                             map.add(key,value);
544                         }
545                         else if (value!=null&&value.length()>0)
546                         {
547                             map.add(value,"");
548                         }
549                         key = null;
550                         value=null;
551                         break;
552                     case '=':
553                         if (key!=null)
554                         {
555                             output.write(c);
556                             break;
557                         }
558                         size=output.size();
559                         key = size==0?"":output.toString(charset);
560                         output.setCount(0);
561                         break;
562                     case '+':
563                         output.write(' ');
564                         break;
565                     case '%':
566                         digits=2;
567                         break;
568                     default:
569                         if (digits==2)
570                         {
571                             digit=TypeUtil.convertHexDigit((byte)c);
572                             digits=1;
573                         }
574                         else if (digits==1)
575                         {
576                             output.write((digit<<4) + TypeUtil.convertHexDigit((byte)c));
577                             digits=0;
578                         }
579                         else
580                             output.write(c);
581                     break;
582                 }
583                 
584                 totalLength++;
585                 if (maxLength>=0 && totalLength > maxLength)
586                     throw new IllegalStateException("Form too large");
587             }
588 
589             size=output.size();
590             if (key != null)
591             {
592                 value = size==0?"":output.toString(charset);
593                 output.setCount(0);
594                 map.add(key,value);
595             }
596             else if (size>0)
597                 map.add(output.toString(charset),"");
598         }
599     }
600     
601     /* -------------------------------------------------------------- */
602     /** Decode String with % encoding.
603      * This method makes the assumption that the majority of calls
604      * will need no decoding.
605      */
606     public static String decodeString(String encoded,int offset,int length,String charset)
607     {
608         if (charset==null)
609             charset=StringUtil.__UTF8;
610         byte[] bytes=null;
611         int n=0;
612         
613         for (int i=0;i<length;i++)
614         {
615             char c = encoded.charAt(offset+i);
616             if (c<0||c>0xff)
617                 throw new IllegalArgumentException("Not encoded");
618             
619             if (c=='+')
620             {
621                 if (bytes==null)
622                 {
623                     bytes=new byte[length*2];
624                     encoded.getBytes(offset, offset+i, bytes, 0);
625                     n=i;
626                 }
627                 bytes[n++] = (byte) ' ';
628             }
629             else if (c=='%' && (i+2)<length)
630             {
631                 byte b;
632                 char cn = encoded.charAt(offset+i+1);
633                 if (cn>='a' && cn<='z')
634                     b=(byte)(10+cn-'a');
635                 else if (cn>='A' && cn<='Z')
636                     b=(byte)(10+cn-'A');
637                 else
638                     b=(byte)(cn-'0');
639                 cn = encoded.charAt(offset+i+2);
640                 if (cn>='a' && cn<='z')
641                     b=(byte)(b*16+10+cn-'a');
642                 else if (cn>='A' && cn<='Z')
643                     b=(byte)(b*16+10+cn-'A');
644                 else
645                     b=(byte)(b*16+cn-'0');
646 
647                 if (bytes==null)
648                 {
649                     bytes=new byte[length];
650                     encoded.getBytes(offset, offset+i, bytes, 0);
651                     n=i;
652                 }
653                 i+=2;
654                 bytes[n++]=b;
655             }
656             else if (n>0)
657                 bytes[n++] = (byte) c;
658         }
659 
660         if (bytes==null)
661         {
662             if (offset==0 && encoded.length()==length)
663                 return encoded;
664             return encoded.substring(offset,offset+length);
665         }
666         
667         try
668         {
669             return new String(bytes,0,n,charset);
670         }
671         catch (UnsupportedEncodingException e)
672         {
673             Log.warn(e.toString());
674             Log.debug(e);
675             return new String(bytes,0,n);
676         }
677         
678     }
679     
680     /* ------------------------------------------------------------ */
681     /** Perform URL encoding.
682      * Assumes 8859 charset
683      * @param string 
684      * @return encoded string.
685      */
686     public static String encodeString(String string)
687     {
688         return encodeString(string,StringUtil.__UTF8);
689     }
690     
691     /* ------------------------------------------------------------ */
692     /** Perform URL encoding.
693      * @param string 
694      * @return encoded string.
695      */
696     public static String encodeString(String string,String charset)
697     {
698         if (charset==null)
699             charset=StringUtil.__UTF8;
700         byte[] bytes=null;
701         try
702         {
703             bytes=string.getBytes(charset);
704         }
705         catch(UnsupportedEncodingException e)
706         {
707             // Log.warn(LogSupport.EXCEPTION,e);
708             bytes=string.getBytes();
709         }
710         
711         int len=bytes.length;
712         byte[] encoded= new byte[bytes.length*3];
713         int n=0;
714         boolean noEncode=true;
715         
716         for (int i=0;i<len;i++)
717         {
718             byte b = bytes[i];
719             
720             if (b==' ')
721             {
722                 noEncode=false;
723                 encoded[n++]=(byte)'+';
724             }
725             else if (b>='a' && b<='z' ||
726                      b>='A' && b<='Z' ||
727                      b>='0' && b<='9')
728             {
729                 encoded[n++]=b;
730             }
731             else
732             {
733                 noEncode=false;
734                 encoded[n++]=(byte)'%';
735                 byte nibble= (byte) ((b&0xf0)>>4);
736                 if (nibble>=10)
737                     encoded[n++]=(byte)('A'+nibble-10);
738                 else
739                     encoded[n++]=(byte)('0'+nibble);
740                 nibble= (byte) (b&0xf);
741                 if (nibble>=10)
742                     encoded[n++]=(byte)('A'+nibble-10);
743                 else
744                     encoded[n++]=(byte)('0'+nibble);
745             }
746         }
747 
748         if (noEncode)
749             return string;
750         
751         try
752         {    
753             return new String(encoded,0,n,charset);
754         }
755         catch(UnsupportedEncodingException e)
756         {
757             // Log.warn(LogSupport.EXCEPTION,e);
758             return new String(encoded,0,n);
759         }
760     }
761 
762 
763     /* ------------------------------------------------------------ */
764     /** 
765      */
766     public Object clone()
767     {
768         return new UrlEncoded(this);
769     }
770 }