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.
  }
}

6 comments:

  1. I love it. Can't wait for the final release of the next version of Xtend. I got the bleeding edge version of it to play around with the active annotation stuff and have a couple of questions.

    Has the previous way of handling active annotations changed all together? I mean are the .macro files and the "@Annotation for class { ... }" syntax obsolete and the https://github.com/DJCordhose/todomvc-xtend-gwt sample aswell?

    Are there any plans for being able to generate files other than classes, interfaces, annotations and enums as in the RegisterGlobalsContext interface? Such as XML files?

    ReplyDelete
  2. >Has the previous way of handling active annotations changed all together?
    >I mean are the .macro files and the "@Annotation for class { ... }" syntax
    >obsolete and the https://github.com/DJCordhose/todomvc-xtend-gwt sample aswell?

    Yes, we've rewrittten the initial prototype. It's now plain Xtend (or even Java).

    >Are there any plans for being able to generate files other than classes, interfaces, annotations and
    >enums as in the RegisterGlobalsContext interface? Such as XML files?

    Yes that is a planned feature.

    ReplyDelete
  3. Excellent! I like how the active annotation processors are handled with plain Xtend very much as oppose to introducing a new concept as with the .macro files :)
    Thanks for the answers!

    ReplyDelete
  4. Active annotation are really nice :-)

    Is it possible to write annotations for fields in methods?

    like:
    class MyClass {
    @MyMethodAnnotation
    def myMethod() {
    @MyFieldAnnotation
    val myfield
    }
    }

    I get errors on the field annotation all the time.

    ReplyDelete
  5. Thanks for the example.
    Just a quick information: you forget to rename the imported packages in the HelloXtend.xtext:

    "import org.example.jettyxtension.HttpHandler
    import org.example.jettyxtension.Get"

    should be:

    "import de.itemis.jettyxtension.HttpHandler
    import de.itemis.jettyxtension.Get"

    ReplyDelete
  6. Thanks for example on Xtend'active annotations.

    Appreciate your hard work, thanks

    ReplyDelete