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