package org.jboss.resteasy.util;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import javax.ws.rs.core.MultivaluedMap;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Author: Bill Burke Version: $Revision: 1 $
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Encode
{
private static final String UTF_8 = StandardCharsets.UTF_8.name();
private static final Pattern PARAM_REPLACEMENT = Pattern.compile("_resteasy_uri_parameter");
private static final String[] pathEncoding = new String[128];
private static final String[] pathSegmentEncoding = new String[128];
private static final String[] matrixParameterEncoding = new String[128];
private static final String[] queryNameValueEncoding = new String[128];
private static final String[] queryStringEncoding = new String[128];
static
{
/*
* Encode via <a href="http://ietf.org/rfc/rfc3986.txt">RFC 3986</a>. PCHAR is allowed allong with '/'
*
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
*
*/
for (int i = 0; i < 128; i++)
{
if (i >= 'a' && i <= 'z') continue;
if (i >= 'A' && i <= 'Z') continue;
if (i >= '0' && i <= '9') continue;
switch ((char) i)
{
case '-':
case '.':
case '_':
case '~':
case '!':
case '$':
case '&':
case '\'':
case '(':
case ')':
case '*':
case '+':
case ',':
case '/':
case ';':
case '=':
case ':':
case '@':
continue;
}
pathEncoding[i] = encodeString(String.valueOf((char) i));
}
pathEncoding[' '] = "%20";
System.arraycopy(pathEncoding, 0, matrixParameterEncoding, 0, pathEncoding.length);
matrixParameterEncoding[';'] = "%3B";
matrixParameterEncoding['='] = "%3D";
matrixParameterEncoding['/'] = "%2F"; // RESTEASY-729
System.arraycopy(pathEncoding, 0, pathSegmentEncoding, 0, pathEncoding.length);
pathSegmentEncoding['/'] = "%2F";
/*
* Encode via <a href="http://ietf.org/rfc/rfc3986.txt">RFC 3986</a>.
*
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* space encoded as '+'
*
*/
for (int i = 0; i < 128; i++)
{
if (i >= 'a' && i <= 'z') continue;
if (i >= 'A' && i <= 'Z') continue;
if (i >= '0' && i <= '9') continue;
switch ((char) i)
{
case '-':
case '.':
case '_':
case '~':
case '?':
continue;
case ' ':
queryNameValueEncoding[i] = "+";
continue;
}
queryNameValueEncoding[i] = encodeString(String.valueOf((char) i));
}
/*
* query = *( pchar / "/" / "?" )
*/
for (int i = 0; i < 128; i++)
{
if (i >= 'a' && i <= 'z') continue;
if (i >= 'A' && i <= 'Z') continue;
if (i >= '0' && i <= '9') continue;
switch ((char) i)
{
case '-':
case '.':
case '_':
case '~':
case '!':
case '$':
case '&':
case '\'':
case '(':
case ')':
case '*':
case '+':
case ',':
case ';':
case '=':
case ':':
case '@':
case '?':
case '/':
continue;
case ' ':
queryStringEncoding[i] = "%20";
continue;
}
queryStringEncoding[i] = encodeString(String.valueOf((char) i));
}
}
Keep encoded values "%..." and template parameters intact.
Params: - value – query string
Returns: encoded query string
/**
* Keep encoded values "%..." and template parameters intact.
* @param value query string
* @return encoded query string
*/
public static String encodeQueryString(String value)
{
return encodeValue(value, queryStringEncoding);
}
Keep encoded values "%...", matrix parameters, template parameters, and '/' characters intact.
Params: - value – path
Returns: encoded path
/**
* Keep encoded values "%...", matrix parameters, template parameters, and '/' characters intact.
* @param value path
* @return encoded path
*/
public static String encodePath(String value)
{
return encodeValue(value, pathEncoding);
}
Keep encoded values "%...", matrix parameters and template parameters intact.
Params: - value – path segment
Returns: encoded path segment
/**
* Keep encoded values "%...", matrix parameters and template parameters intact.
* @param value path segment
* @return encoded path segment
*/
public static String encodePathSegment(String value)
{
return encodeValue(value, pathSegmentEncoding);
}
Keep encoded values "%..." and template parameters intact.
Params: - value – uri fragment
Returns: encoded uri fragment
/**
* Keep encoded values "%..." and template parameters intact.
* @param value uri fragment
* @return encoded uri fragment
*/
public static String encodeFragment(String value)
{
return encodeValue(value, queryStringEncoding);
}
Keep encoded values "%..." and template parameters intact.
Params: - value – matrix parameter
Returns: encoded matrix parameter
/**
* Keep encoded values "%..." and template parameters intact.
* @param value matrix parameter
* @return encoded matrix parameter
*/
public static String encodeMatrixParam(String value)
{
return encodeValue(value, matrixParameterEncoding);
}
Keep encoded values "%..." and template parameters intact.
Params: - value – query parameter
Returns: encoded query parameter
/**
* Keep encoded values "%..." and template parameters intact.
* @param value query parameter
* @return encoded query parameter
*/
public static String encodeQueryParam(String value)
{
return encodeValue(value, queryNameValueEncoding);
}
//private static final Pattern nonCodes = Pattern.compile("%([^a-fA-F0-9]|$)");
private static final Pattern nonCodes = Pattern.compile("%([^a-fA-F0-9]|[a-fA-F0-9]$|$|[a-fA-F0-9][^a-fA-F0-9])");
private static final Pattern encodedChars = Pattern.compile("%([a-fA-F0-9][a-fA-F0-9])");
private static final Pattern encodedCharsMulti = Pattern.compile("((%[a-fA-F0-9][a-fA-F0-9])+)");
public static String decodePath(String path)
{
if (path.indexOf('%') == -1) {
return path;
}
Matcher matcher = encodedCharsMulti.matcher(path);
int start=0;
StringBuilder builder = new StringBuilder();
CharsetDecoder decoder = Charset.forName(UTF_8).newDecoder();
while (matcher.find())
{
builder.append(path, start, matcher.start());
decoder.reset();
String decoded = decodeBytes(matcher.group(1), decoder);
builder.append(decoded);
start = matcher.end();
}
builder.append(path, start, path.length());
return builder.toString();
}
private static String decodeBytes(String enc, CharsetDecoder decoder)
{
Matcher matcher = encodedChars.matcher(enc);
ByteBuffer bytes = ByteBuffer.allocate(enc.length() / 3);
while (matcher.find())
{
int b = Integer.parseInt(matcher.group(1), 16);
bytes.put((byte) b);
}
bytes.flip();
try
{
return decoder.decode(bytes).toString();
}
catch (CharacterCodingException e)
{
throw new RuntimeException(e);
}
}
Encode '%' if it is not an encoding sequence
Params: - string – value to encode
Returns: encoded value
/**
* Encode '%' if it is not an encoding sequence
*
* @param string value to encode
* @return encoded value
*/
public static String encodeNonCodes(String string)
{
Matcher matcher = nonCodes.matcher(string);
StringBuilder builder = new StringBuilder();
// FYI: we do not use the no-arg matcher.find()
// coupled with matcher.appendReplacement()
// because the matched text may contain
// a second % and we must make sure we
// encode it (if necessary).
int idx = 0;
while (matcher.find(idx))
{
int start = matcher.start();
builder.append(string.substring(idx, start));
builder.append("%25");
idx = start + 1;
}
builder.append(string.substring(idx));
return builder.toString();
}
public static boolean savePathParams(String segmentString, StringBuilder newSegment, List<String> params)
{
boolean foundParam = false;
// Regular expressions can have '{' and '}' characters. Replace them to do match
CharSequence segment = PathHelper.replaceEnclosedCurlyBracesCS(segmentString);
Matcher matcher = PathHelper.URI_TEMPLATE_PATTERN.matcher(segment);
int start = 0;
while (matcher.find())
{
newSegment.append(segment, start, matcher.start());
foundParam = true;
String group = matcher.group();
// Regular expressions can have '{' and '}' characters. Recover earlier replacement
params.add(PathHelper.recoverEnclosedCurlyBraces(group));
newSegment.append("_resteasy_uri_parameter");
start = matcher.end();
}
newSegment.append(segment, start, segment.length());
return foundParam;
}
Keep encoded values "%..." and template parameters intact i.e. "{x}"
Params: - segment – value to encode
- encoding – encoding
Returns: encoded value
/**
* Keep encoded values "%..." and template parameters intact i.e. "{x}"
*
* @param segment value to encode
* @param encoding encoding
* @return encoded value
*/
public static String encodeValue(String segment, String[] encoding)
{
ArrayList<String> params = new ArrayList<String>();
boolean foundParam = false;
StringBuilder newSegment = new StringBuilder();
if (savePathParams(segment, newSegment, params))
{
foundParam = true;
segment = newSegment.toString();
}
String result = encodeFromArray(segment, encoding, false);
result = encodeNonCodes(result);
segment = result;
if (foundParam)
{
segment = pathParamReplacement(segment, params);
}
return segment;
}
Encode via RFC 3986. PCHAR is allowed allong with '/'
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
Params: - segment – value to encode
Returns: encoded value
/**
* Encode via <a href="http://ietf.org/rfc/rfc3986.txt">RFC 3986</a>. PCHAR is allowed allong with '/'
* <p>
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
* / "*" / "+" / "," / ";" / "="
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* @param segment value to encode
* @return encoded value
*/
public static String encodePathAsIs(String segment)
{
return encodeFromArray(segment, pathEncoding, true);
}
Keep any valid encodings from string i.e. keep "%2D" but don't keep "%p"
Params: - segment – value to encode
Returns: encoded value
/**
* Keep any valid encodings from string i.e. keep "%2D" but don't keep "%p"
*
* @param segment value to encode
* @return encoded value
*/
public static String encodePathSaveEncodings(String segment)
{
String result = encodeFromArray(segment, pathEncoding, false);
result = encodeNonCodes(result);
return result;
}
Encode via RFC 3986. PCHAR is allowed allong with '/'
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
Params: - segment – value to encode
Returns: encoded value
/**
* Encode via <a href="http://ietf.org/rfc/rfc3986.txt">RFC 3986</a>. PCHAR is allowed allong with '/'
* <p>
* unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
* sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
* / "*" / "+" / "," / ";" / "="
* pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
* @param segment value to encode
* @return encoded value
*/
public static String encodePathSegmentAsIs(String segment)
{
return encodeFromArray(segment, pathSegmentEncoding, true);
}
Keep any valid encodings from string i.e. keep "%2D" but don't keep "%p"
Params: - segment – value to encode
Returns: encoded value
/**
* Keep any valid encodings from string i.e. keep "%2D" but don't keep "%p"
*
* @param segment value to encode
* @return encoded value
*/
public static String encodePathSegmentSaveEncodings(String segment)
{
String result = encodeFromArray(segment, pathSegmentEncoding, false);
result = encodeNonCodes(result);
return result;
}
Encodes everything of a query parameter name or value.
Params: - nameOrValue – value to encode
Returns: encoded value
/**
* Encodes everything of a query parameter name or value.
*
* @param nameOrValue value to encode
* @return encoded value
*/
public static String encodeQueryParamAsIs(String nameOrValue)
{
return encodeFromArray(nameOrValue, queryNameValueEncoding, true);
}
Keep any valid encodings from string i.e. keep "%2D" but don't keep "%p"
Params: - segment – value to encode
Returns: encoded value
/**
* Keep any valid encodings from string i.e. keep "%2D" but don't keep "%p"
*
* @param segment value to encode
* @return encoded value
*/
public static String encodeQueryParamSaveEncodings(String segment)
{
String result = encodeFromArray(segment, queryNameValueEncoding, false);
result = encodeNonCodes(result);
return result;
}
public static String encodeFragmentAsIs(String nameOrValue)
{
return encodeFromArray(nameOrValue, queryNameValueEncoding, true);
}
protected static String encodeFromArray(String segment, String[] encodingMap, boolean encodePercent)
{
StringBuilder result = new StringBuilder();
for (int i = 0; i < segment.length(); i++)
{
char currentChar = segment.charAt(i);
if (!encodePercent && currentChar == '%')
{
result.append(currentChar);
continue;
}
if (Character.isHighSurrogate(currentChar)) {
String part = segment.substring(i, i + 2);
try {
result.append(URLEncoder.encode(part, StandardCharsets.UTF_8.name()));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
++i;
continue;
}
String encoding = encode(currentChar, encodingMap);
if (encoding == null)
{
result.append(currentChar);
}
else
{
result.append(encoding);
}
}
return result.toString();
}
Params: - zhar – integer representation of character
- encodingMap – encoding map
Returns: URL encoded character
/**
* @param zhar integer representation of character
* @param encodingMap encoding map
* @return URL encoded character
*/
private static String encode(int zhar, String[] encodingMap)
{
String encoded;
if (zhar < encodingMap.length)
{
encoded = encodingMap[zhar];
}
else
{
encoded = encodeString(Character.toString((char) zhar));
}
return encoded;
}
Calls URLEncoder.encode(s, "UTF-8") on given input.
Params: - s – string to encode
Returns: encoded string returned by URLEncoder.encode(s, "UTF-8")
/**
* Calls URLEncoder.encode(s, "UTF-8") on given input.
* @param s string to encode
* @return encoded string returned by URLEncoder.encode(s, "UTF-8")
*/
public static String encodeString(String s)
{
try
{
return URLEncoder.encode(s, UTF_8);
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException(e);
}
}
public static String pathParamReplacement(String segment, List<String> params)
{
StringBuilder newSegment = new StringBuilder();
Matcher matcher = PARAM_REPLACEMENT.matcher(segment);
int i = 0;
int start = 0;
while (matcher.find())
{
newSegment.append(segment, start, matcher.start());
String replacement = params.get(i++);
newSegment.append(replacement);
start = matcher.end();
}
newSegment.append(segment, start, segment.length());
segment = newSegment.toString();
return segment;
}
decode an encoded map
Params: - map – map
Returns: decoded map
/**
* decode an encoded map
*
* @param map map
* @return decoded map
*/
public static MultivaluedMap<String, String> decode(MultivaluedMap<String, String> map)
{
MultivaluedMapImpl<String, String> decoded = new MultivaluedMapImpl<String, String>();
for (Map.Entry<String, List<String>> entry : map.entrySet())
{
List<String> values = entry.getValue();
for (String value : values)
{
try
{
decoded.add(URLDecoder.decode(entry.getKey(), UTF_8), URLDecoder.decode(value, UTF_8));
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException(e);
}
}
}
return decoded;
}
decode an encoded map
Params: - map – map
- charset – charset
Returns: decoded map
/**
* decode an encoded map
*
* @param map map
* @param charset charset
* @return decoded map
*/
public static MultivaluedMap<String, String> decode(MultivaluedMap<String, String> map, String charset)
{
if (charset == null)
{
charset = UTF_8;
}
MultivaluedMapImpl<String, String> decoded = new MultivaluedMapImpl<String, String>();
for (Map.Entry<String, List<String>> entry : map.entrySet())
{
List<String> values = entry.getValue();
for (String value : values)
{
try
{
decoded.add(URLDecoder.decode(entry.getKey(), charset), URLDecoder.decode(value, charset));
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException(e);
}
}
}
return decoded;
}
public static MultivaluedMap<String, String> encode(MultivaluedMap<String, String> map)
{
MultivaluedMapImpl<String, String> decoded = new MultivaluedMapImpl<String, String>();
for (Map.Entry<String, List<String>> entry : map.entrySet())
{
List<String> values = entry.getValue();
for (String value : values)
{
try
{
decoded.add(URLEncoder.encode(entry.getKey(), UTF_8), URLEncoder.encode(value, UTF_8));
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException(e);
}
}
}
return decoded;
}
public static String decode(String string)
{
try
{
return URLDecoder.decode(string, UTF_8);
}
catch (UnsupportedEncodingException e)
{
throw new RuntimeException(e);
}
}
}