//
//  ========================================================================
//  Copyright (c) 1995-2019 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 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.log.Log;
import org.eclipse.jetty.util.log.Logger;

import static org.eclipse.jetty.util.TypeUtil.convertHexDigit;

Handles coding of MIME "x-www-form-urlencoded".

This class handles the encoding and decoding for either the query string of a URL or the _content of a POST HTTP request.

Notes

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.

The hashtable either contains String single values, vectors of String or arrays of Strings.

This class is only partially synchronised. In particular, simple get operations are not protected from concurrent updates.

See Also:
  • URLEncoder
/** * 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. * </p> * <b>Notes</b> * <p> * 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> * <p> * The hashtable either contains String single values, vectors * of String or arrays of Strings. * </p> * <p> * This class is only partially synchronised. In particular, simple * get operations are not protected from concurrent updates. * </p> * * @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); } public void decode(String query) { decodeTo(query, this, ENCODING); } public void decode(String query, Charset charset) { decodeTo(query, this, charset); }
Encode MultiMap with % encoding for UTF8 sequences.
Returns:the MultiMap as a string with % encoding
/** * Encode MultiMap with % encoding for UTF8 sequences. * * @return the MultiMap as a string with % encoding */
public String encode() { return encode(ENCODING, false); }
Encode MultiMap with % encoding for arbitrary Charset sequences.
Params:
  • charset – the charset to use for encoding
Returns:the MultiMap as a string encoded with % encodings
/** * Encode MultiMap with % encoding for arbitrary Charset sequences. * * @param charset the charset to use for encoding * @return the MultiMap as a string encoded with % encodings */
public String encode(Charset charset) { return encode(charset, false); }
Encode MultiMap with % encoding.
Params:
  • charset – the charset to encode with
  • equalsForNullValue – if True, then an '=' is always used, even for parameters without a value. e.g. "blah?a=&b=&c=".
Returns:the MultiMap as a string encoded with % encodings
/** * Encode MultiMap with % encoding. * * @param charset the charset to encode with * @param equalsForNullValue if True, then an '=' is always used, even * for parameters without a value. e.g. <code>"blah?a=&amp;b=&amp;c="</code>. * @return the MultiMap as a string encoded with % encodings */
public synchronized String encode(Charset charset, boolean equalsForNullValue) { return encode(this, charset, equalsForNullValue); }
Encode MultiMap with % encoding.
Params:
  • map – the map to encode
  • charset – the charset to use for encoding (uses default encoding if null)
  • equalsForNullValue – if True, then an '=' is always used, even for parameters without a value. e.g. "blah?a=&b=&c=".
Returns:the MultiMap as a string encoded with % encodings.
/** * Encode MultiMap with % encoding. * * @param map the map to encode * @param charset the charset to use for encoding (uses default encoding if null) * @param equalsForNullValue if True, then an '=' is always used, even * for parameters without a value. e.g. <code>"blah?a=&amp;b=&amp;c="</code>. * @return the MultiMap as a string encoded with % encodings. */
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.
Params:
  • content – the string containing the encoded parameters
  • map – the MultiMap to put parsed query parameters into
  • charset – the charset to use for decoding
/** * Decoded parameters to Map. * * @param content the string containing the encoded parameters * @param map the MultiMap to put parsed query parameters into * @param charset the charset to use for decoding */
public static void decodeTo(String content, MultiMap<String> map, String charset) { decodeTo(content, map, charset == null ? null : Charset.forName(charset)); }
Decoded parameters to Map.
Params:
  • content – the string containing the encoded parameters
  • map – the MultiMap to put parsed query parameters into
  • charset – the charset to use for decoding
/** * Decoded parameters to Map. * * @param content the string containing the encoded parameters * @param map the MultiMap to put parsed query parameters into * @param charset the charset to use for decoding */
public static void decodeTo(String content, MultiMap<String> map, Charset charset) { if (charset == null) charset = ENCODING; if (StandardCharsets.UTF_8.equals(charset)) { decodeUtf8To(content, 0, content.length(), map); return; } 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; 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; default: 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.
Params:
  • in – the stream containing the encoded parameters
  • map – the MultiMap to decode into
  • charset – the charset to use for decoding
  • maxLength – the maximum length of the form to decode
  • maxKeys – the maximum number of keys to decode
Throws:
/** * Decoded parameters to Map. * * @param in the stream containing the encoded parameters * @param map the MultiMap to decode into * @param charset the charset to use for decoding * @param maxLength the maximum length of the form to decode * @param maxKeys the maximum number of keys to decode * @throws IOException if unable to decode input stream */
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.
Params:
  • in – the stream containing the encoded parameters
  • map – the MultiMap to decode into
  • charset – the charset to use for decoding
  • maxLength – the maximum length of the form to decode
  • maxKeys – the maximum number of keys to decode
Throws:
/** * Decoded parameters to Map. * * @param in the stream containing the encoded parameters * @param map the MultiMap to decode into * @param charset the charset to use for decoding * @param maxLength the maximum length of the form to decode * @param maxKeys the maximum number of keys to decode * @throws IOException if unable to decode input stream */
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; try (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(String.format("Form with too many keys [%d > %d]", map.size(), maxKeys)); 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(); int code1 = in.read(); output.write(decodeHexChar(code0, code1)); break; default: output.write(c); break; } totalLength++; if (maxLength >= 0 && totalLength > maxLength) throw new IllegalStateException("Form is 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), ""); } } } public static void decodeUtf8To(String query, MultiMap<String> map) { decodeUtf8To(query, 0, query.length(), map); }
Decoded parameters to Map.
Params:
  • query – the string containing the encoded parameters
  • offset – the offset within raw to decode from
  • length – the length of the section to decode
  • map – the MultiMap to populate
/** * Decoded parameters to Map. * * @param query the string 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(String query, 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++) { char c = query.charAt(i); switch (c) { 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(c); break; } key = buffer.toReplacedString(); buffer.reset(); break; case '+': buffer.append((byte)' '); break; case '%': if (i + 2 < end) { char hi = query.charAt(++i); char lo = query.charAt(++i); buffer.append(decodeHexByte(hi, lo)); } else { throw new Utf8Appendable.NotUtf8Exception("Incomplete % encoding"); } break; default: buffer.append(c); break; } } 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.
Params:
  • in – InputSteam to read
  • map – MultiMap to add parameters to
  • maxLength – maximum form length to decode
  • maxKeys – the maximum number of keys to read or -1 for no limit
Throws:
/** * Decoded parameters to Map. * * @param in InputSteam to read * @param map MultiMap to add parameters to * @param maxLength maximum form length to decode * @param maxKeys the maximum number of keys to read or -1 for no limit * @throws IOException if unable to decode input stream */
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) { 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(String.format("Form with too many keys [%d > %d]", map.size(), maxKeys)); break; case '=': if (key != null) { buffer.append((byte)b); break; } key = buffer.toReplacedString(); buffer.reset(); break; case '+': buffer.append((byte)' '); break; case '%': char code0 = (char)in.read(); char code1 = (char)in.read(); buffer.append(decodeHexByte(code0, code1)); break; default: buffer.append((byte)b); break; } if (maxLength >= 0 && (++totalLength > maxLength)) throw new IllegalStateException("Form is 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); // TODO implement maxKeys decodeTo(buf.getBuffer().toString(), map, StandardCharsets.UTF_16); }
Decoded parameters to MultiMap, using ISO8859-1 encodings.
Params:
  • in – InputSteam to read
  • map – MultiMap to add parameters to
  • maxLength – maximum length of form to read
  • maxKeys – maximum number of keys to read or -1 for no limit
Throws:
  • IOException – if unable to decode inputstream as ISO8859-1
/** * Decoded parameters to MultiMap, using ISO8859-1 encodings. * * @param in InputSteam to read * @param map MultiMap to add parameters to * @param maxLength maximum length of form to read * @param maxKeys maximum number of keys to read or -1 for no limit * @throws IOException if unable to decode inputstream as ISO8859-1 */
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(String.format("Form with too many keys [%d > %d]", map.size(), maxKeys)); 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(); int code1 = in.read(); buffer.append(decodeHexChar(code0, code1)); break; default: buffer.append((char)b); break; } if (maxLength >= 0 && (++totalLength > maxLength)) throw new IllegalStateException(String.format("Form with too many keys [%d > %d]", map.size(), maxKeys)); } 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(), ""); } } }
Decode String with % encoding. This method makes the assumption that the majority of calls will need no decoding.
Params:
  • encoded – the encoded string to decode
Returns:the decoded string
/** * Decode String with % encoding. * This method makes the assumption that the majority of calls * will need no decoding. * * @param encoded the encoded string to decode * @return the decoded string */
public static String decodeString(String encoded) { return decodeString(encoded, 0, encoded.length(), ENCODING); }
Decode String with % encoding. This method makes the assumption that the majority of calls will need no decoding.
Params:
  • encoded – the encoded string to decode
  • offset – the offset in the encoded string to decode from
  • length – the length of characters in the encoded string to decode
  • charset – the charset to use for decoding
Returns:the decoded string
/** * Decode String with % encoding. * This method makes the assumption that the majority of calls * will need no decoding. * * @param encoded the encoded string to decode * @param offset the offset in the encoded string to decode from * @param length the length of characters in the encoded string to decode * @param charset the charset to use for decoding * @return the decoded string */
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) { int o = offset + i + 1; i += 2; byte b = (byte)TypeUtil.parseInt(encoded, o, 2, 16); buffer.append(b); } 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) { int o = offset + i + 1; i += 3; ba[n] = (byte)TypeUtil.parseInt(encoded, o, 2, 16); n++; } 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(); } } private static char decodeHexChar(int hi, int lo) { try { return (char)((convertHexDigit(hi) << 4) + convertHexDigit(lo)); } catch (NumberFormatException e) { throw new IllegalArgumentException("Not valid encoding '%" + (char)hi + (char)lo + "'"); } } private static byte decodeHexByte(char hi, char lo) { try { return (byte)((convertHexDigit(hi) << 4) + convertHexDigit(lo)); } catch (NumberFormatException e) { throw new IllegalArgumentException("Not valid encoding '%" + hi + lo + "'"); } }
Perform URL encoding.
Params:
  • string – the string to encode
Returns:encoded string.
/** * Perform URL encoding. * * @param string the string to encode * @return encoded string. */
public static String encodeString(String string) { return encodeString(string, ENCODING); }
Perform URL encoding.
Params:
  • string – the string to encode
  • charset – the charset to use for encoding
Returns:encoded string.
/** * Perform URL encoding. * * @param string the string to encode * @param charset the charset to use for encoding * @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); } }