| /** |
| * 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"); |
| } |
| } |
| } |
| |
| |