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

org.apache.cxf.jaxrs.model.URITemplate Maven / Gradle / Ivy

There is a newer version: 2.7.18
Show newest version
/**
 * 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.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.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 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 final String template;
    private final List variables = new ArrayList();
    private final List customVariables = new ArrayList();
    private final Pattern templateRegexPattern;
    private final String literals;
    private final List 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());
                if (var.getPattern() != null) {
                    customVariables.add(var.getName());
                    patternBuilder.append('(');
                    patternBuilder.append(var.getPattern());
                    patternBuilder.append(')');
                } else {
                    patternBuilder.append(DEFAULT_PATH_VARIABLE_REGEX);
                }
            }
        }
        literals = literalChars.toString();

        int endPos = patternBuilder.length() - 1;
        boolean endsWithSlash = (endPos >= 0) ? patternBuilder.charAt(endPos) == '/' : false;
        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 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 getCustomVariables() {
        return Collections.unmodifiableList(customVariables);
    }

    private static String escapeCharacters(String expression) {

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < expression.length(); i++) {
            char ch = expression.charAt(i);
            sb.append(isReservedCharacter(ch) ? "\\" + ch : ch);
        }
        return sb.toString();
    }

    private static boolean isReservedCharacter(char ch) {
        return CHARACTERS_TO_ESCAPE.indexOf(ch) != -1;
    }

    public boolean match(String uri, MultivaluedMap 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 pList = JAXRSUtils.getPathSegments(template, false);
                List uList = JAXRSUtils.getPathSegments(uri, false);
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < uList.size(); i++) {
                    String segment = null;
                    if (pList.size() > i && pList.get(i).getPath().indexOf('{') == -1) {
                        segment = uList.get(i).getPath();
                    } else {
                        segment = HttpUtils.fromPathSegment(uList.get(i));
                    }
                    if (segment.length() > 0) {
                        sb.append(SLASH);    
                    }
                    sb.append(segment);
                }
                uri = sb.toString();
                if (uri.length() == 0) {
                    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) {
                    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 = 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.
     * 

* 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". *

* 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 values) throws IllegalArgumentException { if (values == null) { throw new IllegalArgumentException("values is null"); } Iterator 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(); } /** * 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. *

* 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 valuesMap) 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()); } sb.append(value); } 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) { return new URITemplate("/"); } if (!pathValue.startsWith("/")) { pathValue = "/" + pathValue; } return new URITemplate(pathValue); } public static int compareTemplates(URITemplate t1, URITemplate t2) { String l1 = t1.getLiteralChars(); String l2 = t2.getLiteralChars(); if (!l1.equals(l2)) { // descending order return l1.length() < l2.length() ? 1 : -1; } int g1 = t1.getVariables().size(); int g2 = t2.getVariables().size(); // descending order int result = g1 < g2 ? 1 : g1 > g2 ? -1 : 0; if (result == 0) { int gCustom1 = t1.getCustomVariables().size(); int gCustom2 = t2.getCustomVariables().size(); if (gCustom1 != gCustom2) { // descending order return gCustom1 < gCustom2 ? 1 : -1; } } 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; } else { return pattern.matcher(value).matches(); } } @Override public String getValue() { if (pattern != null) { return "{" + name + ":" + pattern + "}"; } else { 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". *

* 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 tokens = new ArrayList(); private int tokenIdx; public 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); } else { return token; } } public boolean hasNext() { return tokens.size() > tokenIdx; } public String next() { if (hasNext()) { return tokens.get(tokenIdx++); } else { throw new IllegalStateException("no more elements"); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy