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 }