View Javadoc
1   /*
2    * Copyright 2016 The Lannister Project
3    * 
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * 
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    * 
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package net.anyflow.lannister.http;
18  
19  import java.lang.annotation.ElementType;
20  import java.lang.annotation.Retention;
21  import java.lang.annotation.RetentionPolicy;
22  import java.lang.annotation.Target;
23  import java.net.URISyntaxException;
24  import java.util.Map;
25  import java.util.Set;
26  import java.util.stream.Stream;
27  
28  import org.reflections.Reflections;
29  
30  import com.google.common.collect.Maps;
31  
32  import net.anyflow.lannister.Settings;
33  
34  public abstract class HttpRequestHandler {
35  
36  	public static final String NO_WEB_RESOURCE_PATH = "none";
37  
38  	private static final Map<String, Class<? extends HttpRequestHandler>> HANDLER_CLASS_MAP = Maps.newHashMap();
39  	private static Set<Class<? extends HttpRequestHandler>> REQUEST_HANDLER_CLASSES;
40  	private static String REQUEST_HANDLER_PACKAGE_ROOT;
41  
42  	private HttpRequest request;
43  	private HttpResponse response;
44  
45  	@Retention(RetentionPolicy.RUNTIME)
46  	@Target({ ElementType.TYPE, ElementType.METHOD })
47  	public @interface Handles {
48  		String[] paths();
49  
50  		String[] httpMethods();
51  
52  		String webResourcePath() default NO_WEB_RESOURCE_PATH;
53  	}
54  
55  	public abstract String service();
56  
57  	protected HttpRequestHandler initialize(HttpRequest request, HttpResponse response) throws URISyntaxException {
58  		this.request = request;
59  		this.response = response;
60  
61  		return this;
62  	}
63  
64  	public HttpRequest httpRequest() {
65  		return request;
66  	}
67  
68  	public HttpResponse httpResponse() {
69  		return response;
70  	}
71  
72  	public static void setRequestHandlerPakcageRoot(String requestHandlerPakcageRoot) {
73  		REQUEST_HANDLER_PACKAGE_ROOT = requestHandlerPakcageRoot;
74  	}
75  
76  	protected static MatchedCriterion findRequestHandler(String requestedPath, String httpMethod) {
77  		for (Map.Entry<String, Class<? extends HttpRequestHandler>> item : HANDLER_CLASS_MAP.entrySet()) {
78  			MatchedCriterion mc = match(requestedPath, httpMethod, item.getKey());
79  
80  			if (mc.matched) {
81  				mc.requestHandlerClass = HANDLER_CLASS_MAP.get(item.getKey());
82  				return mc;
83  			}
84  		}
85  
86  		if (REQUEST_HANDLER_CLASSES == null) {
87  			REQUEST_HANDLER_CLASSES = new Reflections(REQUEST_HANDLER_PACKAGE_ROOT)
88  					.getSubTypesOf(HttpRequestHandler.class);
89  		}
90  
91  		final ReturnWrapper wrapper = new ReturnWrapper();
92  		REQUEST_HANDLER_CLASSES.stream().anyMatch(c -> {
93  			HttpRequestHandler.Handles annotation = c.getAnnotation(HttpRequestHandler.Handles.class);
94  			if (annotation == null) { return false; }
95  
96  			return Stream.of(annotation.httpMethods()).anyMatch(m -> {
97  				if (!m.equalsIgnoreCase(httpMethod)) { return false; }
98  
99  				return Stream.of(annotation.paths()).anyMatch(p -> {
100 					return wrapper.setValueIfMatch(requestedPath, c, m, p);
101 				});
102 			});
103 		});
104 
105 		return wrapper.value == null ? new MatchedCriterion() : wrapper.value;
106 	}
107 
108 	private static class ReturnWrapper {
109 		private MatchedCriterion value = null;
110 
111 		private boolean setValueIfMatch(String requestedPath, Class<? extends HttpRequestHandler> handlerClass,
112 				String methodToCheck, String pathToCheck) {
113 			String path = (pathToCheck.charAt(0) == '/') ? pathToCheck
114 					: Settings.INSTANCE.httpContextRoot() + pathToCheck;
115 			String criterion = path + "/" + methodToCheck;
116 
117 			MatchedCriterion mc = match(requestedPath, methodToCheck, criterion);
118 
119 			if (mc.matched) {
120 				HANDLER_CLASS_MAP.put(criterion, handlerClass);
121 				mc.requestHandlerClass = handlerClass;
122 				this.value = mc;
123 
124 				return true;
125 			}
126 			else {
127 				return false;
128 			}
129 		};
130 	}
131 
132 	protected static class MatchedCriterion {
133 		private boolean matched = false;
134 		private Class<? extends HttpRequestHandler> requestHandlerClass;
135 		private String criterionPath;
136 		private String criterionHttpMethod;
137 		private final Map<String, String> pathParameters = Maps.newHashMap();
138 
139 		public Class<? extends HttpRequestHandler> requestHandlerClass() {
140 			return requestHandlerClass;
141 		}
142 
143 		public String criterionPath() {
144 			return criterionPath;
145 		}
146 
147 		public String criterionHttpMethod() {
148 			return criterionHttpMethod;
149 		}
150 
151 		public Map<String, String> pathParameters() {
152 			return pathParameters;
153 		}
154 	}
155 
156 	private static MatchedCriterion match(String requestedPath, String httpMethod, String criterion) {
157 		MatchedCriterion ret = new MatchedCriterion();
158 
159 		String testTarget = requestedPath + "/" + httpMethod;
160 
161 		String[] testTokens = testTarget.split("/");
162 		String[] criterionTokens = criterion.split("/");
163 
164 		if (criterionTokens.length != testTokens.length) { return ret; }
165 
166 		// should start with #1 due to item[0] is whitespace.
167 		for (int i = 1; i < criterionTokens.length; ++i) {
168 			if (criterionTokens[i].startsWith("{") && criterionTokens[i].endsWith("}")) {
169 				ret.pathParameters.put(criterionTokens[i].substring(1, criterionTokens[i].length() - 1), testTokens[i]);
170 			}
171 			else if (!criterionTokens[i].equalsIgnoreCase(testTokens[i])) { return ret; }
172 		}
173 
174 		ret.matched = true;
175 		ret.criterionHttpMethod = httpMethod;
176 		ret.criterionPath = criterion.replace("/" + httpMethod, "");
177 
178 		return ret;
179 	}
180 }