/** * */ package org.minnal.utils.route; import static org.minnal.utils.http.HttpUtil.decode; 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 com.google.common.base.Splitter; /** * Defines the pattern of a path. Incoming requests will be matched against the route pattern to determine the route. * <p/> * * Route pattern can contain optional parameter names that will be mapped against the request path. The parameters * should be specified within flower brackets like <code> {order_id}</code> and should be unique for a route. * Few examples on valid route patterns, * * <pre> * /orders/{order_id}/order_items * /orders/{order_id}/order_items/{order_item_id} * /orders/1/order_items/1 * /orders * <pre> * * @author ganeshs * */ public class RoutePattern { private static final String PLACEHOLDER_REGEX = "[^/]+?"; private static final String PLACEHOLDER_PATTERN_REGEX = "\\{" + PLACEHOLDER_REGEX + "\\}"; private static final String PATH_PATTERN_REGEX = "(\\/[a-z0-9\\-_\\.A-Z]*(" + PLACEHOLDER_PATTERN_REGEX + ")*)+"; private static final Pattern PATH_PATTERN = Pattern.compile(PATH_PATTERN_REGEX); private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile(PLACEHOLDER_PATTERN_REGEX); private String pathPattern; private Pattern regex; private List<String> parameterNames = new ArrayList<String>(); /** * Constructs a route pattern. If the route pattern is not valid throws an IllegalArgumentException * * @param pathPattern */ public RoutePattern(String pathPattern) { this.pathPattern = pathPattern; validate(); compile(); } /** * Checks if the route pattern is valid */ protected void validate() { if (! PATH_PATTERN.matcher(pathPattern).matches()) { throw new IllegalArgumentException("Invalid pattern - " + pathPattern + ". Doesn't match the regex - " + PATH_PATTERN_REGEX); } } /** * Compiles the route pattern, retrieves the parameter names from the path. */ protected void compile() { Matcher matcher = PLACEHOLDER_PATTERN.matcher(pathPattern); while(matcher.find()) { String param = matcher.group().replaceAll("\\{|\\}", ""); if (parameterNames.contains(param)) { throw new IllegalArgumentException("Duplicate parameter name - " + param + " detected for the path " + pathPattern); } parameterNames.add(matcher.group().replaceAll("\\{|\\}", "")); } regex = Pattern.compile(matcher.replaceAll("(" + PLACEHOLDER_REGEX + ")")); } /** * Matches the given path with the pattern and returns the path parameter map. If the pattern doesn't have parameters, * returns back an empty map. If the path doesn't match the pattern returns null value. * * @param path the path to be matched with this pattern * @return */ public Map<String, String> match(String path) { Map<String, String> params = new HashMap<String, String>(); Matcher matcher = regex.matcher(path); if (!matcher.matches()) { return null; } for (int i = 1; i <= matcher.groupCount(); i++) { params.put(parameterNames.get(i-1), decode(matcher.group(i))); } return params; } /** * Checks if the given path matches the pattern * * @param path * @return */ public boolean matches(String path) { return regex.matcher(path).matches(); } /** * @return the parameterNames */ public List<String> getParameterNames() { return parameterNames; } /** * @return the pathPattern */ public String getPathPattern() { return pathPattern; } public boolean isExactMatch() { return parameterNames.isEmpty(); } /** * Splits the pattern by '/' into route elements * * @return the elements */ public List<RouteElement> getElements() { List<RouteElement> elements = new ArrayList<RoutePattern.RouteElement>(); for (String element : Splitter.on("/").split(pathPattern)) { elements.add(new RouteElement(element, PLACEHOLDER_PATTERN.matcher(element).matches())); } if (elements.isEmpty()) { System.out.println(""); } elements.remove(0); // Remove the root return elements; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((regex == null) ? 0 : regex.pattern().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; RoutePattern other = (RoutePattern) obj; if (regex == null) { if (other.regex != null) return false; } else if (!regex.pattern().equals(other.regex.pattern())) return false; return true; } /** * Multiple route elements separated by a '/' consitute a route pattern * * @author ganeshs * */ public static class RouteElement { private String name; private boolean parameter; public RouteElement(String name) { this(name, false); } /** * @param name */ public RouteElement(String name, boolean parameter) { this.name = name; this.parameter = parameter; } /** * @return the name */ public String getName() { return name; } public boolean isParameter() { return parameter; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((name == null) ? 0 : name.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; RouteElement other = (RouteElement) obj; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; return true; } @Override public String toString() { return "RouteElement [name=" + name + ", parameter=" + parameter + "]"; } } }