blob: 85fdd7ed016816ec2575593b6659f9a88d797c9a [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.cxf.jaxrs.model;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.ws.rs.Path;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.PathSegment;
import org.apache.cxf.common.util.SystemPropertyAction;
import org.apache.cxf.jaxrs.utils.HttpUtils;
import org.apache.cxf.jaxrs.utils.JAXRSUtils;
public final class URITemplate {
public static final String TEMPLATE_PARAMETERS = "jaxrs.template.parameters";
public static final String URI_TEMPLATE = "jaxrs.template.uri";
public static final String LIMITED_REGEX_SUFFIX = "(/.*)?";
public static final String FINAL_MATCH_GROUP = "FINAL_MATCH_GROUP";
private static final String DEFAULT_PATH_VARIABLE_REGEX = "([^/]+?)";
private static final String CHARACTERS_TO_ESCAPE = ".*+$()";
private static final String SLASH = "/";
private static final String SLASH_QUOTE = "/;";
private static final int MAX_URI_TEMPLATE_CACHE_SIZE =
SystemPropertyAction.getInteger("org.apache.cxf.jaxrs.max_uri_template_cache_size", 2000);
private static final Map<String, URITemplate> URI_TEMPLATE_CACHE = new ConcurrentHashMap<>();
private final String template;
private final List<String> variables = new ArrayList<>();
private final List<String> customVariables = new ArrayList<>();
private final Pattern templateRegexPattern;
private final String literals;
private final List<UriChunk> uriChunks;
public URITemplate(String theTemplate) {
template = theTemplate;
StringBuilder literalChars = new StringBuilder();
StringBuilder patternBuilder = new StringBuilder();
CurlyBraceTokenizer tok = new CurlyBraceTokenizer(template);
uriChunks = new ArrayList<>();
while (tok.hasNext()) {
String templatePart = tok.next();
UriChunk chunk = UriChunk.createUriChunk(templatePart);
uriChunks.add(chunk);
if (chunk instanceof Literal) {
String encodedValue = HttpUtils.encodePartiallyEncoded(chunk.getValue(), false);
String substr = escapeCharacters(encodedValue);
literalChars.append(substr);
patternBuilder.append(substr);
} else if (chunk instanceof Variable) {
Variable var = (Variable)chunk;
variables.add(var.getName());
String pattern = var.getPattern();
if (pattern != null) {
customVariables.add(var.getName());
// Add parenthesis to the pattern to identify a regex in the pattern,
// however do not add them if they already exist since that will cause the Matcher
// to create extraneous values. Parens identify a group so multiple parens would
// indicate multiple groups.
if (pattern.startsWith("(") && pattern.endsWith(")")) {
patternBuilder.append(pattern);
} else {
patternBuilder.append('(');
patternBuilder.append(pattern);
patternBuilder.append(')');
}
} else {
patternBuilder.append(DEFAULT_PATH_VARIABLE_REGEX);
}
}
}
literals = literalChars.toString();
int endPos = patternBuilder.length() - 1;
boolean endsWithSlash = (endPos >= 0) && patternBuilder.charAt(endPos) == '/';
if (endsWithSlash) {
patternBuilder.deleteCharAt(endPos);
}
patternBuilder.append(LIMITED_REGEX_SUFFIX);
templateRegexPattern = Pattern.compile(patternBuilder.toString());
}
public String getLiteralChars() {
return literals;
}
public String getValue() {
return template;
}
public String getPatternValue() {
return templateRegexPattern.toString();
}
/**
* List of all variables in order of appearance in template.
*
* @return unmodifiable list of variable names w/o patterns, e.g. for "/foo/{v1:\\d}/{v2}" returned list
* is ["v1","v2"].
*/
public List<String> getVariables() {
return Collections.unmodifiableList(variables);
}
/**
* List of variables with patterns (regexps). List is subset of elements from {@link #getVariables()}.
*
* @return unmodifiable list of variables names w/o patterns.
*/
public List<String> getCustomVariables() {
return Collections.unmodifiableList(customVariables);
}
private static String escapeCharacters(String expression) {
int length = expression.length();
int i = 0;
char ch = ' ';
for (; i < length; ++i) {
ch = expression.charAt(i);
if (isReservedCharacter(ch)) {
break;
}
}
if (i == length) {
return expression;
}
// Allows for up to 8 escaped characters before we start creating more
// StringBuilders. 8 is an arbitrary limit, but it seems to be
// sufficient in most cases.
StringBuilder sb = new StringBuilder(length + 8);
sb.append(expression, 0, i);
sb.append('\\');
sb.append(ch);
++i;
for (; i < length; ++i) {
ch = expression.charAt(i);
if (isReservedCharacter(ch)) {
sb.append('\\');
}
sb.append(ch);
}
return sb.toString();
}
private static boolean isReservedCharacter(char ch) {
return CHARACTERS_TO_ESCAPE.indexOf(ch) != -1;
}
public boolean match(String uri, MultivaluedMap<String, String> templateVariableToValue) {
if (uri == null) {
return (templateRegexPattern == null) ? true : false;
}
if (templateRegexPattern == null) {
return false;
}
Matcher m = templateRegexPattern.matcher(uri);
if (!m.matches() || template.equals(SLASH) && uri.startsWith(SLASH_QUOTE)) {
if (uri.contains(";")) {
// we might be trying to match one or few path segments
// containing matrix
// parameters against a clear path segment as in @Path("base").
List<PathSegment> pList = JAXRSUtils.getPathSegments(template, false);
List<PathSegment> uList = JAXRSUtils.getPathSegments(uri, false);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < uList.size(); i++) {
final String segment;
if (pList.size() > i && pList.get(i).getPath().indexOf('{') == -1) {
segment = uList.get(i).getPath();
} else {
segment = HttpUtils.fromPathSegment(uList.get(i));
}
if (!segment.isEmpty()) {
sb.append(SLASH);
}
sb.append(segment);
}
uri = sb.toString();
if (uri.isEmpty()) {
uri = SLASH;
}
m = templateRegexPattern.matcher(uri);
if (!m.matches()) {
return false;
}
} else {
return false;
}
}
// Assign the matched template values to template variables
int groupCount = m.groupCount();
int i = 1;
for (String name : variables) {
while (i <= groupCount) {
String value = m.group(i++);
if ((value == null || value.length() == 0 && i < groupCount)
&& variables.size() + 1 < groupCount) {
continue;
}
templateVariableToValue.add(name, value);
break;
}
}
// The right hand side value, might be used to further resolve
// sub-resources.
String finalGroup = i > groupCount ? SLASH : m.group(groupCount);
if (finalGroup == null || finalGroup.startsWith(SLASH_QUOTE)) {
finalGroup = SLASH;
}
templateVariableToValue.putSingle(FINAL_MATCH_GROUP, finalGroup);
return true;
}
/**
* Substitutes template variables with listed values. List of values is counterpart for
* {@link #getVariables() list of variables}. When list of value is shorter than variables substitution
* is partial. When variable has pattern, value must fit to pattern, otherwise
* {@link IllegalArgumentException} is thrown.
* <p>
* Example1: for template "/{a}/{b}/{a}" {@link #getVariables()} returns "[a, b, a]"; providing here list
* of value "[foo, bar, baz]" results with "/foo/bar/baz".
* <p>
* Example2: for template "/{a}/{b}/{a}" providing list of values "[foo]" results with "/foo/{b}/{a}".
*
* @param values values for variables
* @return template with bound variables.
* @throws IllegalArgumentException when values is null, any value does not match pattern etc.
*/
public String substitute(List<String> values) throws IllegalArgumentException {
if (values == null) {
throw new IllegalArgumentException("values is null");
}
Iterator<String> iter = values.iterator();
StringBuilder sb = new StringBuilder();
for (UriChunk chunk : uriChunks) {
if (chunk instanceof Variable) {
Variable var = (Variable)chunk;
if (iter.hasNext()) {
String value = iter.next();
if (!var.matches(value)) {
throw new IllegalArgumentException("Value '" + value + "' does not match variable "
+ var.getName() + " with pattern "
+ var.getPattern());
}
sb.append(value);
} else {
sb.append(var);
}
} else {
sb.append(chunk);
}
}
return sb.toString();
}
String substitute(Map<String, ? extends Object> valuesMap) throws IllegalArgumentException {
return this.substitute(valuesMap, Collections.<String>emptySet(), false);
}
/**
* Substitutes template variables with mapped values. Variables are mapped to values; if not all variables
* are bound result will still contain variables. Note that all variables with the same name are replaced
* by one value.
* <p>
* Example: for template "/{a}/{b}/{a}" {@link #getVariables()} returns "[a, b, a]"; providing here
* mapping "[a: foo, b: bar]" results with "/foo/bar/foo" (full substitution) and for mapping "[b: baz]"
* result is "{a}/baz/{a}" (partial substitution).
*
* @param valuesMap map variables to their values; on each value Object.toString() is called.
* @return template with bound variables.
*/
public String substitute(Map<String, ? extends Object> valuesMap,
Set<String> encodePathSlashVars,
boolean allowUnresolved) throws IllegalArgumentException {
if (valuesMap == null) {
throw new IllegalArgumentException("valuesMap is null");
}
StringBuilder sb = new StringBuilder();
for (UriChunk chunk : uriChunks) {
if (chunk instanceof Variable) {
Variable var = (Variable)chunk;
Object value = valuesMap.get(var.getName());
if (value != null) {
String sval = value.toString();
if (!var.matches(sval)) {
throw new IllegalArgumentException("Value '" + sval + "' does not match variable "
+ var.getName() + " with pattern "
+ var.getPattern());
}
if (encodePathSlashVars.contains(var.getName())) {
sval = sval.replaceAll("/", "%2F");
}
sb.append(sval);
} else if (allowUnresolved) {
sb.append(chunk);
} else {
throw new IllegalArgumentException("Template variable " + var.getName()
+ " has no matching value");
}
} else {
sb.append(chunk);
}
}
return sb.toString();
}
/**
* Encoded literal characters surrounding template variables,
* ex. "a {id} b" will be encoded to "a%20{id}%20b"
* @return encoded value
*/
public String encodeLiteralCharacters(boolean isQuery) {
final float encodedRatio = 1.5f;
StringBuilder sb = new StringBuilder((int)(encodedRatio * template.length()));
for (UriChunk chunk : uriChunks) {
String val = chunk.getValue();
if (chunk instanceof Literal) {
sb.append(HttpUtils.encodePartiallyEncoded(val, isQuery));
} else {
sb.append(val);
}
}
return sb.toString();
}
public static URITemplate createTemplate(Path path) {
return createTemplate(path == null ? null : path.value());
}
public static URITemplate createTemplate(String pathValue) {
if (pathValue == null) {
pathValue = "/";
} else if (!pathValue.startsWith("/")) {
pathValue = "/" + pathValue;
}
return createExactTemplate(pathValue);
}
public static URITemplate createExactTemplate(String pathValue) {
URITemplate template = URI_TEMPLATE_CACHE.get(pathValue);
if (template == null) {
template = new URITemplate(pathValue);
if (URI_TEMPLATE_CACHE.size() >= MAX_URI_TEMPLATE_CACHE_SIZE) {
URI_TEMPLATE_CACHE.clear();
}
URI_TEMPLATE_CACHE.put(pathValue, template);
}
return template;
}
public static int compareTemplates(URITemplate t1, URITemplate t2) {
int l1 = t1.getLiteralChars().length();
int l2 = t2.getLiteralChars().length();
// descending order
int result = l1 < l2 ? 1 : l1 > l2 ? -1 : 0;
if (result == 0) {
int g1 = t1.getVariables().size();
int g2 = t2.getVariables().size();
// descending order
result = g1 < g2 ? 1 : g1 > g2 ? -1 : 0;
if (result == 0) {
int gCustom1 = t1.getCustomVariables().size();
int gCustom2 = t2.getCustomVariables().size();
result = gCustom1 < gCustom2 ? 1 : gCustom1 > gCustom2 ? -1 : 0;
if (result == 0) {
result = t1.getPatternValue().compareTo(t2.getPatternValue());
}
}
}
return result;
}
/**
* Stringified part of URI. Chunk is not URI segment; chunk can span over multiple URI segments or one URI
* segments can have multiple chunks. Chunk is used to decompose URI of {@link URITemplate} into literals
* and variables. Example: "foo/bar/{baz}{blah}" is decomposed into chunks: "foo/bar", "{baz}" and
* "{blah}".
*/
private abstract static class UriChunk {
/**
* Creates object form string.
*
* @param uriChunk stringified uri chunk
* @return If param has variable form then {@link Variable} instance is created, otherwise chunk is
* treated as {@link Literal}.
*/
public static UriChunk createUriChunk(String uriChunk) {
if (uriChunk == null || "".equals(uriChunk)) {
throw new IllegalArgumentException("uriChunk is empty");
}
UriChunk uriChunkRepresentation = Variable.create(uriChunk);
if (uriChunkRepresentation == null) {
uriChunkRepresentation = Literal.create(uriChunk);
}
return uriChunkRepresentation;
}
public abstract String getValue();
@Override
public String toString() {
return getValue();
}
}
private static final class Literal extends UriChunk {
private String value;
private Literal() {
// empty constructor
}
public static Literal create(String uriChunk) {
if (uriChunk == null || "".equals(uriChunk)) {
throw new IllegalArgumentException("uriChunk is empty");
}
Literal literal = new Literal();
literal.value = uriChunk;
return literal;
}
@Override
public String getValue() {
return value;
}
}
/**
* Variable of URITemplate. Variable has either "{varname:pattern}" syntax or "{varname}".
*/
private static final class Variable extends UriChunk {
private static final Pattern VARIABLE_PATTERN = Pattern.compile("(\\w[-\\w\\.]*[ ]*)(\\:(.+))?");
private String name;
private Pattern pattern;
private Variable() {
// empty constructor
}
/**
* Creates variable from stringified part of URI.
*
* @param uriChunk uriChunk chunk that depicts variable
* @return Variable if variable was successfully created; null if uriChunk was not a variable
*/
public static Variable create(String uriChunk) {
Variable newVariable = new Variable();
if (uriChunk == null || "".equals(uriChunk)) {
return null;
}
if (CurlyBraceTokenizer.insideBraces(uriChunk)) {
uriChunk = CurlyBraceTokenizer.stripBraces(uriChunk).trim();
Matcher matcher = VARIABLE_PATTERN.matcher(uriChunk);
if (matcher.matches()) {
newVariable.name = matcher.group(1).trim();
if (matcher.group(2) != null && matcher.group(3) != null) {
String patternExpression = matcher.group(3).trim();
newVariable.pattern = Pattern.compile(patternExpression);
}
return newVariable;
}
}
return null;
}
public String getName() {
return name;
}
public String getPattern() {
return pattern != null ? pattern.pattern() : null;
}
/**
* Checks whether value matches variable. If variable has pattern its checked against, otherwise true
* is returned.
*
* @param value value of variable
* @return true if value is valid for variable, false otherwise.
*/
public boolean matches(String value) {
if (pattern == null) {
return true;
}
return pattern.matcher(value).matches();
}
@Override
public String getValue() {
if (pattern != null) {
return "{" + name + ":" + pattern + "}";
}
return "{" + name + "}";
}
}
/**
* Splits string into parts inside and outside curly braces. Nested curly braces are ignored and treated
* as part inside top-level curly braces. Example: string "foo{bar{baz}}blah" is split into three tokens,
* "foo","{bar{baz}}" and "blah". When closed bracket is missing, whole unclosed part is returned as one
* token, e.g.: "foo{bar" is split into "foo" and "{bar". When opening bracket is missing, closing
* bracket is ignored and taken as part of current token e.g.: "foo{bar}baz}blah" is split into "foo",
* "{bar}" and "baz}blah".
* <p>
* This is helper class for {@link URITemplate} that enables recurring literals appearing next to regular
* expressions e.g. "/foo/{zipcode:[0-9]{5}}/". Nested expressions with closed sections, like open-closed
* brackets causes expression to be out of regular grammar (is context-free grammar) which are not
* supported by Java regexp version.
*/
static class CurlyBraceTokenizer {
private List<String> tokens = new ArrayList<>();
private int tokenIdx;
CurlyBraceTokenizer(String string) {
boolean outside = true;
int level = 0;
int lastIdx = 0;
int idx;
for (idx = 0; idx < string.length(); idx++) {
if (string.charAt(idx) == '{') {
if (outside) {
if (lastIdx < idx) {
tokens.add(string.substring(lastIdx, idx));
}
lastIdx = idx;
outside = false;
} else {
level++;
}
} else if (string.charAt(idx) == '}' && !outside) {
if (level > 0) {
level--;
} else {
if (lastIdx < idx) {
tokens.add(string.substring(lastIdx, idx + 1));
}
lastIdx = idx + 1;
outside = true;
}
}
}
if (lastIdx < idx) {
tokens.add(string.substring(lastIdx, idx));
}
}
/**
* Token is enclosed by curly braces.
*
* @param token
* text to verify
* @return true if enclosed, false otherwise.
*/
public static boolean insideBraces(String token) {
return token.charAt(0) == '{' && token.charAt(token.length() - 1) == '}';
}
/**
* Strips token from enclosed curly braces. If token is not enclosed method
* has no side effect.
*
* @param token
* text to verify
* @return text stripped from curly brace begin-end pair.
*/
public static String stripBraces(String token) {
if (insideBraces(token)) {
return token.substring(1, token.length() - 1);
}
return token;
}
public boolean hasNext() {
return tokens.size() > tokenIdx;
}
public String next() {
if (hasNext()) {
return tokens.get(tokenIdx++);
}
throw new IllegalStateException("no more elements");
}
}
}