--- /dev/null
+//
+// ========================================================================
+// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package org.eclipse.jetty.util;
+
+import static org.eclipse.jetty.util.TypeUtil.convertHexDigit;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringWriter;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+import org.eclipse.jetty.util.Utf8Appendable.NotUtf8Exception;
+import org.eclipse.jetty.util.log.Log;
+import org.eclipse.jetty.util.log.Logger;
+
+/* ------------------------------------------------------------ */
+/** Handles coding of MIME "x-www-form-urlencoded".
+ * <p>
+ * This class handles the encoding and decoding for either
+ * the query string of a URL or the _content of a POST HTTP request.
+ *
+ * <h4>Notes</h4>
+ * The UTF-8 charset is assumed, unless otherwise defined by either
+ * passing a parameter or setting the "org.eclipse.jetty.util.UrlEncoding.charset"
+ * System property.
+ * <p>
+ * The hashtable either contains String single values, vectors
+ * of String or arrays of Strings.
+ * <p>
+ * This class is only partially synchronised. In particular, simple
+ * get operations are not protected from concurrent updates.
+ *
+ * @see java.net.URLEncoder
+ */
+@SuppressWarnings("serial")
+public class UrlEncoded extends MultiMap<String> implements Cloneable
+{
+ static final Logger LOG = Log.getLogger(UrlEncoded.class);
+
+ public static final Charset ENCODING;
+ static
+ {
+ Charset encoding;
+ try
+ {
+ String charset = System.getProperty("org.eclipse.jetty.util.UrlEncoding.charset");
+ encoding = charset == null ? StandardCharsets.UTF_8 : Charset.forName(charset);
+ }
+ catch(Exception e)
+ {
+ LOG.warn(e);
+ encoding=StandardCharsets.UTF_8;
+ }
+ ENCODING=encoding;
+ }
+
+ /* ----------------------------------------------------------------- */
+ public UrlEncoded(UrlEncoded url)
+ {
+ super(url);
+ }
+
+ /* ----------------------------------------------------------------- */
+ public UrlEncoded()
+ {
+ }
+
+ public UrlEncoded(String query)
+ {
+ decodeTo(query,this,ENCODING,-1);
+ }
+
+ /* ----------------------------------------------------------------- */
+ public void decode(String query)
+ {
+ decodeTo(query,this,ENCODING,-1);
+ }
+
+ /* ----------------------------------------------------------------- */
+ public void decode(String query,Charset charset)
+ {
+ decodeTo(query,this,charset,-1);
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Encode Hashtable with % encoding.
+ */
+ public String encode()
+ {
+ return encode(ENCODING,false);
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Encode Hashtable with % encoding.
+ */
+ public String encode(Charset charset)
+ {
+ return encode(charset,false);
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Encode Hashtable with % encoding.
+ * @param equalsForNullValue if True, then an '=' is always used, even
+ * for parameters without a value. e.g. "blah?a=&b=&c=".
+ */
+ public synchronized String encode(Charset charset, boolean equalsForNullValue)
+ {
+ return encode(this,charset,equalsForNullValue);
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Encode Hashtable with % encoding.
+ * @param equalsForNullValue if True, then an '=' is always used, even
+ * for parameters without a value. e.g. "blah?a=&b=&c=".
+ */
+ public static String encode(MultiMap<String> map, Charset charset, boolean equalsForNullValue)
+ {
+ if (charset==null)
+ charset=ENCODING;
+
+ StringBuilder result = new StringBuilder(128);
+
+ boolean delim = false;
+ for(Map.Entry<String, List<String>> entry: map.entrySet())
+ {
+ String key = entry.getKey().toString();
+ List<String> list = entry.getValue();
+ int s=list.size();
+
+ if (delim)
+ {
+ result.append('&');
+ }
+
+ if (s==0)
+ {
+ result.append(encodeString(key,charset));
+ if(equalsForNullValue)
+ result.append('=');
+ }
+ else
+ {
+ for (int i=0;i<s;i++)
+ {
+ if (i>0)
+ result.append('&');
+ String val=list.get(i);
+ result.append(encodeString(key,charset));
+
+ if (val!=null)
+ {
+ String str=val.toString();
+ if (str.length()>0)
+ {
+ result.append('=');
+ result.append(encodeString(str,charset));
+ }
+ else if (equalsForNullValue)
+ result.append('=');
+ }
+ else if (equalsForNullValue)
+ result.append('=');
+ }
+ }
+ delim = true;
+ }
+ return result.toString();
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Decoded parameters to Map.
+ * @param content the string containing the encoded parameters
+ */
+ public static void decodeTo(String content, MultiMap<String> map, String charset, int maxKeys)
+ {
+ decodeTo(content,map,charset==null?null:Charset.forName(charset),maxKeys);
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Decoded parameters to Map.
+ * @param content the string containing the encoded parameters
+ */
+ public static void decodeTo(String content, MultiMap<String> map, Charset charset, int maxKeys)
+ {
+ if (charset==null)
+ charset=ENCODING;
+
+ synchronized(map)
+ {
+ String key = null;
+ String value = null;
+ int mark=-1;
+ boolean encoded=false;
+ for (int i=0;i<content.length();i++)
+ {
+ char c = content.charAt(i);
+ switch (c)
+ {
+ case '&':
+ int l=i-mark-1;
+ value = l==0?"":
+ (encoded?decodeString(content,mark+1,l,charset):content.substring(mark+1,i));
+ mark=i;
+ encoded=false;
+ if (key != null)
+ {
+ map.add(key,value);
+ }
+ else if (value!=null&&value.length()>0)
+ {
+ map.add(value,"");
+ }
+ key = null;
+ value=null;
+ if (maxKeys>0 && map.size()>maxKeys)
+ throw new IllegalStateException("Form too many keys");
+ break;
+ case '=':
+ if (key!=null)
+ break;
+ key = encoded?decodeString(content,mark+1,i-mark-1,charset):content.substring(mark+1,i);
+ mark=i;
+ encoded=false;
+ break;
+ case '+':
+ encoded=true;
+ break;
+ case '%':
+ encoded=true;
+ break;
+ }
+ }
+
+ if (key != null)
+ {
+ int l=content.length()-mark-1;
+ value = l==0?"":(encoded?decodeString(content,mark+1,l,charset):content.substring(mark+1));
+ map.add(key,value);
+ }
+ else if (mark<content.length())
+ {
+ key = encoded
+ ?decodeString(content,mark+1,content.length()-mark-1,charset)
+ :content.substring(mark+1);
+ if (key != null && key.length() > 0)
+ {
+ map.add(key,"");
+ }
+ }
+ }
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Decoded parameters to Map.
+ * @param raw the byte[] containing the encoded parameters
+ * @param offset the offset within raw to decode from
+ * @param length the length of the section to decode
+ * @param map the {@link MultiMap} to populate
+ */
+ public static void decodeUtf8To(byte[] raw,int offset, int length, MultiMap<String> map)
+ {
+ Utf8StringBuilder buffer = new Utf8StringBuilder();
+ synchronized(map)
+ {
+ String key = null;
+ String value = null;
+
+ int end=offset+length;
+ for (int i=offset;i<end;i++)
+ {
+ byte b=raw[i];
+ try
+ {
+ switch ((char)(0xff&b))
+ {
+ case '&':
+ value = buffer.toReplacedString();
+ buffer.reset();
+ if (key != null)
+ {
+ map.add(key,value);
+ }
+ else if (value!=null&&value.length()>0)
+ {
+ map.add(value,"");
+ }
+ key = null;
+ value=null;
+ break;
+
+ case '=':
+ if (key!=null)
+ {
+ buffer.append(b);
+ break;
+ }
+ key = buffer.toReplacedString();
+ buffer.reset();
+ break;
+
+ case '+':
+ buffer.append((byte)' ');
+ break;
+
+ case '%':
+ if (i+2<end)
+ {
+ if ('u'==raw[i+1])
+ {
+ i++;
+ if (i+4<end)
+ {
+ byte top=raw[++i];
+ byte hi=raw[++i];
+ byte lo=raw[++i];
+ byte bot=raw[++i];
+ buffer.getStringBuilder().append(Character.toChars((convertHexDigit(top)<<12) +(convertHexDigit(hi)<<8) + (convertHexDigit(lo)<<4) +convertHexDigit(bot)));
+ }
+ else
+ {
+ buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
+ i=end;
+ }
+ }
+ else
+ {
+ byte hi=raw[++i];
+ byte lo=raw[++i];
+ buffer.append((byte)((convertHexDigit(hi)<<4) + convertHexDigit(lo)));
+ }
+ }
+ else
+ {
+ buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
+ i=end;
+ }
+ break;
+
+ default:
+ buffer.append(b);
+ break;
+ }
+ }
+ catch(NotUtf8Exception e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ }
+ catch(NumberFormatException e)
+ {
+ buffer.append(Utf8Appendable.REPLACEMENT_UTF8,0,3);
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ }
+ }
+
+ if (key != null)
+ {
+ value = buffer.toReplacedString();
+ buffer.reset();
+ map.add(key,value);
+ }
+ else if (buffer.length()>0)
+ {
+ map.add(buffer.toReplacedString(),"");
+ }
+ }
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Decoded parameters to Map.
+ * @param in InputSteam to read
+ * @param map MultiMap to add parameters to
+ * @param maxLength maximum number of keys to read or -1 for no limit
+ */
+ public static void decode88591To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
+ throws IOException
+ {
+ synchronized(map)
+ {
+ StringBuffer buffer = new StringBuffer();
+ String key = null;
+ String value = null;
+
+ int b;
+
+ int totalLength=0;
+ while ((b=in.read())>=0)
+ {
+ switch ((char) b)
+ {
+ case '&':
+ value = buffer.length()==0?"":buffer.toString();
+ buffer.setLength(0);
+ if (key != null)
+ {
+ map.add(key,value);
+ }
+ else if (value!=null&&value.length()>0)
+ {
+ map.add(value,"");
+ }
+ key = null;
+ value=null;
+ if (maxKeys>0 && map.size()>maxKeys)
+ throw new IllegalStateException("Form too many keys");
+ break;
+
+ case '=':
+ if (key!=null)
+ {
+ buffer.append((char)b);
+ break;
+ }
+ key = buffer.toString();
+ buffer.setLength(0);
+ break;
+
+ case '+':
+ buffer.append(' ');
+ break;
+
+ case '%':
+ int code0=in.read();
+ if ('u'==code0)
+ {
+ int code1=in.read();
+ if (code1>=0)
+ {
+ int code2=in.read();
+ if (code2>=0)
+ {
+ int code3=in.read();
+ if (code3>=0)
+ buffer.append(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3)));
+ }
+ }
+ }
+ else if (code0>=0)
+ {
+ int code1=in.read();
+ if (code1>=0)
+ buffer.append((char)((convertHexDigit(code0)<<4)+convertHexDigit(code1)));
+ }
+ break;
+
+ default:
+ buffer.append((char)b);
+ break;
+ }
+ if (maxLength>=0 && (++totalLength > maxLength))
+ throw new IllegalStateException("Form too large");
+ }
+
+ if (key != null)
+ {
+ value = buffer.length()==0?"":buffer.toString();
+ buffer.setLength(0);
+ map.add(key,value);
+ }
+ else if (buffer.length()>0)
+ {
+ map.add(buffer.toString(), "");
+ }
+ }
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Decoded parameters to Map.
+ * @param in InputSteam to read
+ * @param map MultiMap to add parameters to
+ * @param maxLength maximum number of keys to read or -1 for no limit
+ */
+ public static void decodeUtf8To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys)
+ throws IOException
+ {
+ synchronized(map)
+ {
+ Utf8StringBuilder buffer = new Utf8StringBuilder();
+ String key = null;
+ String value = null;
+
+ int b;
+
+ int totalLength=0;
+ while ((b=in.read())>=0)
+ {
+ try
+ {
+ switch ((char) b)
+ {
+ case '&':
+ value = buffer.toReplacedString();
+ buffer.reset();
+ if (key != null)
+ {
+ map.add(key,value);
+ }
+ else if (value!=null&&value.length()>0)
+ {
+ map.add(value,"");
+ }
+ key = null;
+ value=null;
+ if (maxKeys>0 && map.size()>maxKeys)
+ throw new IllegalStateException("Form too many keys");
+ break;
+
+ case '=':
+ if (key!=null)
+ {
+ buffer.append((byte)b);
+ break;
+ }
+ key = buffer.toReplacedString();
+ buffer.reset();
+ break;
+
+ case '+':
+ buffer.append((byte)' ');
+ break;
+
+ case '%':
+ int code0=in.read();
+ boolean decoded=false;
+ if ('u'==code0)
+ {
+ code0=in.read(); // XXX: we have to read the next byte, otherwise code0 is always 'u'
+ if (code0>=0)
+ {
+ int code1=in.read();
+ if (code1>=0)
+ {
+ int code2=in.read();
+ if (code2>=0)
+ {
+ int code3=in.read();
+ if (code3>=0)
+ {
+ buffer.getStringBuilder().append(Character.toChars
+ ((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3)));
+ decoded=true;
+ }
+ }
+ }
+ }
+ }
+ else if (code0>=0)
+ {
+ int code1=in.read();
+ if (code1>=0)
+ {
+ buffer.append((byte)((convertHexDigit(code0)<<4)+convertHexDigit(code1)));
+ decoded=true;
+ }
+ }
+
+ if (!decoded)
+ buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT);
+
+ break;
+
+ default:
+ buffer.append((byte)b);
+ break;
+ }
+ }
+ catch(NotUtf8Exception e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ }
+ catch(NumberFormatException e)
+ {
+ buffer.append(Utf8Appendable.REPLACEMENT_UTF8,0,3);
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ }
+ if (maxLength>=0 && (++totalLength > maxLength))
+ throw new IllegalStateException("Form too large");
+ }
+
+ if (key != null)
+ {
+ value = buffer.toReplacedString();
+ buffer.reset();
+ map.add(key,value);
+ }
+ else if (buffer.length()>0)
+ {
+ map.add(buffer.toReplacedString(), "");
+ }
+ }
+ }
+
+ /* -------------------------------------------------------------- */
+ public static void decodeUtf16To(InputStream in, MultiMap<String> map, int maxLength, int maxKeys) throws IOException
+ {
+ InputStreamReader input = new InputStreamReader(in,StandardCharsets.UTF_16);
+ StringWriter buf = new StringWriter(8192);
+ IO.copy(input,buf,maxLength);
+
+ decodeTo(buf.getBuffer().toString(),map,StandardCharsets.UTF_16,maxKeys);
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Decoded parameters to Map.
+ * @param in the stream containing the encoded parameters
+ */
+ public static void decodeTo(InputStream in, MultiMap<String> map, String charset, int maxLength, int maxKeys)
+ throws IOException
+ {
+ if (charset==null)
+ {
+ if (ENCODING.equals(StandardCharsets.UTF_8))
+ decodeUtf8To(in,map,maxLength,maxKeys);
+ else
+ decodeTo(in,map,ENCODING,maxLength,maxKeys);
+ }
+ else if (StringUtil.__UTF8.equalsIgnoreCase(charset))
+ decodeUtf8To(in,map,maxLength,maxKeys);
+ else if (StringUtil.__ISO_8859_1.equalsIgnoreCase(charset))
+ decode88591To(in,map,maxLength,maxKeys);
+ else if (StringUtil.__UTF16.equalsIgnoreCase(charset))
+ decodeUtf16To(in,map,maxLength,maxKeys);
+ else
+ decodeTo(in,map,Charset.forName(charset),maxLength,maxKeys);
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Decoded parameters to Map.
+ * @param in the stream containing the encoded parameters
+ */
+ public static void decodeTo(InputStream in, MultiMap<String> map, Charset charset, int maxLength, int maxKeys)
+ throws IOException
+ {
+ //no charset present, use the configured default
+ if (charset==null)
+ charset=ENCODING;
+
+ if (StandardCharsets.UTF_8.equals(charset))
+ {
+ decodeUtf8To(in,map,maxLength,maxKeys);
+ return;
+ }
+
+ if (StandardCharsets.ISO_8859_1.equals(charset))
+ {
+ decode88591To(in,map,maxLength,maxKeys);
+ return;
+ }
+
+ if (StandardCharsets.UTF_16.equals(charset)) // Should be all 2 byte encodings
+ {
+ decodeUtf16To(in,map,maxLength,maxKeys);
+ return;
+ }
+
+ synchronized(map)
+ {
+ String key = null;
+ String value = null;
+
+ int c;
+
+ int totalLength = 0;
+ ByteArrayOutputStream2 output = new ByteArrayOutputStream2();
+
+ int size=0;
+
+ while ((c=in.read())>0)
+ {
+ switch ((char) c)
+ {
+ case '&':
+ size=output.size();
+ value = size==0?"":output.toString(charset);
+ output.setCount(0);
+ if (key != null)
+ {
+ map.add(key,value);
+ }
+ else if (value!=null&&value.length()>0)
+ {
+ map.add(value,"");
+ }
+ key = null;
+ value=null;
+ if (maxKeys>0 && map.size()>maxKeys)
+ throw new IllegalStateException("Form too many keys");
+ break;
+ case '=':
+ if (key!=null)
+ {
+ output.write(c);
+ break;
+ }
+ size=output.size();
+ key = size==0?"":output.toString(charset);
+ output.setCount(0);
+ break;
+ case '+':
+ output.write(' ');
+ break;
+ case '%':
+ int code0=in.read();
+ if ('u'==code0)
+ {
+ int code1=in.read();
+ if (code1>=0)
+ {
+ int code2=in.read();
+ if (code2>=0)
+ {
+ int code3=in.read();
+ if (code3>=0)
+ output.write(new String(Character.toChars((convertHexDigit(code0)<<12)+(convertHexDigit(code1)<<8)+(convertHexDigit(code2)<<4)+convertHexDigit(code3))).getBytes(charset));
+ }
+ }
+
+ }
+ else if (code0>=0)
+ {
+ int code1=in.read();
+ if (code1>=0)
+ output.write((convertHexDigit(code0)<<4)+convertHexDigit(code1));
+ }
+ break;
+ default:
+ output.write(c);
+ break;
+ }
+
+ totalLength++;
+ if (maxLength>=0 && totalLength > maxLength)
+ throw new IllegalStateException("Form too large");
+ }
+
+ size=output.size();
+ if (key != null)
+ {
+ value = size==0?"":output.toString(charset);
+ output.setCount(0);
+ map.add(key,value);
+ }
+ else if (size>0)
+ map.add(output.toString(charset),"");
+ }
+ }
+
+ /* -------------------------------------------------------------- */
+ /** Decode String with % encoding.
+ * This method makes the assumption that the majority of calls
+ * will need no decoding.
+ */
+ public static String decodeString(String encoded,int offset,int length,Charset charset)
+ {
+ if (charset==null || StandardCharsets.UTF_8.equals(charset))
+ {
+ Utf8StringBuffer buffer=null;
+
+ for (int i=0;i<length;i++)
+ {
+ char c = encoded.charAt(offset+i);
+ if (c<0||c>0xff)
+ {
+ if (buffer==null)
+ {
+ buffer=new Utf8StringBuffer(length);
+ buffer.getStringBuffer().append(encoded,offset,offset+i+1);
+ }
+ else
+ buffer.getStringBuffer().append(c);
+ }
+ else if (c=='+')
+ {
+ if (buffer==null)
+ {
+ buffer=new Utf8StringBuffer(length);
+ buffer.getStringBuffer().append(encoded,offset,offset+i);
+ }
+
+ buffer.getStringBuffer().append(' ');
+ }
+ else if (c=='%')
+ {
+ if (buffer==null)
+ {
+ buffer=new Utf8StringBuffer(length);
+ buffer.getStringBuffer().append(encoded,offset,offset+i);
+ }
+
+ if ((i+2)<length)
+ {
+ try
+ {
+ if ('u'==encoded.charAt(offset+i+1))
+ {
+ if((i+5)<length)
+ {
+ int o=offset+i+2;
+ i+=5;
+ String unicode = new String(Character.toChars(TypeUtil.parseInt(encoded,o,4,16)));
+ buffer.getStringBuffer().append(unicode);
+ }
+ else
+ {
+ i=length;
+ buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT);
+ }
+ }
+ else
+ {
+ int o=offset+i+1;
+ i+=2;
+ byte b=(byte)TypeUtil.parseInt(encoded,o,2,16);
+ buffer.append(b);
+ }
+ }
+ catch(NotUtf8Exception e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ }
+ catch(NumberFormatException e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT);
+ }
+ }
+ else
+ {
+ buffer.getStringBuffer().append(Utf8Appendable.REPLACEMENT);
+ i=length;
+ }
+ }
+ else if (buffer!=null)
+ buffer.getStringBuffer().append(c);
+ }
+
+ if (buffer==null)
+ {
+ if (offset==0 && encoded.length()==length)
+ return encoded;
+ return encoded.substring(offset,offset+length);
+ }
+
+ return buffer.toReplacedString();
+ }
+ else
+ {
+ StringBuffer buffer=null;
+
+ for (int i=0;i<length;i++)
+ {
+ char c = encoded.charAt(offset+i);
+ if (c<0||c>0xff)
+ {
+ if (buffer==null)
+ {
+ buffer=new StringBuffer(length);
+ buffer.append(encoded,offset,offset+i+1);
+ }
+ else
+ buffer.append(c);
+ }
+ else if (c=='+')
+ {
+ if (buffer==null)
+ {
+ buffer=new StringBuffer(length);
+ buffer.append(encoded,offset,offset+i);
+ }
+
+ buffer.append(' ');
+ }
+ else if (c=='%')
+ {
+ if (buffer==null)
+ {
+ buffer=new StringBuffer(length);
+ buffer.append(encoded,offset,offset+i);
+ }
+
+ byte[] ba=new byte[length];
+ int n=0;
+ while(c>=0 && c<=0xff)
+ {
+ if (c=='%')
+ {
+ if(i+2<length)
+ {
+ try
+ {
+ if ('u'==encoded.charAt(offset+i+1))
+ {
+ if (i+6<length)
+ {
+ int o=offset+i+2;
+ i+=6;
+ String unicode = new String(Character.toChars(TypeUtil.parseInt(encoded,o,4,16)));
+ byte[] reencoded = unicode.getBytes(charset);
+ System.arraycopy(reencoded,0,ba,n,reencoded.length);
+ n+=reencoded.length;
+ }
+ else
+ {
+ ba[n++] = (byte)'?';
+ i=length;
+ }
+ }
+ else
+ {
+ int o=offset+i+1;
+ i+=3;
+ ba[n]=(byte)TypeUtil.parseInt(encoded,o,2,16);
+ n++;
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.warn(e.toString());
+ LOG.debug(e);
+ ba[n++] = (byte)'?';
+ }
+ }
+ else
+ {
+ ba[n++] = (byte)'?';
+ i=length;
+ }
+ }
+ else if (c=='+')
+ {
+ ba[n++]=(byte)' ';
+ i++;
+ }
+ else
+ {
+ ba[n++]=(byte)c;
+ i++;
+ }
+
+ if (i>=length)
+ break;
+ c = encoded.charAt(offset+i);
+ }
+
+ i--;
+ buffer.append(new String(ba,0,n,charset));
+
+ }
+ else if (buffer!=null)
+ buffer.append(c);
+ }
+
+ if (buffer==null)
+ {
+ if (offset==0 && encoded.length()==length)
+ return encoded;
+ return encoded.substring(offset,offset+length);
+ }
+
+ return buffer.toString();
+ }
+
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Perform URL encoding.
+ * @param string
+ * @return encoded string.
+ */
+ public static String encodeString(String string)
+ {
+ return encodeString(string,ENCODING);
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Perform URL encoding.
+ * @param string
+ * @return encoded string.
+ */
+ public static String encodeString(String string,Charset charset)
+ {
+ if (charset==null)
+ charset=ENCODING;
+ byte[] bytes=null;
+ bytes=string.getBytes(charset);
+
+ int len=bytes.length;
+ byte[] encoded= new byte[bytes.length*3];
+ int n=0;
+ boolean noEncode=true;
+
+ for (int i=0;i<len;i++)
+ {
+ byte b = bytes[i];
+
+ if (b==' ')
+ {
+ noEncode=false;
+ encoded[n++]=(byte)'+';
+ }
+ else if (b>='a' && b<='z' ||
+ b>='A' && b<='Z' ||
+ b>='0' && b<='9')
+ {
+ encoded[n++]=b;
+ }
+ else
+ {
+ noEncode=false;
+ encoded[n++]=(byte)'%';
+ byte nibble= (byte) ((b&0xf0)>>4);
+ if (nibble>=10)
+ encoded[n++]=(byte)('A'+nibble-10);
+ else
+ encoded[n++]=(byte)('0'+nibble);
+ nibble= (byte) (b&0xf);
+ if (nibble>=10)
+ encoded[n++]=(byte)('A'+nibble-10);
+ else
+ encoded[n++]=(byte)('0'+nibble);
+ }
+ }
+
+ if (noEncode)
+ return string;
+
+ return new String(encoded,0,n,charset);
+ }
+
+
+ /* ------------------------------------------------------------ */
+ /**
+ */
+ @Override
+ public Object clone()
+ {
+ return new UrlEncoded(this);
+ }
+}