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