HttpRequestHandler.java

/*
 * Copyright 2016 The Lannister Project
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package net.anyflow.lannister.http;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.URISyntaxException;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import org.reflections.Reflections;

import com.google.common.collect.Maps;

import net.anyflow.lannister.Settings;

public abstract class HttpRequestHandler {

	public static final String NO_WEB_RESOURCE_PATH = "none";

	private static final Map<String, Class<? extends HttpRequestHandler>> HANDLER_CLASS_MAP = Maps.newHashMap();
	private static Set<Class<? extends HttpRequestHandler>> REQUEST_HANDLER_CLASSES;
	private static String REQUEST_HANDLER_PACKAGE_ROOT;

	private HttpRequest request;
	private HttpResponse response;

	@Retention(RetentionPolicy.RUNTIME)
	@Target({ ElementType.TYPE, ElementType.METHOD })
	public @interface Handles {
		String[] paths();

		String[] httpMethods();

		String webResourcePath() default NO_WEB_RESOURCE_PATH;
	}

	public abstract String service();

	protected HttpRequestHandler initialize(HttpRequest request, HttpResponse response) throws URISyntaxException {
		this.request = request;
		this.response = response;

		return this;
	}

	public HttpRequest httpRequest() {
		return request;
	}

	public HttpResponse httpResponse() {
		return response;
	}

	public static void setRequestHandlerPakcageRoot(String requestHandlerPakcageRoot) {
		REQUEST_HANDLER_PACKAGE_ROOT = requestHandlerPakcageRoot;
	}

	protected static MatchedCriterion findRequestHandler(String requestedPath, String httpMethod) {
		for (Map.Entry<String, Class<? extends HttpRequestHandler>> item : HANDLER_CLASS_MAP.entrySet()) {
			MatchedCriterion mc = match(requestedPath, httpMethod, item.getKey());

			if (mc.matched) {
				mc.requestHandlerClass = HANDLER_CLASS_MAP.get(item.getKey());
				return mc;
			}
		}

		if (REQUEST_HANDLER_CLASSES == null) {
			REQUEST_HANDLER_CLASSES = new Reflections(REQUEST_HANDLER_PACKAGE_ROOT)
					.getSubTypesOf(HttpRequestHandler.class);
		}

		final ReturnWrapper wrapper = new ReturnWrapper();
		REQUEST_HANDLER_CLASSES.stream().anyMatch(c -> {
			HttpRequestHandler.Handles annotation = c.getAnnotation(HttpRequestHandler.Handles.class);
			if (annotation == null) { return false; }

			return Stream.of(annotation.httpMethods()).anyMatch(m -> {
				if (!m.equalsIgnoreCase(httpMethod)) { return false; }

				return Stream.of(annotation.paths()).anyMatch(p -> {
					return wrapper.setValueIfMatch(requestedPath, c, m, p);
				});
			});
		});

		return wrapper.value == null ? new MatchedCriterion() : wrapper.value;
	}

	private static class ReturnWrapper {
		private MatchedCriterion value = null;

		private boolean setValueIfMatch(String requestedPath, Class<? extends HttpRequestHandler> handlerClass,
				String methodToCheck, String pathToCheck) {
			String path = (pathToCheck.charAt(0) == '/') ? pathToCheck
					: Settings.INSTANCE.httpContextRoot() + pathToCheck;
			String criterion = path + "/" + methodToCheck;

			MatchedCriterion mc = match(requestedPath, methodToCheck, criterion);

			if (mc.matched) {
				HANDLER_CLASS_MAP.put(criterion, handlerClass);
				mc.requestHandlerClass = handlerClass;
				this.value = mc;

				return true;
			}
			else {
				return false;
			}
		};
	}

	protected static class MatchedCriterion {
		private boolean matched = false;
		private Class<? extends HttpRequestHandler> requestHandlerClass;
		private String criterionPath;
		private String criterionHttpMethod;
		private final Map<String, String> pathParameters = Maps.newHashMap();

		public Class<? extends HttpRequestHandler> requestHandlerClass() {
			return requestHandlerClass;
		}

		public String criterionPath() {
			return criterionPath;
		}

		public String criterionHttpMethod() {
			return criterionHttpMethod;
		}

		public Map<String, String> pathParameters() {
			return pathParameters;
		}
	}

	private static MatchedCriterion match(String requestedPath, String httpMethod, String criterion) {
		MatchedCriterion ret = new MatchedCriterion();

		String testTarget = requestedPath + "/" + httpMethod;

		String[] testTokens = testTarget.split("/");
		String[] criterionTokens = criterion.split("/");

		if (criterionTokens.length != testTokens.length) { return ret; }

		// should start with #1 due to item[0] is whitespace.
		for (int i = 1; i < criterionTokens.length; ++i) {
			if (criterionTokens[i].startsWith("{") && criterionTokens[i].endsWith("}")) {
				ret.pathParameters.put(criterionTokens[i].substring(1, criterionTokens[i].length() - 1), testTokens[i]);
			}
			else if (!criterionTokens[i].equalsIgnoreCase(testTokens[i])) { return ret; }
		}

		ret.matched = true;
		ret.criterionHttpMethod = httpMethod;
		ret.criterionPath = criterion.replace("/" + httpMethod, "");

		return ret;
	}
}