
io.jsync.http.RouteMatcher Maven / Gradle / Ivy
Show all versions of jsync.io Show documentation
/*
* Copyright (c) 2011-2013 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.jsync.http;
import io.jsync.Handler;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class allows you to do route requests based on the HTTP verb and the request URI, in a manner similar
* to Sinatra or Express.
* RouteMatcher also lets you extract parameters from the request URI either a simple pattern or using
* regular expressions for more complex matches. Any parameters extracted will be added to the requests parameters
* which will be available to you in your request handler.
* It's particularly useful when writing REST-ful web applications.
* To use a simple pattern to extract parameters simply prefix the parameter name in the pattern with a ':' (colon).
* Different handlers can be specified for each of the HTTP verbs, GET, POST, PUT, DELETE etc.
* For more complex matches regular expressions can be used in the pattern. When regular expressions are used, the extracted
* parameters do not have a name, so they are put into the HTTP request with names of param0, param1, param2 etc.
* Multiple matches can be specified for each HTTP verb. In the case there are more than one matching patterns for
* a particular request, the first matching one will be used.
* Instances of this class are not thread-safe
*
* @author Tim Fox
*/
public class RouteMatcher implements Handler {
private final List getBindings = new CopyOnWriteArrayList<>();
private final List putBindings = new CopyOnWriteArrayList<>();
private final List postBindings = new CopyOnWriteArrayList<>();
private final List deleteBindings = new CopyOnWriteArrayList<>();
private final List optionsBindings = new CopyOnWriteArrayList<>();
private final List headBindings = new CopyOnWriteArrayList<>();
private final List traceBindings = new CopyOnWriteArrayList<>();
private final List connectBindings = new CopyOnWriteArrayList<>();
private final List patchBindings = new CopyOnWriteArrayList<>();
private Handler noMatchHandler;
private static void addPattern(String input, Handler handler, List bindings) {
// We need to search for any : tokens in the String and replace them with named capture groups
Matcher m = Pattern.compile(":([A-Za-z][A-Za-z0-9_]*)").matcher(input);
StringBuffer sb = new StringBuffer();
Set groups = new HashSet<>();
while (m.find()) {
String group = m.group().substring(1);
if (groups.contains(group)) {
throw new IllegalArgumentException("Cannot use identifier " + group + " more than once in pattern string");
}
m.appendReplacement(sb, "(?<$1>[^\\/]+)");
groups.add(group);
}
m.appendTail(sb);
String regex = sb.toString();
PatternBinding binding = new PatternBinding(Pattern.compile(regex), groups, handler);
bindings.add(binding);
}
private static void addRegEx(String input, Handler handler, List bindings) {
PatternBinding binding = new PatternBinding(Pattern.compile(input), null, handler);
bindings.add(binding);
}
@Override
public void handle(HttpServerRequest request) {
switch (request.method()) {
case "GET":
route(request, getBindings);
break;
case "PUT":
route(request, putBindings);
break;
case "POST":
route(request, postBindings);
break;
case "DELETE":
route(request, deleteBindings);
break;
case "OPTIONS":
route(request, optionsBindings);
break;
case "HEAD":
route(request, headBindings);
break;
case "TRACE":
route(request, traceBindings);
break;
case "PATCH":
route(request, patchBindings);
break;
case "CONNECT":
route(request, connectBindings);
break;
default:
notFound(request);
}
}
/**
* Specify a handler that will be called for a matching HTTP GET
*
* @param pattern The simple pattern
* @param handler The handler to call
*/
public RouteMatcher get(String pattern, Handler handler) {
addPattern(pattern, handler, getBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP PUT
*
* @param pattern The simple pattern
* @param handler The handler to call
*/
public RouteMatcher put(String pattern, Handler handler) {
addPattern(pattern, handler, putBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP POST
*
* @param pattern The simple pattern
* @param handler The handler to call
*/
public RouteMatcher post(String pattern, Handler handler) {
addPattern(pattern, handler, postBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP DELETE
*
* @param pattern The simple pattern
* @param handler The handler to call
*/
public RouteMatcher delete(String pattern, Handler handler) {
addPattern(pattern, handler, deleteBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP OPTIONS
*
* @param pattern The simple pattern
* @param handler The handler to call
*/
public RouteMatcher options(String pattern, Handler handler) {
addPattern(pattern, handler, optionsBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP HEAD
*
* @param pattern The simple pattern
* @param handler The handler to call
*/
public RouteMatcher head(String pattern, Handler handler) {
addPattern(pattern, handler, headBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP TRACE
*
* @param pattern The simple pattern
* @param handler The handler to call
*/
public RouteMatcher trace(String pattern, Handler handler) {
addPattern(pattern, handler, traceBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP CONNECT
*
* @param pattern The simple pattern
* @param handler The handler to call
*/
public RouteMatcher connect(String pattern, Handler handler) {
addPattern(pattern, handler, connectBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP PATCH
*
* @param pattern The simple pattern
* @param handler The handler to call
*/
public RouteMatcher patch(String pattern, Handler handler) {
addPattern(pattern, handler, patchBindings);
return this;
}
/**
* Specify a handler that will be called for all HTTP methods
*
* @param pattern The simple pattern
* @param handler The handler to call
*/
public RouteMatcher all(String pattern, Handler handler) {
addPattern(pattern, handler, getBindings);
addPattern(pattern, handler, putBindings);
addPattern(pattern, handler, postBindings);
addPattern(pattern, handler, deleteBindings);
addPattern(pattern, handler, optionsBindings);
addPattern(pattern, handler, headBindings);
addPattern(pattern, handler, traceBindings);
addPattern(pattern, handler, connectBindings);
addPattern(pattern, handler, patchBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP GET
*
* @param regex A regular expression
* @param handler The handler to call
*/
public RouteMatcher getWithRegEx(String regex, Handler handler) {
addRegEx(regex, handler, getBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP PUT
*
* @param regex A regular expression
* @param handler The handler to call
*/
public RouteMatcher putWithRegEx(String regex, Handler handler) {
addRegEx(regex, handler, putBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP POST
*
* @param regex A regular expression
* @param handler The handler to call
*/
public RouteMatcher postWithRegEx(String regex, Handler handler) {
addRegEx(regex, handler, postBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP DELETE
*
* @param regex A regular expression
* @param handler The handler to call
*/
public RouteMatcher deleteWithRegEx(String regex, Handler handler) {
addRegEx(regex, handler, deleteBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP OPTIONS
*
* @param regex A regular expression
* @param handler The handler to call
*/
public RouteMatcher optionsWithRegEx(String regex, Handler handler) {
addRegEx(regex, handler, optionsBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP HEAD
*
* @param regex A regular expression
* @param handler The handler to call
*/
public RouteMatcher headWithRegEx(String regex, Handler handler) {
addRegEx(regex, handler, headBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP TRACE
*
* @param regex A regular expression
* @param handler The handler to call
*/
public RouteMatcher traceWithRegEx(String regex, Handler handler) {
addRegEx(regex, handler, traceBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP CONNECT
*
* @param regex A regular expression
* @param handler The handler to call
*/
public RouteMatcher connectWithRegEx(String regex, Handler handler) {
addRegEx(regex, handler, connectBindings);
return this;
}
/**
* Specify a handler that will be called for a matching HTTP PATCH
*
* @param regex A regular expression
* @param handler The handler to call
*/
public RouteMatcher patchWithRegEx(String regex, Handler handler) {
addRegEx(regex, handler, patchBindings);
return this;
}
/**
* Specify a handler that will be called for all HTTP methods
*
* @param regex A regular expression
* @param handler The handler to call
*/
public RouteMatcher allWithRegEx(String regex, Handler handler) {
addRegEx(regex, handler, getBindings);
addRegEx(regex, handler, putBindings);
addRegEx(regex, handler, postBindings);
addRegEx(regex, handler, deleteBindings);
addRegEx(regex, handler, optionsBindings);
addRegEx(regex, handler, headBindings);
addRegEx(regex, handler, traceBindings);
addRegEx(regex, handler, connectBindings);
addRegEx(regex, handler, patchBindings);
return this;
}
/**
* Specify a handler that will be called when no other handlers match.
* If this handler is not specified default behaviour is to return a 404
*/
public RouteMatcher noMatch(Handler handler) {
noMatchHandler = handler;
return this;
}
private void route(HttpServerRequest request, List bindings) {
for (PatternBinding binding : bindings) {
Matcher m = binding.pattern.matcher(request.path());
if (m.matches()) {
Map params = new HashMap<>(m.groupCount());
if (binding.paramNames != null) {
// Named params
for (String param : binding.paramNames) {
params.put(param, m.group(param));
}
} else {
// Un-named params
for (int i = 0; i < m.groupCount(); i++) {
params.put("param" + i, m.group(i + 1));
}
}
request.params().add(params);
binding.handler.handle(request);
return;
}
}
notFound(request);
}
private void notFound(HttpServerRequest request) {
if (noMatchHandler != null) {
noMatchHandler.handle(request);
} else {
// Default 404
request.response().setStatusCode(404);
request.response().end();
}
}
private static class PatternBinding {
final Pattern pattern;
final Handler handler;
final Set paramNames;
private PatternBinding(Pattern pattern, Set paramNames, Handler handler) {
this.pattern = pattern;
this.paramNames = paramNames;
this.handler = handler;
}
}
}