/*
ESXX - The friendly ECMAscript/XML Application Server
Copyright (C) 2007-2015 Martin Blom <martin@blom.org>
This program is free software: you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation, either version 3
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.esxx.util;
import java.util.LinkedList;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.*;
import org.mozilla.javascript.*;
public class RequestMatcher {
public RequestMatcher() {
patterns = new LinkedList<Request>();
}
public void clear() {
patterns.clear();
}
public void addRequestPattern(String method, String uri, String handler) {
if (method.isEmpty()) {
method = "[^" + SEPARATOR + "]+";
}
if (uri.isEmpty()) {
uri = ".*";
}
patterns.add(new Request(method, uri, handler));
}
public void compile() {
StringBuilder regex = new StringBuilder();
for (Request r : patterns) {
if (regex.length() > 0) {
regex.append("|");
}
regex.append("^(");
regex.append(r.pattern.toString());
regex.append(")");
}
compiledPattern = Pattern.compile(regex.toString());
}
public Match matchRequest(String method, String uri,
Context cx, Scriptable scope) {
Matcher m = compiledPattern.matcher(method + SEPARATOR + uri);
if (m.matches()) {
int group = 1;
for (Request r : patterns) {
if (m.start(group) != -1) {
Match res = new Match();
// We found the request that matched. Now calculate return
// values and exit.
res.params = cx.newObject(scope);
// // Put unnamed groups, incl. a faked group 0
// ScriptableObject.putProperty(res.params, 0, uri);
for (int i = 0; i <= r.numGroups; ++i) {
Object value = m.group(group + i);
if (value == null) {
value = Context.getUndefinedValue();
}
ScriptableObject.putProperty(res.params, i, m.group(group + i));
}
// Put named groups
for (Map.Entry<String,Integer> e : r.namedGroups.entrySet()) {
String name = e.getKey();
Object value = m.group(group + e.getValue());
if (value == null) {
value = Context.getUndefinedValue();
}
ScriptableObject.putProperty(res.params, name, value);
}
StringBuffer sb = new StringBuffer();
Matcher tm = Request.namedReferencePattern.matcher(r.handlerTemplate);
while (tm.find()) {
String ref = tm.group();
String name = ref.substring(1, ref.length() - 1);
int group_num = group + r.namedGroups.get(name);
tm.appendReplacement(sb, m.group(group_num));
}
tm.appendTail(sb);
res.handler = sb.toString();
return res;
}
group += r.numGroups + 1;
}
throw new InternalError("Internal error in matchRequest: Failed to find the match!");
}
else {
return null;
}
}
public static class Match {
public String handler;
public Scriptable params;
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("RequestMatcher.Match[");
sb.append(handler);
for (Object o : params.getIds()) {
sb.append(", ");
sb.append(o);
sb.append(": ");
if (o instanceof Integer) {
sb.append(params.get((Integer) o, params));
}
else {
sb.append(params.get((String) o, params));
}
}
sb.append("] ");
return sb.toString();
}
}
private static class Request {
public Request(String m, String u, String h) {
handlerTemplate = h;
namedGroups = new TreeMap<String, Integer>();
compilePattern(m, u);
}
private void compilePattern(String method, String uri) {
String regex = "(?:" + method + ")" + SEPARATOR + "(?:" + uri + ")";
Matcher gm = groupPattern.matcher(regex);
Matcher nm = namedGroupPattern.matcher(regex);
TreeMap<Integer, Integer> offset2group = new TreeMap<Integer, Integer>();
numGroups = 0;
// System.err.println(regex);
while (gm.find()) {
++numGroups;
offset2group.put(gm.start(1), numGroups);
// System.err.println(gm.group(1) + " at " + gm.start(1));
}
while (nm.find()) {
Integer g = offset2group.get(nm.start(1));
// System.err.println(nm.group(1) + " at " + nm.start(1));
if (g == null) {
throw new InternalError("Internal error #1 in RequestMatcher.compilePattern()");
}
String match = nm.group(1);
String name = match.substring(3, match.length() - 1);
namedGroups.put(name, g);
}
String unnamed = nm.replaceAll("(");
// Compile regex and make sure our group count is the same as Java's.
// try {
pattern = Pattern.compile(unnamed);
// }
// catch (PatternSyntaxException ex) {
// throw new ESXXException("Unable to compile request regex: " + unnamed);
// }
if (pattern.matcher("").groupCount() != numGroups) {
// System.err.println(pattern.toString());
// System.err.println(pattern.matcher("").groupCount());
// System.err.println(numGroups);
throw new InternalError("Internal error #2 in RequestMatcher.compilePattern()");
}
}
private String handlerTemplate;
private TreeMap<String, Integer> namedGroups;
private int numGroups;
private Pattern pattern;
/** A Pattern that matches '(' or '(?{' but not '\(' or '(?' */
private static Pattern groupPattern = Pattern.compile("(((^\\()|((?<!\\\\)\\())" +
"(?=[^?]|\\?\\{))");
/** A Pattern that matches '(?{...}', where ... is anything but a '}' */
private static Pattern namedGroupPattern = Pattern.compile("(\\(\\?\\{[^}]+})");
/** A Pattern that matches '{...}', where ... is anything but a '}' */
static Pattern namedReferencePattern = Pattern.compile("(\\{[^\\}]+\\})");
}
private LinkedList<Request> patterns;
private Pattern compiledPattern;
private static char SEPARATOR = '\n';
public static void main(String args[]) {
final RequestMatcher rm = new RequestMatcher();
rm.addRequestPattern("GET",
"articles",
"1");
rm.addRequestPattern("(GET|POST)",
"articles/books",
"2");
rm.addRequestPattern("[^/]+",
"articles/(?{article}[a-z]+)/(?{id}\\d+)",
"3");
rm.addRequestPattern("[^/]+",
"article\\(s\\)/(\\d+)/(?{article}[a-z]+)/(?{id}\\d+)",
"3");
rm.addRequestPattern("(?{method}[^/]+)",
"(?{cat}\\p{javaLetter}+)/(?{item}\\p{javaLetter}+)/(?{id}\\d+)",
"{method}_{cat}_{item}_{id}");
rm.compile();
ContextFactory.getGlobal().call(new ContextAction() {
public Object run(Context cx) {
Scriptable scope = new ImporterTopLevel(cx);
System.out.println(rm.matchRequest("POST", "articles/books", cx, scope));
System.out.println(rm.matchRequest("POST", "articles/", cx, scope));
System.out.println(rm.matchRequest("GET", "articles/books", cx, scope));
System.out.println(rm.matchRequest("GET", "articles/", cx, scope));
System.out.println(rm.matchRequest("REPORT", "article(s)/100/hejhopp/12", cx, scope));
System.out.println(rm.matchRequest("DELETE", "gr\u00F6na/\u00C4pplen/10", cx, scope));
return null;
}
});
}
}