/** * Copyright 2005-2014 Restlet * * The contents of this file are subject to the terms of one of the following * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can * select the license that you prefer but you may not use this file except in * compliance with one of these Licenses. * * You can obtain a copy of the Apache 2.0 license at * http://www.opensource.org/licenses/apache-2.0 * * You can obtain a copy of the EPL 1.0 license at * http://www.opensource.org/licenses/eclipse-1.0 * * See the Licenses for the specific language governing permissions and * limitations under the Licenses. * * Alternatively, you can obtain a royalty free commercial license with less * limitations, transferable or non-transferable, directly at * http://restlet.com/products/restlet-framework * * Restlet is a registered trademark of Restlet S.A.S. */ package org.restlet.ext.jaxrs.internal.util; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.ws.rs.Path; import org.restlet.engine.util.SystemUtils; import org.restlet.ext.jaxrs.internal.exceptions.IllegalPathException; import org.restlet.ext.jaxrs.internal.exceptions.IllegalPathOnClassException; import org.restlet.ext.jaxrs.internal.exceptions.IllegalPathOnMethodException; import org.restlet.ext.jaxrs.internal.exceptions.MissingAnnotationException; /** * Wraps a regular expression of a @{@link Path}. Instances are immutable. * The regular expression has no '/' t the start. * * @author Stephan Koops */ public class PathRegExp { /** * Default regular expression to use, if no reg exp is given (matches every * character expect '/'). * * @see #defaultRegExp */ private static final String DEFAULT_REG_EXP = "[^/]+?"; /** * The PathRegExp with an empty path. */ public static final PathRegExp EMPTY; private static final byte NAME_READ = 2; private static final byte NAME_READ_READY = 3; private static final byte NAME_READ_START = 1; private static final byte REGEXP_READ = 12; private static final byte REGEXP_READ_READY = 13; private static final byte REGEXP_READ_START = 11; static { try { EMPTY = new PathRegExp("", null); } catch (IllegalPathException e) { throw new RuntimeException("This could not occur", e); } } /** * Creates a {@link PathRegExp} for a root resource class. * * @param rrc * the JAX-RS root resource class * @return the PathRegExp from the given root resource class * @throws MissingAnnotationException * if the {@link Path} annotation is missing. * @throws IllegalPathOnClassException * if the {@link Path} annotation is not valid. * @throws IllegalArgumentException * if the rrc is null. * @see {@link #EMPTY} */ public static PathRegExp createForClass(Class<?> rrc) throws MissingAnnotationException, IllegalPathOnClassException, IllegalArgumentException { final Path path = Util.getPathAnnotation(rrc); try { return new PathRegExp(path.value(), path); } catch (IllegalPathException ipe) { throw new IllegalPathOnClassException(ipe); } } /** * Creates a {@link PathRegExp} for a sub resource method or sub resource * locator. Returns {@link #EMPTY}, if the method is not annotated with * @Path. * * @param annotatedMethod * @return the {@link PathRegExp}. Never returns null. * @throws IllegalPathOnMethodException * if the annotation on the method is invalid. * @throws IllegalArgumentException * if the method is null. */ public static PathRegExp createForMethod(Method annotatedMethod) throws IllegalPathOnMethodException, IllegalArgumentException { final Path pathAnno = Util.getPathAnnotationOrNull(annotatedMethod); if (pathAnno == null) { return EMPTY; } try { return new PathRegExp(pathAnno.value(), pathAnno); } catch (IllegalPathException ipe) { throw new IllegalPathOnMethodException(ipe); } } private final boolean emptyOrSlash; /** Contains the number of literal chars in this Regular Expression */ private final Integer noLitChars; /** * Contains the number of capturing groups with regular expressions that are * not the default. * * @see #DEFAULT_REG_EXP */ private int noNonDefaultRegExp = 0; private final int noOfCapturingGroups; private final String pathTemplateDec; private final String pathTemplateEnc; private final Pattern pattern; private final List<String> varNames = new ArrayList<String>(); /** * Is intended for internal use and testing. Otherwise use the static * methods {@link #createForClass(Class)} or * {@link #createForMethod(Method)}, or the constant {@link #EMPTY}. * * @param pathPattern * @param pathForExcMess */ private PathRegExp(String pathTemplate, Path pathForExcMess) throws IllegalPathException { // 1. URI encode the template, ignoring URI template variable specs. // 2. Escape any regular expression characters in the URI template, // again ignoring URI template variable specifications. // 3. Replace each URI template variable with a capturing group // containing the specified regular expression or "([^/]+?)" if no // regular expression is specified. // LATER regexp in @Path: @Path("{p}/abc/{p}") is allowed, p may be != p if (pathTemplate == null) { throw new IllegalArgumentException( "The path template must not be null"); } final int l = pathTemplate.length(); final StringBuilder pathPattern = new StringBuilder(); int forStart = 0; if (l > 0 && pathTemplate.charAt(0) == '/') forStart = 1; int noLitChars = 0; int numberOfCapturingGroups = 0; for (int i = forStart; i < l; i++) { final char c = pathTemplate.charAt(i); if (c == '{') { i = processTemplVarname(pathTemplate, i, pathPattern, pathForExcMess); numberOfCapturingGroups++; } else if (c == '%') { try { EncodeOrCheck.processPercent(i, true, pathTemplate, pathPattern); } catch (IllegalArgumentException e) { throw new IllegalPathException(pathForExcMess, e); } } else if (c == '}') { throw new IllegalPathException(pathForExcMess, "'}' is only allowed as " + "end of a variable name in \"" + pathTemplate + "\""); } else if (c == ';') { throw new IllegalPathException(pathForExcMess, "matrix parameters are not allowed in a @Path"); } else if (!false && (c == '/')) { pathPattern.append(c); noLitChars++; } else { noLitChars += EncodeOrCheck.encode(c, pathPattern); } } this.noLitChars = noLitChars; this.noOfCapturingGroups = numberOfCapturingGroups; // 4. If the resulting string ends with "/" then remove the final char. // 5. Append "(/.*)?" to the result. if (pathPattern.length() > 0 && pathPattern.charAt(pathPattern.length() - 1) != '/') { pathPattern.append('/'); } pathPattern.append("(.*)"); this.pattern = Pattern.compile(pathPattern.toString()); this.emptyOrSlash = Util.isEmptyOrSlash(pathTemplate); if (l > 0) { if (pathTemplate.charAt(0) != '/') { pathTemplate = '/' + pathTemplate; } if (l > 1 && pathTemplate.endsWith("/")) { pathTemplate = pathTemplate.substring(0, pathTemplate.length() - 2); } } this.pathTemplateEnc = pathTemplate; // LATER encode unencoded here this.pathTemplateDec = pathTemplate; // LATER decode here } /** * Compares this regular expression of a @{@link Path} with the given * Object by comparing given patterns. */ @Override public boolean equals(Object anotherObject) { if (this == anotherObject) { return true; } if (!(anotherObject instanceof PathRegExp)) { return false; } final PathRegExp otherRegExp = (PathRegExp) anotherObject; return this.pattern.pattern().equals(otherRegExp.pattern.pattern()); } /** * @return the number of capturing groups with regular expressions that are * not the default. */ public int getNoNonDefCaprGroups() { return this.noNonDefaultRegExp; } /** * @return Returns the number of capturing groups. */ public int getNoOfCapturingGroups() { return this.noOfCapturingGroups; } /** * See Footnode to JSR-311-Spec, Section 2.6, Algorithm, Part 1e * * @return Returns the number of literal chars in the path patern */ public int getNoOfLiteralChars() { return this.noLitChars; } /** * @return the decoded path template with a '/' at the beginning, and no one * at the end. */ public String getPathTemplateDec() { return this.pathTemplateDec; } /** * @return the encoded path template with a '/' at the beginning, and no one * at the end. */ public String getPathTemplateEnc() { return this.pathTemplateEnc; } @Override public int hashCode() { return SystemUtils.hashCode(this.pattern); } /** * Checks if the URI template is empty or only a slash. * * @return if this path regular expression is empty or "/" */ public boolean isEmptyOrSlash() { return this.emptyOrSlash; } /** * Checks if this regular expression matches the given remaining path. * * @param remainingPath * @return Returns an MatchingResult, if the remainingPath matches to this * template, or null, if not. */ public MatchingResult match(RemainingPath remainingPath) { String givenPath = remainingPath.getWithoutParams(); Matcher matcher = pattern.matcher(givenPath); if (!matcher.matches()) { return null; } final Map<String, String> templateVars = new HashMap<String, String>(); for (int i = 1; i < matcher.groupCount(); i++) { templateVars.put(this.varNames.get(i - 1), matcher.group(i)); } String finalCapturingGroup = matcher.group(matcher.groupCount()); // if (finalCapturingGroup.length() > 0) { // if (pathSuppl && finalCapturingGroup.endsWith("/")) { // finalCapturingGroup = finalCapturingGroup.substring(0, // finalCapturingGroup.length() - 1); // } // if (!finalCapturingGroup.startsWith("/")) { // finalCapturingGroup = "/" + finalCapturingGroup; // } // } int matchedChars = givenPath.length() - finalCapturingGroup.length(); if ((matchedChars > 0) && (givenPath.charAt(matchedChars - 1) == '/')) { matchedChars--; // ignore '/' at end } final String matchedPart = givenPath.substring(0, matchedChars); return new MatchingResult(matchedPart, templateVars, finalCapturingGroup); } /** * Checks, if this regular expression matches the given path with no final * matching group. * * @param remainingPath * @return true, if this regular expression matches exectly the given path, * without a final capturing group. */ public boolean matchesWithEmpty(RemainingPath remainingPath) { final MatchingResult matchingResult = match(remainingPath); if (matchingResult == null) { return false; } return matchingResult.getFinalCapturingGroup().isEmptyOrSlash(); } /** * @param pathTemplate * @param braceIndex * @param pathPattern * @param pathForExcMess * @throws IllegalPathException */ private int processTemplVarname(final String pathTemplate, final int braceIndex, final StringBuilder pathPattern, final Path pathForExcMess) throws IllegalPathException { pathPattern.append('('); final int l = pathTemplate.length(); final StringBuilder varName = new StringBuilder(); final StringBuilder regExp = new StringBuilder(); int state = NAME_READ_START; for (int i = braceIndex + 1; i < l; i++) { final char c = pathTemplate.charAt(i); if (c == '{') { throw new IllegalPathException(pathForExcMess, "A variable must not " + "contain an extra '{' in \"" + pathTemplate + "\""); } else if (c == ' ' || c == '\t') { if (state == NAME_READ) state = NAME_READ_READY; else if (state == REGEXP_READ) state = REGEXP_READ_READY; continue; } else if (c == ':') { if (state == NAME_READ_START) { throw new IllegalPathException(pathForExcMess, "The variable name at position must not be null at " + braceIndex + " of \"" + pathTemplate + "\""); } if (state == NAME_READ || state == NAME_READ_READY) { state = REGEXP_READ_START; } continue; } else if (c == '}') { if (state == NAME_READ_START) { throw new IllegalPathException(pathForExcMess, "The template variable name '{}' is not allowed in " + "\"" + pathTemplate + "\""); } else if ((state == REGEXP_READ) || (state == REGEXP_READ_READY)) { pathPattern.append(regExp); if (!regExp.equals(DEFAULT_REG_EXP)) { this.noNonDefaultRegExp++; } } else { pathPattern.append(DEFAULT_REG_EXP); } pathPattern.append(')'); this.varNames.add(varName.toString()); return i; } if (state == NAME_READ_START) { state = NAME_READ; varName.append(c); } else if (state == NAME_READ) { varName.append(c); } else if (state == REGEXP_READ_START) { state = REGEXP_READ; regExp.append(c); } else if (state == REGEXP_READ) { regExp.append(c); } else { throw new IllegalPathException(pathForExcMess, "Invalid character found at position " + i + " of \"" + pathTemplate + "\""); } } throw new IllegalPathException(pathForExcMess, "No '}' found after '{' " + "at position " + braceIndex + " of \"" + pathTemplate + "\""); } @Override public String toString() { return this.pattern.pattern(); } }