2 // ========================================================================
3 // Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
4 // ------------------------------------------------------------------------
5 // All rights reserved. This program and the accompanying materials
6 // are made available under the terms of the Eclipse Public License v1.0
7 // and Apache License v2.0 which accompanies this distribution.
9 // The Eclipse Public License is available at
10 // http://www.eclipse.org/legal/epl-v10.html
12 // The Apache License v2.0 is available at
13 // http://www.opensource.org/licenses/apache2.0.php
15 // You may elect to redistribute this code under either of these licenses.
16 // ========================================================================
19 package org.eclipse.jetty.util;
21 import static org.eclipse.jetty.util.TypeUtil.convertHexDigit;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.InputStreamReader;
26 import java.io.StringWriter;
27 import java.nio.charset.Charset;
28 import java.nio.charset.StandardCharsets;
29 import java.util.List;
32 import org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception;
33 import org.eclipse.jetty.util.log.Log;
34 import org.eclipse.jetty.util.log.Logger;
36 /* ------------------------------------------------------------ */
37 /** Handles coding of MIME "x-www-form-urlencoded".
39 * This class handles the encoding and decoding for either
40 * the query string of a URL or the _content of a POST HTTP request.
43 * The UTF-8 charset is assumed, unless otherwise defined by either
44 * passing a parameter or setting the "org.eclipse.jetty.util.UrlEncoding.charset"
47 * The hashtable either contains String single values, vectors
48 * of String or arrays of Strings.
50 * This class is only partially synchronised. In particular, simple
51 * get operations are not protected from concurrent updates.
53 * @see java.net.URLEncoder
55 @SuppressWarnings("serial")
56 public class UrlEncoded extends MultiMap<String> implements Cloneable
58 static final Logger LOG = Log.getLogger(UrlEncoded.class);
60 public static final Charset ENCODING;
66 String charset = System.getProperty("org.eclipse.jetty.util.UrlEncoding.charset");
67 encoding = charset == null ? StandardCharsets.UTF_8 : Charset.forName(charset);
72 encoding=StandardCharsets.UTF_8;
77 /* ----------------------------------------------------------------- */
78 public UrlEncoded(UrlEncoded url)
83 /* ----------------------------------------------------------------- */
88 public UrlEncoded(String query)
90 decodeTo(query,this,ENCODING,-1);
93 /* ----------------------------------------------------------------- */
94 public void decode(String query)
96 decodeTo(query,this,ENCODING,-1);
99 /* ----------------------------------------------------------------- */
100 public void decode(String query,Charset charset)
102 decodeTo(query,this,charset,-1);
105 /* -------------------------------------------------------------- */
106 /** Encode Hashtable with % encoding.
108 public String encode()
110 return encode(ENCODING,false);
113 /* -------------------------------------------------------------- */
114 /** Encode Hashtable with % encoding.
116 public String encode(Charset charset)
118 return encode(charset,false);
121 /* -------------------------------------------------------------- */
122 /** Encode Hashtable with % encoding.
123 * @param equalsForNullValue if True, then an '=' is always used, even
124 * for parameters without a value. e.g. "blah?a=&b=&c=".
126 public synchronized String encode(Charset charset, boolean equalsForNullValue)
128 return encode(this,charset,equalsForNullValue);
131 /* -------------------------------------------------------------- */
132 /** Encode Hashtable with % encoding.
133 * @param equalsForNullValue if True, then an '=' is always used, even
134 * for parameters without a value. e.g. "blah?a=&b=&c=".
136 public static String encode(MultiMap<String> map, Charset charset, boolean equalsForNullValue)
141 StringBuilder result = new StringBuilder(128);
143 boolean delim = false;
144 for(Map.Entry<String, List<String>> entry: map.entrySet())
146 String key = entry.getKey().toString();
147 List<String> list = entry.getValue();
157 result.append(encodeString(key,charset));
158 if(equalsForNullValue)
163 for (int i=0;i<s;i++)
167 String val=list.get(i);
168 result.append(encodeString(key,charset));
172 String str=val.toString();
176 result.append(encodeString(str,charset));
178 else if (equalsForNullValue)
181 else if (equalsForNullValue)
187 return result.toString();
190 /* -------------------------------------------------------------- */
191 /** Decoded parameters to Map.
192 * @param content the string containing the encoded parameters
194 public static void decodeTo(String content, MultiMap<String> map, String charset, int maxKeys)
196 decodeTo(content,map,charset==null?null:Charset.forName(charset),maxKeys);
199 /* -------------------------------------------------------------- */
200 /** Decoded parameters to Map.
201 * @param content the string containing the encoded parameters
203 public static void decodeTo(String content, MultiMap<String> map, Charset charset, int maxKeys)
213 boolean encoded=false;
214 for (int i=0;i<content.length();i++)
216 char c = content.charAt(i);
222 (encoded?decodeString(content,mark+1,l,charset):content.substring(mark+1,i));
229 else if (value!=null&&value.length()>0)
235 if (maxKeys>0 && map.size()>maxKeys)
236 throw new IllegalStateException("Form too many keys");
241 key = encoded?decodeString(content,mark+1,i-mark-1,charset):content.substring(mark+1,i);
256 int l=content.length()-mark-1;
257 value = l==0?"":(encoded?decodeString(content,mark+1,l,charset):content.substring(mark+1));
260 else if (mark<content.length())
263 ?decodeString(content,mark+1,content.length()-mark-1,charset)
264 :content.substring(mark+1);
265 if (key != null && key.length() > 0)
273 /* -------------------------------------------------------------- */
274 /** Decoded parameters to Map.
275 * @param raw the byte[] containing the encoded parameters
276 * @param offset the offset within raw to decode from
277 * @param length the length of the section to decode
278 * @param map the {@link MultiMap} to populate
280 public static void decodeUtf8To(byte[] raw,int offset, int length, MultiMap<String> map)
282 Utf8StringBuilder buffer = new Utf8StringBuilder();
288 int end=offset+length;
289 for (int i=offset;i<end;i++)
294 switch ((char)(0xff&b))
297 value = buffer.toReplacedString();
303 else if (value!=null&&value.length()>0)
317 key = buffer.toReplacedString();
322 buffer.append((byte)' ');
337 buffer.getStringBuilder().append(Character.toChars((convertHexDigit(top)<<12) +(convertHexDigit(hi)<<8) + (convertHexDigit(lo)<<4) +convertHexDigit(bot)));
341 buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
349 buffer.append((byte)((convertHexDigit(hi)<<4) + convertHexDigit(lo)));
354 buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
364 catch(NotUtf8Exception e)
366 LOG.warn(e.toString());
369 catch(NumberFormatException e)
371 buffer.append(Utf8Appendable.REPLACEMENT_UTF8,0,3);
372 LOG.warn(e.toString());
379 value = buffer.toReplacedString();
383 else if (buffer.length()>0)
385 map.add(buffer.toReplacedString(),"");
390 /* -------------------------------------------------------------- */
391 /** Decoded parameters to Map.
392 * @param in InputSteam to read
393 * @param map MultiMap to add parameters to
394 * @param maxLength maximum number of keys to read or -1 for no limit
396 public static void decode88591To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
401 StringBuffer buffer = new StringBuffer();
408 while ((b=in.read())>=0)
413 value = buffer.length()==0?"":buffer.toString();
419 else if (value!=null&&value.length()>0)
425 if (maxKeys>0 && map.size()>maxKeys)
426 throw new IllegalStateException("Form too many keys");
432 buffer.append((char)b);
435 key = buffer.toString();
455 buffer.append(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3)));
463 buffer.append((char)((convertHexDigit(code0)<<4)+convertHexDigit(code1)));
468 buffer.append((char)b);
471 if (maxLength>=0 && (++totalLength > maxLength))
472 throw new IllegalStateException("Form too large");
477 value = buffer.length()==0?"":buffer.toString();
481 else if (buffer.length()>0)
483 map.add(buffer.toString(), "");
488 /* -------------------------------------------------------------- */
489 /** Decoded parameters to Map.
490 * @param in InputSteam to read
491 * @param map MultiMap to add parameters to
492 * @param maxLength maximum number of keys to read or -1 for no limit
494 public static void decodeUtf8To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
499 Utf8StringBuilder buffer = new Utf8StringBuilder();
506 while ((b=in.read())>=0)
513 value = buffer.toReplacedString();
519 else if (value!=null&&value.length()>0)
525 if (maxKeys>0 && map.size()>maxKeys)
526 throw new IllegalStateException("Form too many keys");
532 buffer.append((byte)b);
535 key = buffer.toReplacedString();
540 buffer.append((byte)' ');
545 boolean decoded=false;
548 code0=in.read(); // XXX: we have to read the next byte, otherwise code0 is always 'u'
560 buffer.getStringBuilder().append(Character.toChars
561 ((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3)));
573 buffer.append((byte)((convertHexDigit(code0)<<4)+convertHexDigit(code1)));
579 buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
584 buffer.append((byte)b);
588 catch(NotUtf8Exception e)
590 LOG.warn(e.toString());
593 catch(NumberFormatException e)
595 buffer.append(Utf8Appendable.REPLACEMENT_UTF8,0,3);
596 LOG.warn(e.toString());
599 if (maxLength>=0 && (++totalLength > maxLength))
600 throw new IllegalStateException("Form too large");
605 value = buffer.toReplacedString();
609 else if (buffer.length()>0)
611 map.add(buffer.toReplacedString(), "");
616 /* -------------------------------------------------------------- */
617 public static void decodeUtf16To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys) throws IOException
619 InputStreamReader input = new InputStreamReader(in,StandardCharsets.UTF_16);
620 StringWriter buf = new StringWriter(8192);
621 IO.copy(input,buf,maxLength);
623 decodeTo(buf.getBuffer().toString(),map,StandardCharsets.UTF_16,maxKeys);
626 /* -------------------------------------------------------------- */
627 /** Decoded parameters to Map.
628 * @param in the stream containing the encoded parameters
630 public static void decodeTo(InputStream in, MultiMap<String> map, String charset, int maxLength, int maxKeys)
635 if (ENCODING.equals(StandardCharsets.UTF_8))
636 decodeUtf8To(in,map,maxLength,maxKeys);
638 decodeTo(in,map,ENCODING,maxLength,maxKeys);
640 else if (StringUtil.__UTF8.equalsIgnoreCase(charset))
641 decodeUtf8To(in,map,maxLength,maxKeys);
642 else if (StringUtil.__ISO_8859_1.equalsIgnoreCase(charset))
643 decode88591To(in,map,maxLength,maxKeys);
644 else if (StringUtil.__UTF16.equalsIgnoreCase(charset))
645 decodeUtf16To(in,map,maxLength,maxKeys);
647 decodeTo(in,map,Charset.forName(charset),maxLength,maxKeys);
650 /* -------------------------------------------------------------- */
651 /** Decoded parameters to Map.
652 * @param in the stream containing the encoded parameters
654 public static void decodeTo(InputStream in, MultiMap<String> map, Charset charset, int maxLength, int maxKeys)
657 //no charset present, use the configured default
661 if (StandardCharsets.UTF_8.equals(charset))
663 decodeUtf8To(in,map,maxLength,maxKeys);
667 if (StandardCharsets.ISO_8859_1.equals(charset))
669 decode88591To(in,map,maxLength,maxKeys);
673 if (StandardCharsets.UTF_16.equals(charset)) // Should be all 2 byte encodings
675 decodeUtf16To(in,map,maxLength,maxKeys);
687 ByteArrayOutputStream2 output = new ByteArrayOutputStream2();
691 while ((c=in.read())>0)
697 value = size==0?"":output.toString(charset);
703 else if (value!=null&&value.length()>0)
709 if (maxKeys>0 && map.size()>maxKeys)
710 throw new IllegalStateException("Form too many keys");
719 key = size==0?"":output.toString(charset);
737 output.write(new String(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3))).getBytes(charset));
746 output.write((convertHexDigit(code0)<<4)+convertHexDigit(code1));
755 if (maxLength>=0 && totalLength > maxLength)
756 throw new IllegalStateException("Form too large");
762 value = size==0?"":output.toString(charset);
767 map.add(output.toString(charset),"");
771 /* -------------------------------------------------------------- */
772 /** Decode String with % encoding.
773 * This method makes the assumption that the majority of calls
774 * will need no decoding.
776 public static String decodeString(String encoded,int offset,int length,Charset charset)
778 if (charset==null || StandardCharsets.UTF_8.equals(charset))
780 Utf8StringBuffer buffer=null;
782 for (int i=0;i<length;i++)
784 char c = encoded.charAt(offset+i);
789 buffer=new Utf8StringBuffer(length);
790 buffer.getStringBuffer().append(encoded,offset,offset+i+1);
793 buffer.getStringBuffer().append(c);
799 buffer=new Utf8StringBuffer(length);
800 buffer.getStringBuffer().append(encoded,offset,offset+i);
803 buffer.getStringBuffer().append(' ');
809 buffer=new Utf8StringBuffer(length);
810 buffer.getStringBuffer().append(encoded,offset,offset+i);
817 if ('u'==encoded.charAt(offset+i+1))
823 String unicode = new String(Character.toChars(TypeUtil.parseInt(encoded,o,4,16)));
824 buffer.getStringBuffer().append(unicode);
829 buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT);
836 byte b=(byte)TypeUtil.parseInt(encoded,o,2,16);
840 catch(NotUtf8Exception e)
842 LOG.warn(e.toString());
845 catch(NumberFormatException e)
847 LOG.warn(e.toString());
849 buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT);
854 buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT);
858 else if (buffer!=null)
859 buffer.getStringBuffer().append(c);
864 if (offset==0 && encoded.length()==length)
866 return encoded.substring(offset,offset+length);
869 return buffer.toReplacedString();
873 StringBuffer buffer=null;
875 for (int i=0;i<length;i++)
877 char c = encoded.charAt(offset+i);
882 buffer=new StringBuffer(length);
883 buffer.append(encoded,offset,offset+i+1);
892 buffer=new StringBuffer(length);
893 buffer.append(encoded,offset,offset+i);
902 buffer=new StringBuffer(length);
903 buffer.append(encoded,offset,offset+i);
906 byte[] ba=new byte[length];
908 while(c>=0 && c<=0xff)
916 if ('u'==encoded.charAt(offset+i+1))
922 String unicode = new String(Character.toChars(TypeUtil.parseInt(encoded,o,4,16)));
923 byte[] reencoded = unicode.getBytes(charset);
924 System.arraycopy(reencoded,0,ba,n,reencoded.length);
937 ba[n]=(byte)TypeUtil.parseInt(encoded,o,2,16);
943 LOG.warn(e.toString());
967 c = encoded.charAt(offset+i);
971 buffer.append(new String(ba,0,n,charset));
974 else if (buffer!=null)
980 if (offset==0 && encoded.length()==length)
982 return encoded.substring(offset,offset+length);
985 return buffer.toString();
990 /* ------------------------------------------------------------ */
991 /** Perform URL encoding.
993 * @return encoded string.
995 public static String encodeString(String string)
997 return encodeString(string,ENCODING);
1000 /* ------------------------------------------------------------ */
1001 /** Perform URL encoding.
1003 * @return encoded string.
1005 public static String encodeString(String string,Charset charset)
1010 bytes=string.getBytes(charset);
1012 int len=bytes.length;
1013 byte[] encoded= new byte[bytes.length*3];
1015 boolean noEncode=true;
1017 for (int i=0;i<len;i++)
1024 encoded[n++]=(byte)'+';
1026 else if (b>='a' && b<='z' ||
1035 encoded[n++]=(byte)'%';
1036 byte nibble= (byte) ((b&0xf0)>>4);
1038 encoded[n++]=(byte)('A'+nibble-10);
1040 encoded[n++]=(byte)('0'+nibble);
1041 nibble= (byte) (b&0xf);
1043 encoded[n++]=(byte)('A'+nibble-10);
1045 encoded[n++]=(byte)('0'+nibble);
1052 return new String(encoded,0,n,charset);
1056 /* ------------------------------------------------------------ */
1060 public Object clone()
1062 return new UrlEncoded(this);