![JAR search and dependency download from the Maven repository](/logo.png)
net.officefloor.web.route.WebRouterBuilder Maven / Gradle / Ivy
/*
* OfficeFloor - http://www.officefloor.net
* Copyright (C) 2005-2018 Daniel Sagenschneider
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package net.officefloor.web.route;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import net.officefloor.server.http.HttpMethod;
import net.officefloor.server.http.HttpMethod.HttpMethodEnum;
import net.officefloor.web.HttpInputPath;
import net.officefloor.web.HttpInputPathImpl;
import net.officefloor.web.HttpInputPathSegment;
import net.officefloor.web.HttpInputPathSegment.HttpInputPathSegmentEnum;
/**
* Builds the {@link WebRouter}.
*
* @author Daniel Sagenschneider
*/
public class WebRouterBuilder {
/**
* Indicates if the path contains parameters.
*
* @param path Path.
* @return true
should the path contain parameters.
*/
public static boolean isPathParameters(String path) {
return path.contains("{");
}
/**
* Context path.
*/
private final String contextPath;
/**
* {@link WebRoute} instances.
*/
private final List routes = new ArrayList<>();
/**
* Instantiate.
*
* @param contextPath Context path.
*/
public WebRouterBuilder(String contextPath) {
this.contextPath = contextPath;
}
/**
* Adds a route.
*
* @param method {@link HttpMethod}.
* @param path Path. Use {param}
to signify path parameters.
* @param handler {@link WebRouteHandler} for the route.
* @return {@link HttpInputPath} for the route.
*/
public HttpInputPath addRoute(HttpMethod method, String path, WebRouteHandler handler) {
// Keep track of input path
final String inputPath = path;
// Ignore / at end of path
if ((!"/".equals(path)) && (path.endsWith("/"))) {
path = path.substring(0, path.length() - 1);
}
// Include the context path
if (this.contextPath != null) {
path = this.contextPath + path;
}
// Parse out the static segments and parameters from path
List segments = new ArrayList<>();
int currentIndex = 0;
do {
// Find the next parameter
int nextParamStart = path.indexOf('{', currentIndex);
if (nextParamStart < 0) {
// No further parameters
String staticContent = path.substring(currentIndex);
segments.add(new HttpInputPathSegment(HttpInputPathSegmentEnum.STATIC, staticContent));
currentIndex = path.length();
} else {
// Another parameter, so ensure static path separation
// (also handle parameter at start)
if ((currentIndex > 0) && ((nextParamStart - currentIndex) == 0)) {
throw new IllegalArgumentException("Must have static characters between path parameters");
}
// Include the static content
String staticContent = path.substring(currentIndex, nextParamStart);
segments.add(new HttpInputPathSegment(HttpInputPathSegmentEnum.STATIC, staticContent));
// Find the end of the parameter
int nextParamEnd = path.indexOf('}', nextParamStart);
if (nextParamEnd < 0) {
throw new IllegalArgumentException("No terminating '}' for parameter");
}
String parameterName = path.substring(nextParamStart + 1, nextParamEnd);
segments.add(new HttpInputPathSegment(HttpInputPathSegmentEnum.PARAMETER, parameterName));
// Move to after parameter
currentIndex = nextParamEnd + "}".length();
}
} while (currentIndex < path.length());
// Stitch the segments into linked list
for (int i = 0; i < (segments.size() - 1); i++) {
segments.get(i).next = segments.get(i + 1);
}
// Create and register the web route
WebRoute route = new WebRoute(method, inputPath, segments.get(0), handler);
this.routes.add(route);
// Return HTTP input path
return route.createHttpInputPath();
}
/**
* Builds the {@link WebRouter}.
*
* @return {@link WebRouter}.
*/
public WebRouter build() {
// Sort the routes
this.routes.sort((a, b) -> {
// Sort first by more static
int staticCompare = b.staticCharacterCount - a.staticCharacterCount;
if (staticCompare != 0) {
// Sorted by static
return staticCompare;
}
// Sort next by less parameters
int parameterCompare = a.parameterCount - b.parameterCount;
if (parameterCompare != 0) {
return parameterCompare;
}
// As here, same route weighting
return 0;
});
// Create the route tree of choices
WebRouteChoice[] choices = this.createChoices(this.routes);
// Create the route tree
WebRouteNode[] nodes = new WebRouteNode[choices.length];
for (int i = 0; i < nodes.length; i++) {
nodes[i] = this.createNode(choices[i], new LinkedList<>());
}
// Return the web router
return new WebRouter(nodes);
}
/**
* Creates the {@link WebRouteNode}.
*
* @param choice {@link WebRouteChoice}.
* @param staticCharacters Previous static characters.
* @return {@link WebRouteNode}.
*/
private WebRouteNode createNode(WebRouteChoice choice, List staticCharacters) {
// Supply the characters
Supplier getStatic = () -> {
char[] characters = new char[staticCharacters.size()];
for (int i = 0; i < characters.length; i++) {
characters[i] = staticCharacters.get(i);
}
return characters;
};
// Wrap node with potential static
Function getStaticWrap = (node) -> {
char[] characters = getStatic.get();
if (characters.length == 0) {
return node; // no need to wrap
} else {
// Wrap with static
return new StaticWebRouteNode(characters, new WebRouteNode[] { node });
}
};
// Branch choice in routing tree
switch (choice.type) {
case LEAF:
// Create the mapping of route to parameter names
Map routeParameterNames = new HashMap<>();
for (WebRoute route : choice.routes) {
List parameterNames = new LinkedList<>();
HttpInputPathSegment segment = route.segmentHead;
while (segment != null) {
if (segment.type == HttpInputPathSegmentEnum.PARAMETER) {
parameterNames.add(segment.value);
}
segment = segment.next;
}
routeParameterNames.put(route, parameterNames.toArray(new String[parameterNames.size()]));
}
// Create the method handling (by name)
Map genericHandling = new HashMap<>();
Map genericParameterNames = new HashMap<>();
for (WebRoute route : choice.routes) {
String methodName = route.method.getName();
genericHandling.put(methodName, route.handler);
String[] parameterNames = routeParameterNames.get(route);
genericParameterNames.put(methodName, parameterNames);
}
Map handlers = new EnumMap<>(HttpMethodEnum.class);
handlers.put(HttpMethodEnum.OTHER,
new LeafWebRouteHandling((method) -> genericParameterNames.get(method.getName()),
(method) -> genericHandling.get(method.getName())));
// Obtain the allowed methods
Set allowedMethodsSet = new HashSet<>(genericHandling.keySet());
allowedMethodsSet.add(HttpMethod.OPTIONS.getName());
if (allowedMethodsSet.contains(HttpMethod.GET.getName())) {
allowedMethodsSet.add(HttpMethod.HEAD.getName());
}
String[] allowedMethods = allowedMethodsSet.stream().sorted().toArray(String[]::new);
// Load handling by enum
for (WebRoute route : choice.routes) {
String[] parameterNames = routeParameterNames.get(route);
HttpMethodEnum routeMethod = route.method.getEnum();
if (routeMethod != HttpMethodEnum.OTHER) {
handlers.put(routeMethod,
new LeafWebRouteHandling((method) -> parameterNames, (method) -> route.handler));
}
}
// Return the leaf node
return getStaticWrap.apply(new LeafWebRouteNode(allowedMethods, handlers));
case STATIC:
// Add the character to static routes and continue static route
char character = choice.value.charAt(0);
staticCharacters.add(character);
// Handle based on further routes
switch (choice.routes.size()) {
case 0:
throw new IllegalStateException("Ending static route should always have leaf choice");
case 1:
// Single choice, so carry on with static characters
WebRouteChoice singleChoice = new WebRouteChoice(choice.routes.get(0));
return this.createNode(singleChoice, staticCharacters);
default:
// Multiple routes, so create the children
WebRouteChoice[] childChoices = this.createChoices(choice.routes);
WebRouteNode[] children = new WebRouteNode[childChoices.length];
for (int i = 0; i < children.length; i++) {
children[i] = this.createNode(childChoices[i], new LinkedList<>());
}
char[] characters = getStatic.get();
return new StaticWebRouteNode(characters, children);
}
case PARAMETER:
// Load children nodes
LeafWebRouteNode leafNode = null;
List staticNodes = new LinkedList<>();
WebRouteChoice[] paramEndChoices = this.createChoices(choice.routes);
for (WebRouteChoice paramChoice : paramEndChoices) {
switch (paramChoice.type) {
case LEAF:
// Ensure only leaf node
if (leafNode != null) {
throw new IllegalStateException("May only have one leaf node after a parameter");
}
leafNode = (LeafWebRouteNode) this.createNode(paramChoice, new LinkedList<>());
break;
case STATIC:
StaticWebRouteNode paramStatic = (StaticWebRouteNode) this.createNode(paramChoice,
new LinkedList<>());
staticNodes.add(paramStatic);
break;
case PARAMETER:
// Parameters not follow (need static demarcation)
throw new IllegalStateException(
"May not have a path parameter directly after another path parameter");
}
}
// Return the parameter node
return getStaticWrap.apply(new ParameterWebRouteNode(
staticNodes.toArray(new StaticWebRouteNode[staticNodes.size()]), leafNode));
default:
throw new IllegalStateException("Unhandled type " + choice.type);
}
}
/**
* Create the {@link WebRouteChoice} values for the {@link WebRoute}.
*
* @param routes {@link WebRoute}.
* @return {@link WebRouteChoice}.
*/
private WebRouteChoice[] createChoices(List routes) {
// Create the listing of choices (static always before parameters)
WebRouteChoice endChoice = null;
List staticChoices = new ArrayList<>();
WebRouteChoice paramChoice = null;
// Load the route choices
NEXT_ROUTE: for (WebRoute route : routes) {
// Create the choice
WebRouteChoice choice = new WebRouteChoice(route);
switch (choice.type) {
case LEAF:
if (endChoice == null) {
endChoice = choice;
} else {
endChoice.routes.addAll(choice.routes);
}
break;
case PARAMETER:
if (paramChoice == null) {
paramChoice = choice;
} else {
paramChoice.routes.addAll(choice.routes);
}
break;
case STATIC:
// Determine if match static character
for (WebRouteChoice staticChoice : staticChoices) {
if (staticChoice.value.charAt(0) == choice.value.charAt(0)) {
staticChoice.routes.add(choice.routes.get(0));
continue NEXT_ROUTE;
}
}
// As here, new static route
staticChoices.add(choice);
break;
}
}
// Load the choices
WebRouteChoice[] choices = new WebRouteChoice[(endChoice == null ? 0 : 1) + staticChoices.size()
+ (paramChoice == null ? 0 : 1)];
int index = 0;
if (endChoice != null) {
choices[index++] = endChoice;
}
for (WebRouteChoice staticChoice : staticChoices) {
choices[index++] = staticChoice;
}
if (paramChoice != null) {
choices[index++] = paramChoice;
}
// Return the choices
return choices;
}
/**
* Web route.
*/
private static class WebRoute {
/**
* {@link HttpMethod}.
*/
private final HttpMethod method;
/**
* Route path.
*/
private final String routePath;
/**
* Head {@link HttpInputPathSegment} of linked list of
* {@link HttpInputPathSegment} instances.
*/
private final HttpInputPathSegment segmentHead;
/**
* Number of static path characters for sorting routes.
*/
private final int staticCharacterCount;
/**
* Number of path parameters for sorting routes.
*/
private final int parameterCount;
/**
* {@link WebRouteHandler} for the route.
*/
private final WebRouteHandler handler;
/**
* Current {@link HttpInputPathSegment}.
*/
private HttpInputPathSegment currentSegment;
/**
* Indicates index into the static characters for current
* {@link HttpInputPathSegment}.
*/
private int staticIndex = -1; // before start
/**
* Instantiate.
*
* @param method {@link HttpMethod}.
* @param routePath Route path.
* @param segmentHead Head {@link HttpInputPathSegment} of linked list of
* {@link HttpInputPathSegment} instances.
* @param handler {@link WebRouteHandler}.
*/
public WebRoute(HttpMethod method, String routePath, HttpInputPathSegment segmentHead,
WebRouteHandler handler) {
this.method = method;
this.routePath = routePath;
this.segmentHead = segmentHead;
this.handler = handler;
// Determine the sort metrics
int staticCharacterCount = 0;
int parameterCount = 0;
HttpInputPathSegment segment = this.segmentHead;
while (segment != null) {
switch (segment.type) {
case STATIC:
staticCharacterCount += segment.value.length();
break;
case PARAMETER:
parameterCount++;
break;
default:
throw new IllegalStateException(
WebRoute.class.getSimpleName() + " should not have segment of type " + segment.type);
}
segment = segment.next;
}
this.staticCharacterCount = staticCharacterCount;
this.parameterCount = parameterCount;
// Load the current segment
this.currentSegment = this.segmentHead;
}
/**
* Creates the {@link HttpInputPath}.
*
* @return {@link HttpInputPath}.
*/
public HttpInputPath createHttpInputPath() {
return new HttpInputPathImpl(this.routePath, this.segmentHead, this.parameterCount);
}
}
/**
* Type of {@link WebRouteChoice}.
*/
private static enum WebRouteChoiceEnum {
STATIC, PARAMETER, LEAF
}
/**
* Choice in route tree.
*/
private static class WebRouteChoice {
/**
* {@link WebRouteChoiceEnum}.
*/
private final WebRouteChoiceEnum type;
/**
* Static path or parameter name.
*/
private final String value;
/**
* {@link WebRoute} instances for this choice.
*/
private List routes = new LinkedList<>();
/**
* Instantiate.
*
* @param route {@link WebRoute}.
*/
private WebRouteChoice(WebRoute route) {
// Increment for next character
route.staticIndex++;
// Determine if exceed static path length
if ((route.currentSegment != null) && (route.currentSegment.type == HttpInputPathSegmentEnum.STATIC)) {
if (route.staticIndex >= route.currentSegment.value.length()) {
// Move to next segment
route.currentSegment = route.currentSegment.next;
route.staticIndex = 0;
}
}
// Configure choice based on current segment
if (route.currentSegment == null) {
// End of route
this.type = WebRouteChoiceEnum.LEAF;
this.value = null;
} else {
// Load static or parameter
switch (route.currentSegment.type) {
case STATIC:
// Add the static route
this.type = WebRouteChoiceEnum.STATIC;
this.value = String.valueOf(route.currentSegment.value.charAt(route.staticIndex));
break;
case PARAMETER:
// Add the parameter route
this.type = WebRouteChoiceEnum.PARAMETER;
this.value = route.currentSegment.value;
// Move past parameter segment
route.currentSegment = route.currentSegment.next;
route.staticIndex = -1;
break;
default:
throw new Error("Unknown input path segment type " + route.currentSegment.type.name());
}
}
// Include the route
this.routes.add(route);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy