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    }