All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.officefloor.web.route.WebRouterBuilder Maven / Gradle / Ivy

There is a newer version: 3.40.0
Show newest version
/*
 * 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
				if ((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