Saturday, March 16, 2013

Fun With Active Annotations: My Little REST-Server API

These days I've been playing around a lot with Xtend's new language feature addition, called Active Annotations. It's so exciting want kind of things you can do with them. Today I prototyped a nice little REST-API (code can be found at github):

@HttpHandler class HelloXtend {

  @Get('/sayHello/:name') def sayHello() '''
    <html>
      <title>Hello «name»!</title>
      <body>
        <h1>Hello «name»!</h1>
      </body>
    </html>
  '''
 
  @Get('/sayHello/:firstName/:LastName') def sayHello() '''
    <html>
      <title>Hello «firstName» «LastName»!</title>
      <body>
        <h1>Hello «firstName» «LastName»!</h1>
      </body>
    </html>
  '''

  def static void main(String[] args) throws Exception {
    new Server(4711) => [
      handler = new HelloXtend
      start 
      join
    ]
  } 
}

This is a single class with no further framework directly running an embedded Jetty server. The interesting part here is, that you don't have to redeclare the parameters, as the active HttpHandler annotation will automatically add them to the method. See the method is overloaded but both declare zero parameters? That's usually a compiler error, but here they actually have five resp. six parameters, because my annotation adds the parameters from the pattern in the @Get annotation as well as the original parameters from Jetty's handle method signature. Just so you can use them when needed.

Not only the compiler is aware of the changes caused by the annotation, but so is the IDE. Content assist, navigation, outline views etc. just work as expected.

This is how it works

The HttpHandler implements org.eclipse.jetty.server.handler.AbstractHandler and adds a synthetic implementation of the method handle(). The effective code it adds to the class we've seen above is :

// generated, synthetic Java code
public void handle(final String target
                 , final Request baseRequest
                 , final HttpServletRequest request
                 , final HttpServletResponse response) throws IOException, ServletException {
  {
    Matcher sayHelloMatcher = Pattern.compile("\\/sayHello\\/(\\w+)").matcher(target);
    if (sayHelloMatcher.matches()) {
      String name = sayHelloMatcher.group(1);
      response.setContentType("text/html;charset=utf-8");
      response.setStatus(HttpServletResponse.SC_OK);
      response.getWriter().println(sayHello(name, target, baseRequest, request, response));
      baseRequest.setHandled(true);
    }
  }
  {
    Matcher sayHelloMatcher = Pattern.compile("\\/sayHello\\/(\\w+)\\/(\\w+)").matcher(target);
    if (sayHelloMatcher.matches()) {
      String firstName = sayHelloMatcher.group(1);
      String LastName = sayHelloMatcher.group(2);
      response.setContentType("text/html;charset=utf-8");
      response.setStatus(HttpServletResponse.SC_OK);
      response.getWriter().println(
          sayHello(firstName, LastName, target, baseRequest, request, response));
      baseRequest.setHandled(true);
    }
  }
}

Of course there's some room for optimizations :-) but I hope you get the idea. The implementation of the @HttpHandler and its processor is added below :

@Active(typeof(HttpHandlerProcessor))
annotation HttpHandler {
}

annotation Get {
 String value
}

class HttpHandlerProcessor implements TransformationParticipant<MutableClassDeclaration> {
 
  override doTransform(List<? extends MutableClassDeclaration> annotatedTargetElements, 
                       extension TransformationContext context) {
    val httpGet = typeof(Get).findTypeGlobally
    for (clazz : annotatedTargetElements) {
      clazz.extendedClass = typeof(AbstractHandler).newTypeReference
      val annotatedMethods = 
          clazz.declaredMethods.filter[findAnnotation(httpGet)?.getValue('value')!=null]
  
      // add and implement the Jetty's handle method
      clazz.addMethod('handle') [
        returnType = primitiveVoid
        addParameter('target', string)
        addParameter('baseRequest', typeof(Request).newTypeReference) 
        addParameter('request', typeof(HttpServletRequest).newTypeReference) 
        addParameter('response', typeof(HttpServletResponse).newTypeReference)
    
        setExceptions(typeof(IOException).newTypeReference
                    , typeof(ServletException).newTypeReference)
    
        body = ['''
          «FOR m : annotatedMethods»
            {
            «toJavaCode(typeof(Matcher).newTypeReference)» matcher = 
                «toJavaCode(typeof(Pattern).newTypeReference)»
                .compile("«m.getPattern(httpGet)»").matcher(target);
              if (matcher.matches()) {
                «var i = 0»
                «FOR v : m.getVariables(httpGet)»
                  String «v» = matcher.group(«i=i+1»);
                «ENDFOR»
                response.setContentType("text/html;charset=utf-8");
                response.setStatus(HttpServletResponse.SC_OK);
                response.getWriter().println(
                    «m.simpleName»(«m.getVariables(httpGet).map[it+', ']
                    .join»target, baseRequest, request, response));
  baseRequest.setHandled(true);
              }
            }
          «ENDFOR»
        ''']
      ]
  
      // enhance the handler methods
      for (m : annotatedMethods) {
        for (variable : m.getVariables(httpGet)) {
          m.addParameter(variable, string)
        }
        m.addParameter('target', string)
        m.addParameter('baseRequest', typeof(Request).newTypeReference) 
        m.addParameter('request', typeof(HttpServletRequest).newTypeReference) 
        m.addParameter('response', typeof(HttpServletResponse).newTypeReference)
      }
    }
  }

  private def getVariables(MutableMethodDeclaration m, Type annotationType) {
    // parses the pattern in @Get and returns the variable names
  }
 
  private def getPattern(MutableMethodDeclaration m, Type annotationType) {
    // replaces the variables in the pattern with (\\w+) groups, so we can use it as a regex pattern.
  }
}