001 package com.khubla.pragmatach.framework.router;
002
003 import java.lang.annotation.Annotation;
004 import java.lang.reflect.Method;
005 import java.util.ArrayList;
006 import java.util.HashSet;
007 import java.util.List;
008 import java.util.Set;
009
010 import org.slf4j.Logger;
011 import org.slf4j.LoggerFactory;
012
013 import com.khubla.pragmatach.framework.annotation.AfterInvoke;
014 import com.khubla.pragmatach.framework.annotation.BeforeInvoke;
015 import com.khubla.pragmatach.framework.annotation.Route;
016 import com.khubla.pragmatach.framework.annotation.RouteParameter;
017 import com.khubla.pragmatach.framework.annotation.View;
018 import com.khubla.pragmatach.framework.api.PragmatachException;
019 import com.khubla.pragmatach.framework.api.Request;
020 import com.khubla.pragmatach.framework.controller.PragmatachController;
021 import com.khubla.pragmatach.framework.url.RouteSpecification;
022
023 /**
024 * a specific route
025 *
026 * @author tome
027 */
028 public class PragmatachRoute implements Comparable<PragmatachRoute> {
029 private static boolean isWildcardURI(String uri) {
030 return (uri.trim().endsWith("*"));
031 }
032
033 /**
034 * check if uri1 scopes uri2. That is, uri1 is more general than uri2.
035 */
036 public static boolean scopes(String uri1, String uri2) {
037 /*
038 * the root url is the most general
039 */
040 if (uri1.compareTo("/*") == 0) {
041 return true;
042 } else {
043 if (isWildcardURI(uri1) && (false == isWildcardURI(uri2))) {
044 /*
045 * uri1 is a wildcard and uri2 is specific.
046 */
047 return true;
048 } else if (isWildcardURI(uri2) && (false == isWildcardURI(uri1))) {
049 /*
050 * uri1 is specific and uri2 is a wildcard route
051 */
052 return false;
053 } else if ((false == isWildcardURI(uri2)) && (false == isWildcardURI(uri1))) {
054 /*
055 * both routes are specific
056 */
057 if (uri1.startsWith(uri2)) {
058 /*
059 * uri1 is a subroute of uri2
060 */
061 return false;
062 } else if (uri2.startsWith(uri1)) {
063 /*
064 * uri2 is a subroute of uri1
065 */
066 return true;
067 } else {
068 /*
069 * routes are not related or are equal
070 */
071 return false;
072 }
073 } else {
074 /*
075 * both routes are wildcard
076 */
077 final String trimmeduri1 = uri1.substring(0, uri1.length() - 1);
078 final String trimmeduri2 = uri2.substring(0, uri2.length() - 1);
079 if (trimmeduri1.startsWith(trimmeduri2)) {
080 /*
081 * uri1 is a subroute of uri2
082 */
083 return false;
084 } else if (trimmeduri2.startsWith(trimmeduri1)) {
085 /*
086 * uri2 is a subroute of uri1
087 */
088 return true;
089 } else {
090 /*
091 * routes are not related or are equal
092 */
093 return false;
094 }
095 }
096 }
097 }
098
099 /**
100 * route annotation
101 */
102 private final Route route;
103 /**
104 * method
105 */
106 private final Method method;
107 /**
108 * methods to call before the method
109 */
110 private final Set<Method> beforeMethods;
111 /**
112 * methods to call after the method
113 */
114 private final Set<Method> afterMethods;
115 /**
116 * route specification
117 */
118 private final RouteSpecification routeSpecification;
119 /**
120 * logger
121 */
122 private final Logger logger = LoggerFactory.getLogger(this.getClass());
123 /**
124 * the @view annotation, if it exists
125 */
126 private final View view;
127
128 /**
129 * ctor
130 */
131 public PragmatachRoute(Method method) throws PragmatachException {
132 this.method = method;
133 view = findView();
134 route = method.getAnnotation(Route.class);
135 routeSpecification = new RouteSpecification(route.uri());
136 beforeMethods = findBeforeMethods();
137 afterMethods = findAfterMethods();
138 /*
139 * check the route
140 */
141 checkRouteSpecificationSanity();
142 }
143
144 /**
145 * check that the route specification makes sense
146 */
147 private void checkRouteSpecificationSanity() throws PragmatachException {
148 try {
149 /*
150 * wildcards are special
151 */
152 if (false == isWildcardRoute()) {
153 /*
154 * there are parameters?
155 */
156 if (false == ((method.getParameterTypes().length == 0) && (0 == routeSpecification.getIds().size()))) {
157 /*
158 * number of route specification ids must match number of method parameters
159 */
160 if (method.getParameterTypes().length != routeSpecification.getIds().size()) {
161 throw new PragmatachException("Parameter number mismatch. Method '" + method.getDeclaringClass().getName() + ":" + method.getName() + "' has '" + method.getParameterTypes().length
162 + "' parameters, but route has '" + routeSpecification.getIds().size() + "'");
163 }
164 /*
165 * check that the number of supplied variable bindings annotations matches the number of parameters
166 */
167 if (method.getParameterAnnotations().length != method.getParameterTypes().length) {
168 throw new PragmatachException("Annotation number mismatch. Method '" + method.getDeclaringClass().getName() + ":" + method.getName() + "' has '"
169 + method.getParameterAnnotations().length + "' annotated parameters, but method has '" + method.getParameterTypes().length + "' parameters");
170 }
171 /*
172 * each bound name in the method must match up with a id in the route specification
173 */
174 final List<RouteParameter> routeParameters = getBoundRouteParameters();
175 if ((null != routeParameters) && (routeParameters.size() > 0)) {
176 for (final RouteParameter routeParameter : routeParameters) {
177 final String boundName = routeParameter.name();
178 if (false == routeSpecification.getIds().contains(boundName)) {
179 throw new PragmatachException("Route specfication does not specify an id for bound variable '" + boundName + "'");
180 }
181 }
182 } else {
183 throw new PragmatachException("Bound parameter number mismatch. '" + routeSpecification.getIds().size() + "' annotations were expected but none were found");
184 }
185 }
186 } else {
187 /*
188 * method should have a single parameter
189 */
190 if (1 != method.getParameterTypes().length) {
191 throw new PragmatachException("Parameter number mismatch. Method '" + method.getDeclaringClass().getName() + ":" + method.getName()
192 + "' is bound to a wildcard route and must be a a single parameter");
193 }
194 }
195 } catch (final Exception e) {
196 throw new PragmatachException("Exception in checkRouteSpecificationSanity", e);
197 }
198 }
199
200 @Override
201 public int compareTo(PragmatachRoute pragmatachRoute) {
202 if (pragmatachRoute.scopes(this)) {
203 return -1;
204 } else if (scopes(pragmatachRoute)) {
205 return 1;
206 } else {
207 /*
208 * neither route scopes the other, for the purposes of sorting they are equal
209 */
210 return 0;
211 }
212 }
213
214 /**
215 * find the after methods
216 */
217 private Set<Method> findAfterMethods() throws PragmatachException {
218 try {
219 final Class<?> controllerClass = method.getDeclaringClass();
220 return findAfterMethods(controllerClass);
221 } catch (final Exception e) {
222 throw new PragmatachException("Exception in findBeforeMethods", e);
223 }
224 }
225
226 /**
227 * recursively find all after methods
228 */
229 private Set<Method> findAfterMethods(Class<?> clazz) throws PragmatachException {
230 try {
231 /*
232 * ret
233 */
234 final Set<Method> ret = new HashSet<Method>();
235 /*
236 * walk the methods
237 */
238 for (final Method method : clazz.getDeclaredMethods()) {
239 for (final Annotation annotation : method.getAnnotations()) {
240 if (annotation.annotationType() == AfterInvoke.class) {
241 ret.add(method);
242 }
243 }
244 }
245 /*
246 * superclass
247 */
248 final Class<?> superClass = clazz.getSuperclass();
249 if (null != superClass) {
250 ret.addAll(findBeforeMethods(superClass));
251 }
252 /*
253 * done
254 */
255 return ret;
256 } catch (final Exception e) {
257 throw new PragmatachException("Exception in findBeforeMethods", e);
258 }
259 }
260
261 /**
262 * find the before methods
263 */
264 private Set<Method> findBeforeMethods() throws PragmatachException {
265 try {
266 final Class<?> controllerClass = method.getDeclaringClass();
267 return findBeforeMethods(controllerClass);
268 } catch (final Exception e) {
269 throw new PragmatachException("Exception in findBeforeMethods", e);
270 }
271 }
272
273 /**
274 * recursively find all before methods
275 */
276 private Set<Method> findBeforeMethods(Class<?> clazz) throws PragmatachException {
277 try {
278 /*
279 * ret
280 */
281 final Set<Method> ret = new HashSet<Method>();
282 /*
283 * walk the methods
284 */
285 for (final Method method : clazz.getDeclaredMethods()) {
286 for (final Annotation annotation : method.getAnnotations()) {
287 if (annotation.annotationType() == BeforeInvoke.class) {
288 ret.add(method);
289 }
290 }
291 }
292 /*
293 * superclass
294 */
295 final Class<?> superClass = clazz.getSuperclass();
296 if (null != superClass) {
297 ret.addAll(findBeforeMethods(superClass));
298 }
299 /*
300 * done
301 */
302 return ret;
303 } catch (final Exception e) {
304 throw new PragmatachException("Exception in findBeforeMethods", e);
305 }
306 }
307
308 /**
309 * find the view declaration for the route
310 * <p>
311 * Firstly, look for an annotation on the method, then the class.
312 * </p>
313 */
314 protected View findView() throws PragmatachException {
315 try {
316 /*
317 * first check the method
318 */
319 View ret = method.getAnnotation(View.class);
320 if (null == ret) {
321 /*
322 * check the class
323 */
324 ret = method.getDeclaringClass().getAnnotation(View.class);
325 }
326 return ret;
327 } catch (final Exception e) {
328 throw new PragmatachException("Exception in findView", e);
329 }
330 }
331
332 public Set<Method> getAfterMethods() {
333 return afterMethods;
334 }
335
336 public Set<Method> getBeforeMethods() {
337 return beforeMethods;
338 }
339
340 /**
341 * get bound parameters annotations, in order of the parameters in the method
342 */
343 public List<RouteParameter> getBoundRouteParameters() {
344 final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
345 if (parameterAnnotations.length > 0) {
346 final List<RouteParameter> ret = new ArrayList<RouteParameter>();
347 for (final Annotation[] p : parameterAnnotations) {
348 for (final Annotation annotation : p) {
349 if (annotation.annotationType() == RouteParameter.class) {
350 ret.add((RouteParameter) annotation);
351 break;
352 }
353 }
354 }
355 if (ret.size() > 0) {
356 return ret;
357 } else {
358 return null;
359 }
360 } else {
361 return null;
362 }
363 }
364
365 /**
366 * get a class instance of the controller, and return a proxy that allows us to intercept method calls
367 */
368 public PragmatachController getControllerClazzInstance(Request request) throws PragmatachException {
369 try {
370 /*
371 * get the actual class type
372 */
373 final Class<?> controllerClazz = method.getDeclaringClass();
374 /*
375 * enhance it
376 */
377 // final Enhancer enhancer = new Enhancer();
378 // enhancer.setSuperclass(controllerClazz);
379 // enhancer.setCallback(new ControllerMethodInterceptor());
380 // return (PragmatachController) enhancer.create();
381 return (PragmatachController) controllerClazz.newInstance();
382 } catch (final Exception e) {
383 throw new PragmatachException("Exception in getControllerClazzInstance", e);
384 }
385 }
386
387 public String getDescription() {
388 return getRoute().uri() + " " + getMethod().getDeclaringClass().getName() + ":" + getMethod().getName();
389 }
390
391 public Method getMethod() {
392 return method;
393 }
394
395 /**
396 * number of method parameters
397 */
398 public int getParameterCount() {
399 return method.getParameterTypes().length;
400 }
401
402 public Route getRoute() {
403 return route;
404 }
405
406 public RouteSpecification getRouteSpecification() {
407 return routeSpecification;
408 }
409
410 /**
411 * number of route segments
412 */
413 public int getSegmentCount() {
414 if (null != routeSpecification.getSegments()) {
415 return routeSpecification.getSegments().size();
416 }
417 return 0;
418 }
419
420 public View getView() {
421 return view;
422 }
423
424 public boolean isWildcardRoute() {
425 final String uri = route.uri();
426 return (true == uri.endsWith("*"));
427 }
428
429 /**
430 * returns true this route is more general than the passed route, false otherwise
431 */
432 public boolean scopes(PragmatachRoute pragmatachRoute) {
433 if (null != pragmatachRoute) {
434 final boolean ret = scopes(route.uri(), pragmatachRoute.route.uri());
435 if (ret) {
436 logger.debug(route.uri() + " scopes " + pragmatachRoute.route.uri());
437 } else {
438 logger.debug(route.uri() + " doesn't scope " + pragmatachRoute.route.uri());
439 }
440 return ret;
441 } else {
442 return false;
443 }
444 }
445 }