package org.esmerilprogramming.overtown.server.handlers;
import io.undertow.UndertowMessages;
import java.util.*;
/**
* Created by efraimgentil<efraimgentil@gmail.com> on 12/02/15.
*/
public class CustomPathTemplate {
private final String templateString;
private final boolean template;
private final String base;
private final List<Part> parts;
private final Set<String> parameterNames;
private CustomPathTemplate(String templateString, final boolean template, final String base, final List<Part> parts, Set<String> parameterNames) {
this.templateString = templateString;
this.template = template;
this.base = base;
this.parts = parts;
this.parameterNames = Collections.unmodifiableSet(parameterNames);
}
public static CustomPathTemplate create(final String inputPath) {
// a path is required
if (inputPath == null) {
throw UndertowMessages.MESSAGES.pathMustBeSpecified();
}
// prepend a "/" if none is present
if (!inputPath.startsWith("/") && inputPath.length() > 1) {
return CustomPathTemplate.create("/" + inputPath);
}
// otherwise normalize template
final StringBuilder builder = new StringBuilder(inputPath);
/*while (builder != null && builder.length() > 1 && '/' == builder.charAt(builder.length() - 1)) {
builder.deleteCharAt(builder.length() - 1);
}*/
// create string from modified string
final String path = builder.toString();
int state = 0;
String base = "";
List<Part> parts = new ArrayList<>();
int stringStart = 0;
//0 parsing base
//1 parsing base, last char was /
//2 in template part
//3 just after template part, expecting /
//4 expecting either template or segment
//5 in segment
for (int i = 0; i < path.length(); ++i) {
final int c = path.charAt(i);
switch (state) {
case 0: {
if (c == '/') {
state = 1;
} else {
state = 0;
}
break;
}
case 1: {
if (c == '{') {
base = path.substring(0, i);
stringStart = i + 1;
state = 2;
} else if (c != '/') {
state = 0;
}
break;
}
case 2: {
if (c == '}') {
Part part = new Part(true, path.substring(stringStart, i));
parts.add(part);
stringStart = i;
state = 3;
}
break;
}
case 3: {
if (c == '/') {
state = 4;
} else {
throw UndertowMessages.MESSAGES.couldNotParseUriTemplate(path, i);
}
break;
}
case 4: {
if (c == '{') {
stringStart = i + 1;
state = 2;
} else if (c != '/') {
stringStart = i;
state = 5;
}
break;
}
case 5: {
if (c == '/') {
Part part = new Part(false, path.substring(stringStart, i));
parts.add(part);
stringStart = i + 1;
state = 4;
}
break;
}
}
}
switch (state) {
case 0:
case 1: {
base = path;
break;
}
case 2: {
throw UndertowMessages.MESSAGES.couldNotParseUriTemplate(path, path.length());
}
case 5: {
Part part = new Part(false, path.substring(stringStart));
parts.add(part);
break;
}
}
final Set<String> templates = new HashSet<>();
for (Part part : parts) {
if (part.template) {
templates.add(part.part);
}
}
return new CustomPathTemplate(path, state > 1, base, parts, templates);
}
/**
* Check if the given uri matches the template. If so then it will return true and
* place the value of any path parameters into the given map.
* <p/>
* Note the map may be modified even if the match in unsuccessful, however in this case
* it will be emptied before the method returns
*
* @param path The request path, relative to the context root
* @param pathParameters The path parameters map to fill out
* @return true if the URI is a match
*/
public boolean matches(final String path, final Map<String, String> pathParameters) {
if (!path.startsWith(base)) {
return false;
}
int baseLength = base.length();
if (!template) {
return path.length() == baseLength;
}
int cp = 0;
Part current = parts.get(cp);
int stringStart = baseLength;
int i;
for (i = baseLength; i < path.length(); ++i) {
final char c = path.charAt(i);
if (c == '?') {
break;
} else if (c == '/') {
String result = path.substring(stringStart, i);
if (current.template) {
pathParameters.put(current.part, result);
} else if (!result.equals(current.part)) {
pathParameters.clear();
return false;
}
++cp;
if (cp == parts.size()) {
//this is a match if this is the last character
return i == (path.length() - 1);
}
current = parts.get(cp);
stringStart = i + 1;
}
}
if (cp + 1 != parts.size()) {
pathParameters.clear();
return false;
}
String result = path.substring(stringStart, i);
if (current.template) {
pathParameters.put(current.part, result);
} else if (!result.equals(current.part)) {
pathParameters.clear();
return false;
}
return true;
}
public int compareTo(final CustomPathTemplate o) {
//we want templates with the highest priority to sort first
//so we sort in reverse priority order
//templates have lower priority
if (template && !o.template) {
return 1;
} else if (o.template && !template) {
return -1;
}
int res = base.compareTo(o.base);
if (res > 0) {
//our base is longer
return -1;
} else if (res < 0) {
return 1;
} else if (!template) {
//they are the same path
return 0;
}
//the first path with a non-template element
int i = 0;
for (; ; ) {
if (parts.size() == i) {
if (o.parts.size() == i) {
return base.compareTo(o.base);
}
return 1;
} else if (o.parts.size() == i) {
//we have more parts, so should be checked first
return -1;
}
Part thisPath = parts.get(i);
Part otherPart = o.parts.get(i);
if (thisPath.template && !otherPart.template) {
//non template part sorts first
return 1;
} else if (!thisPath.template && otherPart.template) {
return -1;
} else if (!thisPath.template) {
int r = thisPath.part.compareTo(otherPart.part);
if (r != 0) {
return r;
}
}
++i;
}
}
public String getBase() {
return base;
}
public String getTemplateString() {
return templateString;
}
public Set<String> getParameterNames() {
return parameterNames;
}
private static class Part {
final boolean template;
final String part;
private Part(final boolean template, final String part) {
this.template = template;
this.part = part;
}
@Override
public String toString() {
return "Part{" +
"template=" + template +
", part='" + part + '\'' +
'}';
}
}
@Override
public String toString() {
return "PathTemplate{" +
"template=" + template +
", base='" + base + '\'' +
", parts=" + parts +
'}';
}
}