All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.firefly.codec.http2.encode.UrlEncoded Maven / Gradle / Ivy

There is a newer version: 4.0.3.2
Show newest version
package com.firefly.codec.http2.encode;

import static com.firefly.utils.lang.TypeUtils.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 com.firefly.utils.collection.MultiMap;
import com.firefly.utils.io.ByteArrayOutputStream2;
import com.firefly.utils.io.IO;
import com.firefly.utils.lang.TypeUtils;
import com.firefly.utils.lang.Utf8Appendable;
import com.firefly.utils.lang.Utf8Appendable.NotUtf8Exception;
import com.firefly.utils.lang.Utf8StringBuffer;
import com.firefly.utils.lang.Utf8StringBuilder;
import com.firefly.utils.log.Log;
import com.firefly.utils.log.LogFactory;

/**
 * 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.firefly.utils.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 java.net.URLEncoder */ @SuppressWarnings("serial") public class UrlEncoded extends MultiMapimplements Cloneable { private static Log LOG = LogFactory.getInstance().getLog("firefly-system"); public static final Charset ENCODING; static { Charset encoding; try { String charset = System.getProperty("org.firefly.utils.UrlEncoding.charset"); encoding = charset == null ? StandardCharsets.UTF_8 : Charset.forName(charset); } catch (Exception 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. * * @return the MultiMap as a string with % encoding */ public String encode() { return encode(ENCODING, false); } /** * 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. * * @param charset * the charset to encode with * @param equalsForNullValue * if True, then an '=' is always used, even for parameters * without a value. e.g. "blah?a=&b=&c=". * @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. * * @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. "blah?a=&b=&c=". * @return the MultiMap as a string encoded with % encodings. */ public static String encode(MultiMap map, Charset charset, boolean equalsForNullValue) { if (charset == null) charset = ENCODING; StringBuilder result = new StringBuilder(128); boolean delim = false; for (Map.Entry> entry : map.entrySet()) { String key = entry.getKey().toString(); List 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 * @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 map, String charset) { decodeTo(content, map, charset == null ? null : Charset.forName(charset)); } /** * 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 map, Charset charset) { if (charset == null) charset = ENCODING; if (charset == StandardCharsets.UTF_8) { 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; } } 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, ""); } } } } public static void decodeUtf8To(String query, MultiMap map) { decodeUtf8To(query, 0, query.length(), map); } /** * 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 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); try { 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) { if ('u' == query.charAt(i + 1)) { i++; if (i + 4 < end) { char top = query.charAt(++i); char hi = query.charAt(++i); char lo = query.charAt(++i); char bot = query.charAt(++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 { char hi = query.charAt(++i); char lo = query.charAt(++i); buffer.append((byte) ((convertHexDigit(hi) << 4) + convertHexDigit(lo))); } } else { buffer.getStringBuilder().append(Utf8Appendable.REPLACEMENT); i = end; } break; default: buffer.append(c); break; } } catch (NotUtf8Exception e) { LOG.warn(e.toString()); } catch (NumberFormatException e) { buffer.append(Utf8Appendable.REPLACEMENT_UTF8, 0, 3); LOG.warn(e.toString()); } } if (key != null) { value = buffer.toReplacedString(); buffer.reset(); map.add(key, value); } else if (buffer.length() > 0) { map.add(buffer.toReplacedString(), ""); } } } /** * 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 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 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 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()); } catch (NumberFormatException e) { buffer.append(Utf8Appendable.REPLACEMENT_UTF8, 0, 3); LOG.warn(e.toString()); } 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 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 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 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 ("utf-8".equalsIgnoreCase(charset)) decodeUtf8To(in, map, maxLength, maxKeys); else if ("iso-8859-1".equalsIgnoreCase(charset)) decode88591To(in, map, maxLength, maxKeys); else if ("utf-16".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 * @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 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("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. * * @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. * * @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) { 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(TypeUtils.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) TypeUtils.parseInt(encoded, o, 2, 16); buffer.append(b); } } catch (NotUtf8Exception e) { LOG.warn(e.toString()); } catch (NumberFormatException e) { LOG.warn(e.toString()); 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(TypeUtils.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) TypeUtils.parseInt(encoded, o, 2, 16); n++; } } catch (Exception e) { LOG.warn(e.toString()); 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 * the string to encode * @return encoded string. */ public static String encodeString(String string) { return encodeString(string, ENCODING); } /** * 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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy