/* * Copyright 2011- Per Wendel * * Licensed 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 spark.route; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import spark.route.HttpMethod; import spark.route.RouteMatch; import spark.route.SimpleRouteMatcher; import spark.utils.MimeParse; import spark.utils.SparkUtils; import com.google.common.base.Throwables; import com.google.common.collect.Sets; /** * Simple route matcher that is supposed to work exactly as Sinatra's * * @author Per Wendel */ public class SimpleRouteMatcher implements RouteMatcher { private List<RouteEntry> routes; private static class RouteEntry { private HttpMethod httpMethod; private String path; private String acceptedType; private Object target; private boolean matches(HttpMethod httpMethod, String path) { if((httpMethod == HttpMethod.before || httpMethod == HttpMethod.after) && (this.httpMethod == httpMethod) && this.path.equals(SparkUtils.ALL_PATHS)) { // Is filter and matches all return true; } boolean match = false; if(this.httpMethod == httpMethod) { match = matchPath(path); } return match; } private boolean matchPath(String path) { // NOSONAR if(!this.path.endsWith("*") && ((path.endsWith("/") && !this.path.endsWith("/")) // NOSONAR || (this.path.endsWith("/") && !path.endsWith("/")))) { // One and not both ends with slash return false; } if(this.path.equals(path)) { // Paths are the same return true; } // check params List<String> thisPathList = SparkUtils .convertRouteToList(this.path); List<String> pathList = SparkUtils.convertRouteToList(path); int thisPathSize = thisPathList.size(); int pathSize = pathList.size(); if(thisPathSize == pathSize) { for (int i = 0; i < thisPathSize; i++) { String thisPathPart = thisPathList.get(i); String pathPart = pathList.get(i); if((i == thisPathSize - 1) && (thisPathPart.equals("*") && this.path .endsWith("*"))) { // wildcard match return true; } if((!thisPathPart.startsWith(":")) && !thisPathPart.equals(pathPart) && !thisPathPart.equals("*")) { return false; } } // All parts matched return true; } else { // Number of "path parts" not the same // check wild card: if(this.path.endsWith("*")) { if(pathSize == (thisPathSize - 1) && (path.endsWith("/"))) { // Hack for making wildcards work with trailing slash pathList.add(""); pathList.add(""); pathSize += 2; } if(thisPathSize < pathSize) { for (int i = 0; i < thisPathSize; i++) { String thisPathPart = thisPathList.get(i); String pathPart = pathList.get(i); if(thisPathPart.equals("*") && (i == thisPathSize - 1) && this.path.endsWith("*")) { // wildcard match return true; } if(!thisPathPart.startsWith(":") && !thisPathPart.equals(pathPart) && !thisPathPart.equals("*")) { return false; } } // All parts matched return true; } // End check wild card } return false; } } public String toString() { return httpMethod.name() + ", " + path + ", " + target; } } public SimpleRouteMatcher() { routes = new ArrayList<RouteEntry>(); } @Override public List<RouteMatch> findTargetsForRequestedRoute(HttpMethod httpMethod, String path, String acceptType) { List<RouteMatch> matchSet = new ArrayList<RouteMatch>(); List<RouteEntry> routeEntries = this.findTargetsForRequestedRoute( httpMethod, path); for (RouteEntry routeEntry : routeEntries) { if(acceptType != null) { String bestMatch = MimeParse.bestMatch( Arrays.asList(routeEntry.acceptedType), acceptType); if(routeWithGivenAcceptType(bestMatch)) { matchSet.add(new RouteMatch(httpMethod, routeEntry.target, routeEntry.path, path, acceptType)); } } else { matchSet.add(new RouteMatch(httpMethod, routeEntry.target, routeEntry.path, path, acceptType)); } } return matchSet; } @Override public RouteMatch findTargetForRequestedRoute(HttpMethod httpMethod, String path, String acceptType) { List<RouteEntry> routeEntries = this.findTargetsForRequestedRoute( httpMethod, path); RouteEntry entry = findTargetWithGivenAcceptType(routeEntries, acceptType); return entry != null ? new RouteMatch(httpMethod, entry.target, entry.path, path, acceptType) : null; } private RouteEntry findTargetWithGivenAcceptType( List<RouteEntry> routeMatchs, String acceptType) { if(acceptType != null && routeMatchs.size() > 0) { Map<String, RouteEntry> acceptedMimeTypes = getAcceptedMimeTypes(routeMatchs); String bestMatch = MimeParse.bestMatch(acceptedMimeTypes.keySet(), acceptType); if(routeWithGivenAcceptType(bestMatch)) { return acceptedMimeTypes.get(bestMatch); } else { return null; } } else { if(routeMatchs.size() > 0) { return routeMatchs.get(0); } } return null; } private boolean routeWithGivenAcceptType(String bestMatch) { return !MimeParse.NO_MIME_TYPE.equals(bestMatch); } private List<RouteEntry> findTargetsForRequestedRoute( HttpMethod httpMethod, String path) { List<RouteEntry> matchSet = new ArrayList<RouteEntry>(); for (RouteEntry entry : routes) { if(entry.matches(httpMethod, path)) { matchSet.add(entry); } } return matchSet; } @Override public void parseValidateAddRoute(String route, String acceptType, Object target) { try { int singleQuoteIndex = route.indexOf(SINGLE_QUOTE); String httpMethod = route.substring(0, singleQuoteIndex).trim() .toLowerCase(); // NOSONAR String url = route.substring(singleQuoteIndex + 1, route.length() - 1).trim(); // NOSONAR // Use special enum stuff to get from value HttpMethod method; try { method = HttpMethod.valueOf(httpMethod); } catch (IllegalArgumentException e) { throw Throwables.propagate(e); } addRoute(method, url, acceptType, target); } catch (Exception e) { throw Throwables.propagate(e); } } private void addRoute(HttpMethod method, String url, String acceptedType, Object target) { RouteEntry entry = new RouteEntry(); entry.httpMethod = method; entry.path = url; entry.target = target; entry.acceptedType = acceptedType; // Adds to end of list routes.add(entry); } // can be cached? I don't think so. private Map<String, RouteEntry> getAcceptedMimeTypes(List<RouteEntry> routes) { Map<String, RouteEntry> acceptedTypes = new HashMap<>(); for (RouteEntry routeEntry : routes) { if(!acceptedTypes.containsKey(routeEntry.acceptedType)) { acceptedTypes.put(routeEntry.acceptedType, routeEntry); } } return acceptedTypes; } @Override public void clearRoutes() { routes.clear(); } @Override public Set<HttpMethod> findMethodsForRequestedPath(String path, String acceptedType) { // CON-476 Set<HttpMethod> methods = Sets.newHashSet(); for (HttpMethod method : HttpMethod.values()) { if(!findTargetsForRequestedRoute(method, path, acceptedType) .isEmpty()) { methods.add(method); } } return methods; } }