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

org.apache.wink.common.internal.uritemplate.UriTemplateProcessor Maven / Gradle / Ivy

The 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.wink.common.internal.uritemplate;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MultivaluedMap;

import org.apache.wink.common.http.HttpStatus;
import org.apache.wink.common.internal.MultivaluedMapImpl;
import org.apache.wink.common.internal.i18n.Messages;
import org.apache.wink.common.internal.uri.UriEncoder;
import org.apache.wink.common.internal.utils.UriHelper;

/**
 * Abstract URI template processor for compiling, matching and expanding URI
 * templates
 */
public abstract class UriTemplateProcessor implements Comparable, Cloneable {

    protected static final String                    TEMPLATE_TAIL_NAME    = "wink.TemplateTail"; //$NON-NLS-1$
    protected static final String                    TEMPLATE_HEAD_NAME    = "wink.TemplateHead"; //$NON-NLS-1$
    protected static final String                    TEMPLATE_TAIL_PATTERN = "(/.*)?"; //$NON-NLS-1$

    protected String                                 template;
    protected Pattern                                pattern;
    protected MultivaluedMap variables;
    protected List                  expanders;
    protected int                                    numOfLiteralCharacters;

    // this is used to hold the string that matches the complete original
    // template as a single
    // capturing group
    protected CapturingGroup                         head;

    // this is used to hold the tail of the matched strings,
    // that is, the trailing part of the matched uri that does not fall into the
    // original
    // the uri template
    protected CapturingGroup                         tail;

    protected UriTemplateProcessor() {
        template = null;
        pattern = null;
        tail = null;
        head = null;
        variables = new MultivaluedMapImpl();
        expanders = new ArrayList();
        numOfLiteralCharacters = 0;
    }

    protected void reset() {
        variables.clear();
        expanders.clear();
        pattern = null;
        tail = null;
        head = null;
        numOfLiteralCharacters = 0;
    }

    /**
     * Get the template that is associated with this processor
     * 
     * @return the processor template
     */
    public String getTemplate() {
        return template;
    }

    /**
     * Get the regular expression pattern that this processor uses to match and
     * expand uri's. The regular expression is the result of compiling the uri
     * template that is associated with this processor.
     * 
     * @return the regular expression that this processor compiled
     */
    public String getPatternString() {
        assertPatternState();
        return pattern.pattern();
    }

    /* package */Pattern getPattern() {
        return pattern;
    }

    /* package */MultivaluedMap getVariables() {
        return variables;
    }

    /**
     * Get a set of the variable names that exist in this processor's template.
     * The order of the variables in the set is the same as it appears in the
     * template.
     * 
     * @return a set of variable names as they appear in the template, and in
     *         the order in which they appear
     * @throws IllegalStateException if the pattern was not compiled
     *             successfully
     */
    public Set getVariableNames() throws IllegalStateException {
        assertPatternState();
        Set set = new LinkedHashSet(variables.keySet());
        return set;
    }

    /**
     * Get a new instance of a {@link UriTemplateMatcher} that can be used to
     * perform matching and matched variables values retrieval
     * 
     * @return a new instance of UriTemplateMatcher
     */
    public UriTemplateMatcher matcher() {
        assertPatternState();
        return new UriTemplateMatcher(this);
    }

    /**
     * Set the template of this processor to the specified template. Only after
     * calling this method can the {@link #matcher()} method be called.
     * 
     * @param template the template to compile and set
     */
    public abstract void compile(String template);

    /**
     * Expands the template into a URI. Same as calling
     * expand(values, true)
     * 
     * @param variables map from variable names to variable values
     * @return the URI instance
     */
    public String expand(Map values) {
        return expand(values, true);
    }

    /**
     * Expands the template into a URI.
     * 

* If the value is an array or a List<? extends Object>, * then all objects are converted to strings using the toString() method, * otherwise the value itself is converted to string using the toString() * method. * * @param variables map from variable names to variable values * @param encode indicates whether to encode the values before expansion * @return the expanded URI string */ public String expand(Map values, boolean encode) { MultivaluedMap mValues = MultivaluedMapImpl.toMultivaluedMapString(values); return expand(mValues, encode); } /** * Expands the template into a URI. Same as calling * expand(values, true) * * @param variables map from variable names to variable values * @return the URI instance */ public String expand(MultivaluedMap values) { return expand(values, true); } /** * Expand the template of this processor using the provided values. Regular * expressions of variables in the template are ignored. All variables * defined in the template must have a value. * * @param values a map with the values of the variables * @return an expanded uri using the supplied variable values */ public String expand(MultivaluedMap values, boolean encode) { if (values == null) { values = new MultivaluedMapImpl(); } assertPatternState(); StringBuilder result = new StringBuilder(); // go over all of the expanders and invoke the expand() method for (TemplateElement expander : expanders) { expander.expand(values, encode, result); } String resultStr = result.toString(); return resultStr; } // @Override // public boolean equals(Object obj) { // return obj instanceof UriTemplateProcessor && template // .equals(((UriTemplateProcessor)obj).template); // } @Override public String toString() { return template; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((template == null) ? 0 : template.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; UriTemplateProcessor other = (UriTemplateProcessor)obj; if (template == null) { if (other.template != null) return false; } else if (!template.equals(other.template)) return false; return true; } @Override public UriTemplateProcessor clone() { try { UriTemplateProcessor ret = (UriTemplateProcessor)super.clone(); return ret; } catch (CloneNotSupportedException e) { // can't happen throw new WebApplicationException(HttpStatus.INTERNAL_SERVER_ERROR.getCode()); } } public int compareTo(UriTemplateProcessor other) { int ret = compareLiteralCharacters(other); if (ret != 0) { return ret; } return compareNumOfVariables(other); } private int compareNumOfVariables(UriTemplateProcessor other) { return (variables.size() - other.variables.size()); } private int compareLiteralCharacters(UriTemplateProcessor other) { return (numOfLiteralCharacters - other.numOfLiteralCharacters); } protected void assertPatternState() { if (pattern == null) { throw new IllegalStateException(Messages.getMessage("patternNotCompiled")); //$NON-NLS-1$ } } /** * Normalizes the URI to a standard form. This includes for example handling * null, removing slashes, unnecessary segments, ... * * @param uri the input template; null is allowed * @return the transformed version of the URI */ public static String normalizeUri(String uri) { String normalizedUri; if (uri != null) { normalizedUri = UriHelper.normalize(uri); } else { normalizedUri = ""; //$NON-NLS-1$ } if (normalizedUri.startsWith("/")) { //$NON-NLS-1$ normalizedUri = normalizedUri.substring(1); } return normalizedUri; } /** * Factory method for normalized uri-templates. * * @param uriTemplate uri-template specification * @return instance representing (normalized) uri-template * @see UriTemplateProcessor#normalizeUri(String) */ public static UriTemplateProcessor newNormalizedInstance(String uriTemplate) { // try { return JaxRsUriTemplateProcessor.newNormalizedInstance(uriTemplate); // } catch (IllegalArgumentException e) { // // fallback to handle BitWorking style templates // return BitWorkingUriTemplateProcessor.newNormalizedInstance(uriTemplate); // } } /** * Check if the path parameter match the template * * @param template * @param path * @return */ public static boolean match(String template, String path) { UriTemplateProcessor processor = UriTemplateProcessor.newNormalizedInstance(template); UriTemplateMatcher matcher = processor.matcher(); return matcher.matches(path); } /** * Represents a basic element of a template. A template consists of literals * and variables. */ protected static interface TemplateElement { /** * Called during the compilation of a template to build a regular * expression pattern corresponding to this element * * @param builder the output StringBuilder */ public void build(StringBuilder builder); /** * Called during the expansion of a template to expand this template * into the output StringBuilder. * * @param values a multivalued map of values to use for the expansion * @param encode indicates whether to uri-encode the value before * writing it to the output * @param builder the output StringBuilder */ public void expand(MultivaluedMap values, boolean encode, StringBuilder builder); } /** * Represents a literal element of the template. A literal is a sequence of * characters in the template without any variables in it. For instance * "/foo/" is a literal in the template "/foo/{var}". */ protected static class Literal implements TemplateElement { private String literal; public Literal(String literal) { assertValid(literal); this.literal = literal; } public void build(StringBuilder builder) { builder.append(Pattern.quote(literal)); } private static void assertValid(String literal) { // assert that the literal does not contain curly brackets if (literal.indexOf('{') != -1 || literal.indexOf('}') != -1) { throw new IllegalArgumentException(Messages.getMessage("variableNotSuppliedAValue", literal)); //$NON-NLS-1$ } } public void expand(MultivaluedMap values, boolean encode, StringBuilder builder) { String literal = this.literal; if (!encode) { literal = UriEncoder.decodeString(literal); } builder.append(literal); } } /** * Represents a single capturing group in the pattern created from a * template uri. */ protected abstract static class CapturingGroup implements TemplateElement { protected static final String REGEX0 = "[^/]*?"; //$NON-NLS-1$ protected static final String REGEX1 = "[^/]+?"; //$NON-NLS-1$ protected int capturingGroupId = -1; /** * Get the pattern capturing group id that is associated with this * variable * * @return capturing group id */ public int getCapturingGroupId() { return capturingGroupId; } /** * Set the pattern capturing group id that is associated with this * variable * * @param capturingGroupId capturing group id */ public void setCapturingGroupId(int capturingGroupId) { this.capturingGroupId = capturingGroupId; } /** * Called during the retrieval of the value(s) of this template variable * from a matched uri * * @param matched the string that was a match to the pattern of this * variable * @param values the output multivalued map to put the values into * @param startIndex the start index of the matched string in the input * uri * @param indices the output multivalued map to put the index into */ public abstract void onMatch(String matched, MultivaluedMap values, int startIndex, MultivaluedMap indices); } /** * Represents a single variable in a template */ protected static class Variable extends CapturingGroup { // variable name protected String name; // variable regular expression protected String regex; // variable default value protected String defaultValue; public Variable(String name, String regex, String defaultValue) { super(); this.name = name; this.defaultValue = defaultValue; if (regex == null) { this.regex = REGEX1; } else { this.regex = regex; } } public String getName() { return name; } public String getRegex() { return regex; } public String getDefaultValue() { return defaultValue; } public void build(StringBuilder builder) { // we don't want any regex supplied by the user to contain any // capturing groups // that can interfere with the capturing groups of the regex that we // are building, // so we convert all the capturing groups into non-capturing groups regex = convertAllGroupsToNonCapturing(regex); builder.append("("); //$NON-NLS-1$ builder.append(regex); builder.append(")"); //$NON-NLS-1$ } @Override public void onMatch(String matched, MultivaluedMap values, int startIndex, MultivaluedMap indices) { values.add(name, matched); indices.add(name, startIndex); } public void expand(MultivaluedMap values, boolean encode, StringBuilder builder) { String value = values.getFirst(name); if (value == null) { value = defaultValue; } if (value == null) { throw new IllegalArgumentException(Messages.getMessage("syntaxErrorInvalidTemplateForm", name)); //$NON-NLS-1$ } if (encode) { value = UriEncoder.encodeString(value); } builder.append(value); } /* * converts all the capturing groups in the provided regex into * non-capturing groups. e.g.: 1. "a(b)*c" is converted to "a(?:b)*c" 2. * "(a+)(?:b+)" is converted to "(?:a+)(?:b+)" */ private String convertAllGroupsToNonCapturing(String regex) { StringBuffer sb = new StringBuffer(); int regexLen = regex.length(); for (int i = 0; i < regexLen; ++i) { char ch = regex.charAt(i); sb.append(ch); // if the char is '(', and the char after it is not a '?', // then this is a capturing group, so replace it with // a non-capturing group if ((ch == '(') && (i + 1 < regexLen) && (regex.charAt(i + 1) != '?')) { sb.append("?:"); //$NON-NLS-1$ } } return sb.toString(); } } protected static interface BaseCompilationHandler { /** * Start of compilation event * * @param template the uri template that is being compiled */ public void startCompile(String template); /** * Literal part event. The literal part is all the characters from the * the previous variable (or the start of the uri template if there was * no variable), up to the next variable found (or the end of the uri * template if none exist). *

* E.g. if the template is "{var1}/foo/{var2}/goo" then there will be * two literal events fired, one for "/foo/" and one for "/goo". * * @param literal the literal part */ public void literal(String literal); /** * End of compilation event. Indicates that the template contains no * more variables. * * @param literal the last literal part of the template after the last * variable that was found */ public void endCompile(String literal); } /** * Abstract base class for concrete pattern builders */ protected static abstract class AbstractPatternBuilder implements BaseCompilationHandler { // the processor whose uri template is being compiled and set protected UriTemplateProcessor processor; // holds the regex template string that will be compiled at the end of // the compilation protected StringBuilder patternBuilder; // counter to track the regex pattern capturing group id protected int capturingGroupId; protected AbstractPatternBuilder(UriTemplateProcessor processor) { this.processor = processor; } public void startCompile(String template) { processor.template = template; processor.reset(); patternBuilder = new StringBuilder(template.length()); capturingGroupId = 0; openHead(); } public void literal(String literal) { int length = literal.length(); if (length == 0) { return; } // create a new literal element and save it for use during expansion Literal element = new Literal(literal); element.build(patternBuilder); // save it for use during expansion processor.expanders.add(element); processor.numOfLiteralCharacters += length; } public void endCompile(String literal) { literal(removeTrailingSlash(literal)); closeHead(); createTail(); // compile and save the regex pattern String templatePatternStr = patternBuilder.toString(); processor.pattern = Pattern.compile(templatePatternStr); } // remove the trailing '/' from the literal if it exists protected String removeTrailingSlash(String literal) { if (literal.length() > 0 && literal.charAt(literal.length() - 1) == '/') { literal = literal.substring(0, literal.length() - 1); } return literal; } // Open the head capturing group protected void openHead() { patternBuilder.append('('); // Create new Variable that holds the head processor.head = new Variable(TEMPLATE_HEAD_NAME, null, null); ++capturingGroupId; processor.head.setCapturingGroupId(capturingGroupId); } // Close the head capturing group protected void closeHead() { patternBuilder.append(')'); } // add the tail part of the template pattern as defined by the JAX-RS // spec protected void createTail() { processor.tail = createVariable(TEMPLATE_TAIL_NAME, TEMPLATE_TAIL_PATTERN, null); } /** * Create a new template variable * * @param name name of the variable * @param regex the regular expression of the variable * @param defaultValue the default value of the variable * @return */ protected CapturingGroup createVariable(String name, String regex, String defaultValue) { CapturingGroup variable = new Variable(name, regex, defaultValue); // build it into the pattern variable.build(patternBuilder); // set its capturing group ++capturingGroupId; variable.setCapturingGroupId(capturingGroupId); return variable; } } /** * Abstract base class for concrete template expanders */ protected abstract static class AbstractTemplateExpander implements BaseCompilationHandler { protected MultivaluedMap values; protected StringBuilder out; public AbstractTemplateExpander(MultivaluedMap values, StringBuilder out) { this.values = values; this.out = out; } public void startCompile(String template) { if (values == null) { values = new MultivaluedMapImpl(); } } public void literal(String literal) { out.append(literal); } public void endCompile(String literal) { literal(literal); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy