org.testifyproject.glassfish.jersey.uri.UriComponent Maven / Gradle / Ivy
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2010-2017 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in org.testifyproject.testifyprojectpliance with the License. You can
* obtain a copy of the License at
* https://oss.oracle.org.testifyproject.testifyproject/licenses/CDDL+GPL-1.1
* or LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package org.testifyproject.glassfish.org.testifyproject.uri;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.PathSegment;
import org.testifyproject.glassfish.org.testifyproject.internal.LocalizationMessages;
import org.testifyproject.glassfish.org.testifyproject.internal.util.collection.MultivaluedStringMap;
/**
* Utility class for validating, encoding and decoding org.testifyproject.testifyprojectponents
* of a URI.
*
* @author Paul Sandoz
* @author Marek Potociar (marek.potociar at oracle.org.testifyproject.testifyproject)
*/
public class UriComponent {
// TODO rewrite to use masks and not lookup tables
/**
* The URI org.testifyproject.testifyprojectponent type.
*/
public enum Type {
/**
* ALPHA / DIGIT / "-" / "." / "_" / "~" characters.
*/
UNRESERVED,
/**
* The URI scheme org.testifyproject.testifyprojectponent type.
*/
SCHEME,
/**
* The URI authority org.testifyproject.testifyprojectponent type.
*/
AUTHORITY,
/**
* The URI user info org.testifyproject.testifyprojectponent type.
*/
USER_INFO,
/**
* The URI host org.testifyproject.testifyprojectponent type.
*/
HOST,
/**
* The URI port org.testifyproject.testifyprojectponent type.
*/
PORT,
/**
* The URI path org.testifyproject.testifyprojectponent type.
*/
PATH,
/**
* The URI path org.testifyproject.testifyprojectponent type that is a path segment.
*/
PATH_SEGMENT,
/**
* The URI path org.testifyproject.testifyprojectponent type that is a matrix parameter.
*/
MATRIX_PARAM,
/**
* The URI query org.testifyproject.testifyprojectponent type, encoded using application/x-www-form-urlencoded rules.
*/
QUERY,
/**
* The URI query org.testifyproject.testifyprojectponent type that is a query parameter, encoded using
* application/x-www-form-urlencoded rules (space character is encoded
* as {@code +}).
*/
QUERY_PARAM,
/**
* The URI query org.testifyproject.testifyprojectponent type that is a query parameter, encoded using
* application/x-www-form-urlencoded (space character is encoded as
* {@code %20}).
*/
QUERY_PARAM_SPACE_ENCODED,
/**
* The URI fragment org.testifyproject.testifyprojectponent type.
*/
FRAGMENT,
}
private UriComponent() {
}
/**
* Validates the legal characters of a percent-encoded string that
* represents a URI org.testifyproject.testifyprojectponent type.
*
* @param s the encoded string.
* @param t the URI org.testifyproject.testifyprojectponent type identifying the legal characters.
* @throws IllegalArgumentException if the encoded string contains illegal
* characters.
*/
public static void validate(final String s, final Type t) {
validate(s, t, false);
}
/**
* Validates the legal characters of a percent-encoded string that
* represents a URI org.testifyproject.testifyprojectponent type.
*
* @param s the encoded string.
* @param t the URI org.testifyproject.testifyprojectponent type identifying the legal characters.
* @param template true if the encoded string contains URI template variables
* @throws IllegalArgumentException if the encoded string contains illegal
* characters.
*/
public static void validate(final String s, final Type t, final boolean template) {
final int i = _valid(s, t, template);
if (i > -1) {
throw new IllegalArgumentException(LocalizationMessages.URI_COMPONENT_INVALID_CHARACTER(s, t, s.charAt(i), i));
}
}
/**
* Validates the legal characters of a percent-encoded string that
* represents a URI org.testifyproject.testifyprojectponent type.
*
* @param s the encoded string.
* @param t the URI org.testifyproject.testifyprojectponent type identifying the legal characters.
* @return true if the encoded string is valid, otherwise false.
*/
public static boolean valid(final String s, final Type t) {
return valid(s, t, false);
}
/**
* Validates the legal characters of a percent-encoded string that
* represents a URI org.testifyproject.testifyprojectponent type.
*
* @param s the encoded string.
* @param t the URI org.testifyproject.testifyprojectponent type identifying the legal characters.
* @param template true if the encoded string contains URI template variables
* @return true if the encoded string is valid, otherwise false.
*/
public static boolean valid(final String s, final Type t, final boolean template) {
return _valid(s, t, template) == -1;
}
private static int _valid(final String s, final Type t, final boolean template) {
final boolean[] table = ENCODING_TABLES[t.ordinal()];
for (int i = 0; i < s.length(); i++) {
final char c = s.charAt(i);
if ((c < 0x80 && c != '%' && !table[c]) || c >= 0x80) {
if (!template || (c != '{' && c != '}')) {
return i;
}
}
}
return -1;
}
/**
* Contextually encodes the characters of string that are either non-ASCII
* characters or are ASCII characters that must be percent-encoded using the
* UTF-8 encoding. Percent-encoded characters will be recognized and not
* double encoded.
*
* @param s the string to be encoded.
* @param t the URI org.testifyproject.testifyprojectponent type identifying the ASCII characters that
* must be percent-encoded.
* @return the encoded string.
*/
public static String contextualEncode(final String s, final Type t) {
return _encode(s, t, false, true);
}
/**
* Contextually encodes the characters of string that are either non-ASCII
* characters or are ASCII characters that must be percent-encoded using the
* UTF-8 encoding. Percent-encoded characters will be recognized and not
* double encoded.
*
* @param s the string to be encoded.
* @param t the URI org.testifyproject.testifyprojectponent type identifying the ASCII characters that
* must be percent-encoded.
* @param template true if the encoded string contains URI template variables
* @return the encoded string.
*/
public static String contextualEncode(final String s, final Type t, final boolean template) {
return _encode(s, t, template, true);
}
/**
* Encodes the characters of string that are either non-ASCII characters
* or are ASCII characters that must be percent-encoded using the
* UTF-8 encoding.
*
* @param s the string to be encoded.
* @param t the URI org.testifyproject.testifyprojectponent type identifying the ASCII characters that
* must be percent-encoded.
* @return the encoded string.
*/
public static String encode(final String s, final Type t) {
return _encode(s, t, false, false);
}
/**
* Encodes the characters of string that are either non-ASCII characters
* or are ASCII characters that must be percent-encoded using the
* UTF-8 encoding.
*
* @param s the string to be encoded.
* @param t the URI org.testifyproject.testifyprojectponent type identifying the ASCII characters that
* must be percent-encoded.
* @param template true if the encoded string contains URI template variables
* @return the encoded string.
*/
public static String encode(final String s, final Type t, final boolean template) {
return _encode(s, t, template, false);
}
/**
* Encodes a string with template parameters names present, specifically the
* characters '{' and '}' will be percent-encoded.
*
* @param s the string with zero or more template parameters names
* @return the string with encoded template parameters names.
*/
public static String encodeTemplateNames(String s) {
int i = s.indexOf('{');
if (i != -1) {
s = s.replace("{", "%7B");
}
i = s.indexOf('}');
if (i != -1) {
s = s.replace("}", "%7D");
}
return s;
}
private static String _encode(final String s, final Type t, final boolean template, final boolean contextualEncode) {
final boolean[] table = ENCODING_TABLES[t.ordinal()];
boolean insideTemplateParam = false;
StringBuilder sb = null;
for (int offset = 0, codePoint; offset < s.length(); offset += Character.charCount(codePoint)) {
codePoint = s.codePointAt(offset);
if (codePoint < 0x80 && table[codePoint]) {
if (sb != null) {
sb.append((char) codePoint);
}
} else {
if (template) {
boolean leavingTemplateParam = false;
if (codePoint == '{') {
insideTemplateParam = true;
} else if (codePoint == '}') {
insideTemplateParam = false;
leavingTemplateParam = true;
}
if (insideTemplateParam || leavingTemplateParam) {
if (sb != null) {
sb.append(Character.toChars(codePoint));
}
continue;
}
}
if (contextualEncode
&& codePoint == '%'
&& offset + 2 < s.length()
&& isHexCharacter(s.charAt(offset + 1))
&& isHexCharacter(s.charAt(offset + 2))) {
if (sb != null) {
sb.append('%').append(s.charAt(offset + 1)).append(s.charAt(offset + 2));
}
offset += 2;
continue;
}
if (sb == null) {
sb = new StringBuilder();
sb.append(s.substring(0, offset));
}
if (codePoint < 0x80) {
if (codePoint == ' ' && (t == Type.QUERY_PARAM)) {
sb.append('+');
} else {
appendPercentEncodedOctet(sb, (char) codePoint);
}
} else {
appendUTF8EncodedCharacter(sb, codePoint);
}
}
}
return (sb == null) ? s : sb.toString();
}
private static final char[] HEX_DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
};
private static void appendPercentEncodedOctet(final StringBuilder sb, final int b) {
sb.append('%');
sb.append(HEX_DIGITS[b >> 4]);
sb.append(HEX_DIGITS[b & 0x0F]);
}
private static void appendUTF8EncodedCharacter(final StringBuilder sb, final int codePoint) {
final CharBuffer chars = CharBuffer.wrap(Character.toChars(codePoint));
final ByteBuffer bytes = UTF_8_CHARSET.encode(chars);
while (bytes.hasRemaining()) {
appendPercentEncodedOctet(sb, bytes.get() & 0xFF);
}
}
private static final String[] SCHEME = {"0-9", "A-Z", "a-z", "+", "-", "."};
private static final String[] UNRESERVED = {"0-9", "A-Z", "a-z", "-", ".", "_", "~"};
private static final String[] SUB_DELIMS = {"!", "$", "&", "'", "(", ")", "*", "+", ",", ";", "="};
private static final boolean[][] ENCODING_TABLES = initEncodingTables();
private static boolean[][] initEncodingTables() {
final boolean[][] tables = new boolean[Type.values().length][];
final List l = new ArrayList();
l.addAll(Arrays.asList(SCHEME));
tables[Type.SCHEME.ordinal()] = initEncodingTable(l);
l.clear();
l.addAll(Arrays.asList(UNRESERVED));
tables[Type.UNRESERVED.ordinal()] = initEncodingTable(l);
l.addAll(Arrays.asList(SUB_DELIMS));
tables[Type.HOST.ordinal()] = initEncodingTable(l);
tables[Type.PORT.ordinal()] = initEncodingTable(Arrays.asList("0-9"));
l.add(":");
tables[Type.USER_INFO.ordinal()] = initEncodingTable(l);
l.add("@");
tables[Type.AUTHORITY.ordinal()] = initEncodingTable(l);
tables[Type.PATH_SEGMENT.ordinal()] = initEncodingTable(l);
tables[Type.PATH_SEGMENT.ordinal()][';'] = false;
tables[Type.MATRIX_PARAM.ordinal()] = tables[Type.PATH_SEGMENT.ordinal()].clone();
tables[Type.MATRIX_PARAM.ordinal()]['='] = false;
l.add("/");
tables[Type.PATH.ordinal()] = initEncodingTable(l);
tables[Type.QUERY.ordinal()] = initEncodingTable(l);
tables[Type.QUERY.ordinal()]['!'] = false;
tables[Type.QUERY.ordinal()]['*'] = false;
tables[Type.QUERY.ordinal()]['\''] = false;
tables[Type.QUERY.ordinal()]['('] = false;
tables[Type.QUERY.ordinal()][')'] = false;
tables[Type.QUERY.ordinal()][';'] = false;
tables[Type.QUERY.ordinal()][':'] = false;
tables[Type.QUERY.ordinal()]['@'] = false;
tables[Type.QUERY.ordinal()]['$'] = false;
tables[Type.QUERY.ordinal()][','] = false;
tables[Type.QUERY.ordinal()]['/'] = false;
tables[Type.QUERY.ordinal()]['?'] = false;
tables[Type.QUERY_PARAM.ordinal()] = Arrays.copyOf(
tables[Type.QUERY.ordinal()],
tables[Type.QUERY.ordinal()].length);
tables[Type.QUERY_PARAM.ordinal()]['='] = false;
tables[Type.QUERY_PARAM.ordinal()]['+'] = false;
tables[Type.QUERY_PARAM.ordinal()]['&'] = false;
tables[Type.QUERY_PARAM_SPACE_ENCODED.ordinal()] = tables[Type.QUERY_PARAM.ordinal()];
tables[Type.FRAGMENT.ordinal()] = tables[Type.QUERY.ordinal()];
return tables;
}
private static boolean[] initEncodingTable(final List allowed) {
final boolean[] table = new boolean[0x80];
for (final String range : allowed) {
if (range.length() == 1) {
table[range.charAt(0)] = true;
} else if (range.length() == 3 && range.charAt(1) == '-') {
for (int i = range.charAt(0); i <= range.charAt(2); i++) {
table[i] = true;
}
}
}
return table;
}
private static final Charset UTF_8_CHARSET = Charset.forName("UTF-8");
/**
* Decodes characters of a string that are percent-encoded octets using
* UTF-8 decoding (if needed).
*
* It is assumed that the string is valid according to an (unspecified) URI
* org.testifyproject.testifyprojectponent type. If a sequence of contiguous percent-encoded octets is
* not a valid UTF-8 character then the octets are replaced with '\uFFFD'.
*
* If the URI org.testifyproject.testifyprojectponent is of type HOST then any "%" found between "[]" is
* left alone. It is an IPv6 literal with a scope_id.
*
* If the URI org.testifyproject.testifyprojectponent is of type QUERY_PARAM then any "+" is decoded as
* as ' '.
*
*
* @param s the string to be decoded.
* @param t the URI org.testifyproject.testifyprojectponent type, may be null.
* @return the decoded string.
* @throws IllegalArgumentException if a malformed percent-encoded octet is
* detected
*/
public static String decode(final String s, final Type t) {
if (s == null) {
throw new IllegalArgumentException();
}
final int n = s.length();
if (n == 0) {
return s;
}
// If there are no percent-escaped octets
if (s.indexOf('%') < 0) {
// If there are no '+' characters for query param
if (t == Type.QUERY_PARAM) {
if (s.indexOf('+') < 0) {
return s;
}
} else {
return s;
}
} else {
// Malformed percent-escaped octet at the end
if (n < 2) {
throw new IllegalArgumentException(LocalizationMessages.URI_COMPONENT_ENCODED_OCTET_MALFORMED(1));
}
// Malformed percent-escaped octet at the end
if (s.charAt(n - 2) == '%') {
throw new IllegalArgumentException(LocalizationMessages.URI_COMPONENT_ENCODED_OCTET_MALFORMED(n - 2));
}
}
if (t == null) {
return decode(s, n);
}
switch (t) {
case HOST:
return decodeHost(s, n);
case QUERY_PARAM:
return decodeQueryParam(s, n);
default:
return decode(s, n);
}
}
/**
* Decode the query org.testifyproject.testifyprojectponent of a URI.
*
* Query parameter names in the returned map are always decoded. Decoding of query parameter
* values can be controlled using the {@code decode} parameter flag.
*
*
* @param u the URI.
* @param decode {@code true} if the returned query parameter values of the query org.testifyproject.testifyprojectponent
* should be in decoded form.
* @return the multivalued map of query parameters.
*/
public static MultivaluedMap decodeQuery(final URI u, final boolean decode) {
return decodeQuery(u.getRawQuery(), decode);
}
/**
* Decode the query org.testifyproject.testifyprojectponent of a URI.
*
* Query parameter names in the returned map are always decoded. Decoding of query parameter
* values can be controlled using the {@code decode} parameter flag.
*
*
* @param q the query org.testifyproject.testifyprojectponent in encoded form.
* @param decode {@code true} if the returned query parameter values of the query org.testifyproject.testifyprojectponent
* should be in decoded form.
* @return the multivalued map of query parameters.
*/
public static MultivaluedMap decodeQuery(final String q, final boolean decode) {
return decodeQuery(q, true, decode);
}
/**
* Decode the query org.testifyproject.testifyprojectponent of a URI.
*
* Decoding of query parameter names and values can be controlled using the {@code decodeNames}
* and {@code decodeValues} parameter flags.
*
*
* @param q the query org.testifyproject.testifyprojectponent in encoded form.
* @param decodeNames {@code true} if the returned query parameter names of the query org.testifyproject.testifyprojectponent
* should be in decoded form.
* @param decodeValues {@code true} if the returned query parameter values of the query org.testifyproject.testifyprojectponent
* should be in decoded form.
* @return the multivalued map of query parameters.
*/
public static MultivaluedMap decodeQuery(final String q, final boolean decodeNames,
final boolean decodeValues) {
final MultivaluedMap queryParameters = new MultivaluedStringMap();
if (q == null || q.length() == 0) {
return queryParameters;
}
int s = 0;
do {
final int e = q.indexOf('&', s);
if (e == -1) {
decodeQueryParam(queryParameters, q.substring(s), decodeNames, decodeValues);
} else if (e > s) {
decodeQueryParam(queryParameters, q.substring(s, e), decodeNames, decodeValues);
}
s = e + 1;
} while (s > 0 && s < q.length());
return queryParameters;
}
@SuppressWarnings("StatementWithEmptyBody")
private static void decodeQueryParam(final MultivaluedMap params, final String param,
final boolean decodeNames, final boolean decodeValues) {
try {
final int equals = param.indexOf('=');
if (equals > 0) {
params.add((decodeNames) ? URLDecoder.decode(param.substring(0, equals), "UTF-8") : param.substring(0, equals),
(decodeValues) ? URLDecoder.decode(param.substring(equals + 1), "UTF-8") : param.substring(equals + 1));
} else if (equals == 0) {
// no key declared, ignore
} else if (param.length() > 0) {
params.add((decodeNames) ? URLDecoder.decode(param, "UTF-8") : param, "");
}
} catch (final UnsupportedEncodingException ex) {
// This should never occur
throw new IllegalArgumentException(ex);
}
}
private static final class PathSegmentImpl implements PathSegment {
private static final PathSegment EMPTY_PATH_SEGMENT = new PathSegmentImpl("", false);
private final String path;
private final MultivaluedMap matrixParameters;
PathSegmentImpl(final String path, final boolean decode) {
this(path, decode, new MultivaluedStringMap());
}
PathSegmentImpl(final String path, final boolean decode, final MultivaluedMap matrixParameters) {
this.path = (decode) ? UriComponent.decode(path, UriComponent.Type.PATH_SEGMENT) : path;
this.matrixParameters = matrixParameters;
}
@Override
public String getPath() {
return path;
}
@Override
public MultivaluedMap getMatrixParameters() {
return matrixParameters;
}
@Override
public String toString() {
return path;
}
}
/**
* Decode the path org.testifyproject.testifyprojectponent of a URI as path segments.
*
* @param u the URI. If the path org.testifyproject.testifyprojectponent is an absolute path org.testifyproject.testifyprojectponent
* then the leading '/' is ignored and is not considered a delimiator
* of a path segment.
* @param decode true if the path segments of the path org.testifyproject.testifyprojectponent
* should be in decoded form.
* @return the list of path segments.
*/
public static List decodePath(final URI u, final boolean decode) {
String rawPath = u.getRawPath();
if (rawPath != null && rawPath.length() > 0 && rawPath.charAt(0) == '/') {
rawPath = rawPath.substring(1);
}
return decodePath(rawPath, decode);
}
/**
* Decode the path org.testifyproject.testifyprojectponent of a URI as path segments.
*
* Any '/' character in the path is considered to be a deliminator
* between two path segments. Thus if the path is '/' then the path segment
* list will contain two empty path segments. If the path is "//" then
* the path segment list will contain three empty path segments. If the path
* is "/a/" the path segment list will consist of the following path
* segments in order: "", "a" and "".
*
*
* @param path the path org.testifyproject.testifyprojectponent in encoded form.
* @param decode true if the path segments of the path org.testifyproject.testifyprojectponent
* should be in decoded form.
* @return the list of path segments.
*/
public static List decodePath(final String path, final boolean decode) {
final List segments = new LinkedList();
if (path == null) {
return segments;
}
int s;
int e = -1;
do {
s = e + 1;
e = path.indexOf('/', s);
if (e > s) {
decodePathSegment(segments, path.substring(s, e), decode);
} else if (e == s) {
segments.add(PathSegmentImpl.EMPTY_PATH_SEGMENT);
}
} while (e != -1);
if (s < path.length()) {
decodePathSegment(segments, path.substring(s), decode);
} else {
segments.add(PathSegmentImpl.EMPTY_PATH_SEGMENT);
}
return segments;
}
/**
* Decode the path segment and add it to the list of path segments.
*
* @param segments mutable list of path segments.
* @param segment path segment to be decoded.
* @param decode {@code true} if the path segment should be in a decoded form.
*/
public static void decodePathSegment(final List segments, final String segment, final boolean decode) {
final int colon = segment.indexOf(';');
if (colon != -1) {
segments.add(new PathSegmentImpl((colon == 0) ? "" : segment.substring(0, colon), decode, decodeMatrix(segment,
decode)));
} else {
segments.add(new PathSegmentImpl(segment, decode));
}
}
/**
* Decode the matrix org.testifyproject.testifyprojectponent of a URI path segment.
*
* @param pathSegment the path segment org.testifyproject.testifyprojectponent in encoded form.
* @param decode true if the matrix parameters of the path segment org.testifyproject.testifyprojectponent
* should be in decoded form.
* @return the multivalued map of matrix parameters.
*/
public static MultivaluedMap decodeMatrix(final String pathSegment, final boolean decode) {
final MultivaluedMap matrixMap = new MultivaluedStringMap();
// Skip over path segment
int s = pathSegment.indexOf(';') + 1;
if (s == 0 || s == pathSegment.length()) {
return matrixMap;
}
do {
final int e = pathSegment.indexOf(';', s);
if (e == -1) {
decodeMatrixParam(matrixMap, pathSegment.substring(s), decode);
} else if (e > s) {
decodeMatrixParam(matrixMap, pathSegment.substring(s, e), decode);
}
s = e + 1;
} while (s > 0 && s < pathSegment.length());
return matrixMap;
}
@SuppressWarnings("StatementWithEmptyBody")
private static void decodeMatrixParam(final MultivaluedMap params, final String param, final boolean decode) {
final int equals = param.indexOf('=');
if (equals > 0) {
params.add(UriComponent.decode(param.substring(0, equals), UriComponent.Type.MATRIX_PARAM),
(decode) ? UriComponent.decode(param.substring(equals + 1), UriComponent.Type.MATRIX_PARAM) : param
.substring(equals + 1));
} else if (equals == 0) {
// no key declared, ignore
} else if (param.length() > 0) {
params.add(UriComponent.decode(param, UriComponent.Type.MATRIX_PARAM), "");
}
}
private static String decode(final String s, final int n) {
final StringBuilder sb = new StringBuilder(n);
ByteBuffer bb = null;
for (int i = 0; i < n; ) {
final char c = s.charAt(i++);
if (c != '%') {
sb.append(c);
} else {
bb = decodePercentEncodedOctets(s, i, bb);
i = decodeOctets(i, bb, sb);
}
}
return sb.toString();
}
private static String decodeQueryParam(final String s, final int n) {
final StringBuilder sb = new StringBuilder(n);
ByteBuffer bb = null;
for (int i = 0; i < n; ) {
final char c = s.charAt(i++);
if (c != '%') {
if (c != '+') {
sb.append(c);
} else {
sb.append(' ');
}
} else {
bb = decodePercentEncodedOctets(s, i, bb);
i = decodeOctets(i, bb, sb);
}
}
return sb.toString();
}
private static String decodeHost(final String s, final int n) {
final StringBuilder sb = new StringBuilder(n);
ByteBuffer bb = null;
boolean betweenBrackets = false;
for (int i = 0; i < n; ) {
final char c = s.charAt(i++);
if (c == '[') {
betweenBrackets = true;
} else if (betweenBrackets && c == ']') {
betweenBrackets = false;
}
if (c != '%' || betweenBrackets) {
sb.append(c);
} else {
bb = decodePercentEncodedOctets(s, i, bb);
i = decodeOctets(i, bb, sb);
}
}
return sb.toString();
}
/**
* Decode a continuous sequence of percent encoded octets.
*
* Assumes the index, i, starts that the first hex digit of the first
* percent-encoded octet.
*/
private static ByteBuffer decodePercentEncodedOctets(final String s, int i, ByteBuffer bb) {
if (bb == null) {
bb = ByteBuffer.allocate(1);
} else {
bb.clear();
}
while (true) {
// Decode the hex digits
bb.put((byte) (decodeHex(s, i++) << 4 | decodeHex(s, i++)));
// Finish if at the end of the string
if (i == s.length()) {
break;
}
// Finish if no more percent-encoded octets follow
if (s.charAt(i++) != '%') {
break;
}
// Check if the byte buffer needs to be increased in size
if (bb.position() == bb.capacity()) {
bb.flip();
// Create a new byte buffer with the maximum number of possible
// octets, hence resize should only occur once
final ByteBuffer bb_new = ByteBuffer.allocate(s.length() / 3);
bb_new.put(bb);
bb = bb_new;
}
}
bb.flip();
return bb;
}
/**
* Decodes octets to characters using the UTF-8 decoding and appends
* the characters to a StringBuffer.
*
* @return the index to the next unchecked character in the string to decode
*/
private static int decodeOctets(final int i, final ByteBuffer bb, final StringBuilder sb) {
// If there is only one octet and is an ASCII character
if (bb.limit() == 1 && (bb.get(0) & 0xFF) < 0x80) {
// Octet can be appended directly
sb.append((char) bb.get(0));
return i + 2;
} else {
//
final CharBuffer cb = UTF_8_CHARSET.decode(bb);
sb.append(cb.toString());
return i + bb.limit() * 3 - 1;
}
}
private static int decodeHex(final String s, final int i) {
final int v = decodeHex(s.charAt(i));
if (v == -1) {
throw new IllegalArgumentException(LocalizationMessages.URI_COMPONENT_ENCODED_OCTET_INVALID_DIGIT(i, s.charAt(i)));
}
return v;
}
private static final int[] HEX_TABLE = initHexTable();
private static int[] initHexTable() {
final int[] table = new int[0x80];
Arrays.fill(table, -1);
for (char c = '0'; c <= '9'; c++) {
table[c] = c - '0';
}
for (char c = 'A'; c <= 'F'; c++) {
table[c] = c - 'A' + 10;
}
for (char c = 'a'; c <= 'f'; c++) {
table[c] = c - 'a' + 10;
}
return table;
}
private static int decodeHex(final char c) {
return (c < 128) ? HEX_TABLE[c] : -1;
}
/**
* Checks whether the character {@code c} is hexadecimal character.
*
* @param c Any character
* @return The is {@code c} is a hexadecimal character (e.g. 0, 5, a, A, f, ...)
*/
public static boolean isHexCharacter(final char c) {
return c < 128 && HEX_TABLE[c] != -1;
}
/**
* Return the {@code Request-Uri} representation as defined by HTTP spec. For example:
* <Method> <Request-URI> HTTP/<Version> (e.g. GET /auth;foo=bar/hello?foo=bar HTTP/1.1)
*
* @param uri uri to obtain {@code Request-Uri} from.
* @return {@code Request-Uri} representation or {@code null} if {@code uri} is not provided.
*/
public static String fullRelativeUri(final URI uri) {
if (uri == null) {
return null;
}
final String query = uri.getRawQuery();
return uri.getRawPath() + (query != null && query.length() > 0 ? "?" + query : "");
}
}