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