Friday, September 18, 2009

java.lang.reflect.Proxy client based on Jersey with a bit of HATEOAS built in

Many people prefer the kind of dynamic fluent API that Jersey provides for calling RESTful services. This doesn't suit every situation though and in some cases it would be nice to have a statically typed interface. RESTEasy provides something along these lines; but I wanted something that worked with Jersey and to take it a bit further to support basic HATEOAS.

So consider the following two interfaces and one bean class that make up the service we are trying to call. Note that there isn't a one to one mapping with the server classes as unlike with SOAP/WSDL you can be a bit flexible. It wouldn't make sense to generate a static client that supports both XML and JSON content types for example where-as the server would support both.

package bucketservice;

import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;

@Path("buckets")
public interface Buckets {

    @POST
    public Bucket createNewBucketBound(String input);


    @GET
    @Produces("application/buckets+xml")
    public BucketList getBuckets();
    
    
    @Path("/{id}")
    public Bucket getBucket(@PathParam("id") String id);
}

Note there is a method for getting a bucket from an "id" property; but more likely you are going to want to create a resource directly from the URI. This you can do easily as we will see later.

The bucket itself is really simple as it just exposes the get and delete services.

package bucketservice;

import javax.ws.rs.DELETE;
import javax.ws.rs.GET;

public interface Bucket {

    @GET 
    public String getBucket();

    @DELETE 
    public Response.Status delete();

}

The bean class is straight forward enough but there is an extra annotation @MappedResource which is used to produce an extra method that is in terms of the resource rather than URIs. Currently this work is done with a Mk1 Human Generator but it wouldn't take too long to implement with with the APT.

package bucketservice;

import javax.xml.bind.annotation.XmlRootElement;

import proxy.MappedResource;


@XmlRootElement
public class BucketList 
{
    
    URI list[];

    public void setList(URI[] list) {
        this.list = list;
    }

    @MappedResource(Bucket.class)
    public URI[] getList() {
        return list;
    }
}

So the key part of the client example below is a static "of" method on ClientProxy that takes as it's input a web resource and the interface you want to use to talk to this resource and return a dynamic proxy based on the interface. This resource location shouldn't include any sub path information included on the interface.

The call to createNewBucketBound(...) results in a HTTP response of "201 Created" with the location of the resource as a URI. The proxy code knows the return type so will return a new proxy for the Bucket interface as determined by the return type of the method. You can then go off and happily invoke methods on this such as get or delete as if it was the real interface.

The rest of the method gets hold of a list of buckets, again these are dynamic proxies based on the interface given; deletes the resource, then show the list again to be sure.

package bucketclient;


import bucketservice.Bucket;
import bucketservice.BucketList;
import bucketservice.Buckets;

import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.WebResource;

import static java.lang.System.out;

import java.net.URI;

import static proxy.ClientProxy.of;

import proxy.hateoas.HATEOASClientConfig;
 

public class BucketClient {
    public static void main(String[] args) {

        Client client = Client.create(new HATEOASClientConfig());
        WebResource rootResource = client.resource("http://localhost:7101/RSProxyClient-BucketService-context-root/jersey/");
        
        // Proxy

        Buckets buckets = of(rootResource, Buckets.class);
        out.println("Using proxy " + buckets);
        
        // Create me two buckets, note nothing is returned in the body of
        // the response message, just a location in the header. 
        //
        
        Bucket firstBucketRSP = 
            buckets.createNewBucketBound("First Bucket"); // POST -> 201
        Bucket secondBucketRSP = 
            buckets.createNewBucketBound("Second Bucket"); // POST -> 201
        
        // Get the contents of each
        //

        out.println("<Contents of buckets>");
        out.println(firstBucketRSP + "First # " 
           + firstBucketRSP.getBucket()); // GET .../id -> 200 text/plain
        out.println(secondBucketRSP + "# " 
           + secondBucketRSP.getBucket()); // GET .../id -> 200 text/plain
        out.println("</Contents of buckets>");
        
        // Get the list of buckets, use the injected getListAsResource method
        // to get bound interfaces
        //

        BucketList bucketList 
            = buckets.getBuckets(); // GET .../ -> 200 application/buckets+xml
        out.println("<Bucket List>");
        for (Bucket next : bucketList.getListAsResources()) {
            out.println("   " + next);
        }
        out.println("</Bucket List>");

        // Remove our buckets using the interface we had before
        //
        
        firstBucketRSP.delete(); // DELETE .../id -> 200
        secondBucketRSP.delete(); // DELETE ../id -> 200
        
        // Trace out bucket list again
        
        bucketList 
            = buckets.getBuckets(); // GET .../ -> 200 application/buckets+xml
        out.println("<Bucket List After Delete>");
        if (bucketList.getListAsResources()!=null)
        {
            for (Bucket next : bucketList.getListAsResources()) {
                out.println("   " + next);
            }
        }
        out.println("</Bucket List After Delete>");
        
    }
}

The getListAsResource(..) method as you might have noticed is not part of the BucketList interface and would be generated based on the @MappedResource annotation. How this this would happen I am not entirely sure; but you can start to see the start of a HATEOAS enabled client. Basically you can access the next resource along without any further work. You can imagine a "Transfer" bean that exposed the resources for the "Bank" at each end of the transfer. The client can deal with wiring this all up for you.

So the output of the run looks like this, note that all the Is-A object are dynamic proxies of the resource.

Using proxy Is-A:bucketservice.Buckets@[uri=http://localhost:7101/RSProxyClient-BucketService-context-root/jersey/buckets]
<Contents of buckets>
Is-A:bucketservice.Bucket@[uri=http://localhost:7101/RSProxyClient-BucketService-context-root/jersey/buckets/2]First # First Bucket
Is-A:bucketservice.Bucket@[uri=http://localhost:7101/RSProxyClient-BucketService-context-root/jersey/buckets/3]# Second Bucket
</Contents of buckets>
<Bucket List>
   Is-A:bucketservice.Bucket@[uri=http://localhost:7101/RSProxyClient-BucketService-context-root/jersey/buckets/2]
   Is-A:bucketservice.Bucket@[uri=http://localhost:7101/RSProxyClient-BucketService-context-root/jersey/buckets/3]
</Bucket List>
<Bucket List After Delete>
</Bucket List After Delete>

I hope you can see by example how easy it is to convert a URI into a strongly typed interface.

   URI bucket = ....
   Bucket bucketIF = of(client.resource(bucket), Bucket.class);

Ideally the interfaces would be generated from a sub set of a WADL, you would want to be able to filter by content-type and resource path. Again this kind of focused generation would have been much harder with JAX-WS / WSDL.

The code for this is still on my laptop, except for the code generation, and unfortunately it is not something I can distribute at the moment. This is something I am going to look into if people find this approach interesting.

Tuesday, September 15, 2009

Wssp1.2-2007-Https-ClientCertReq.xml required further configuration

Just a quick post to note a problem I found with the above mentioned security policy. This policy should enabled mutual or two-way https; but you will find that if you deploy this service to what appears to be a properly configured service that it will fail:

@WebService
@Policy(uri="policy:Wssp1.2-2007-Https-ClientCertReq.xml")
public class HelloTwoWay {
   public String sayHello(String name)
   {
      return "Hello " + name;
   }
}

You need another step compared with other https policies to have this work. You need to go to Servers -> [ServerName] -> SSL -> Advanced and under "Two Way Cert Behaviour" you need at least "Client Certs Requested". You can go for the enforced option if you want to use mutual everywhere; but in that case you can use the more general https policies so it doesn't really make sense.

Thursday, September 10, 2009

A programatic implementation of dump stack trace

When you program hangs you can always ask the user to perform a dump stack trace; jstack or run visual vm; but when you are trying to run automated UI tests there isn't a user on hand so you need a programmatic solution. Now you can do something with Threads and ThreadGroup classes; but you can get a much better dump of current thread state using management beans. This sound complicated right, well it turns out that there are nice static methods to get hold of those beans for the current VM.

Here is a simple example using a JFrame to give some interest to the output:

package management;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

import javax.swing.JFrame;


public class DumpStackTrace {

    public static void main(String[] args) {

        JFrame frame = new JFrame();
        frame.setVisible(true);

        new DumpStackTrace().dumpStack();
        
        System.exit(0);
    }
    
    public synchronized void  dumpStack() {
        
        ThreadMXBean theadMxBean = ManagementFactory.getThreadMXBean();
        
        for (ThreadInfo ti : theadMxBean.dumpAllThreads(true, true)) {
            System.out.print(ti.toString());
        }
    }
}

This is the output on the console, note in this form you can even see the result of the synchronized on the dumpStack method.

"AWT-EventQueue-0" Id=14 RUNNABLE (in native)
 at sun.awt.windows.WComponentPeer._requestFocus(Native Method)
 at sun.awt.windows.WComponentPeer.requestFocus(WComponentPeer.java:586)
 at java.awt.Component.requestFocusHelper(Component.java:7260)
 at java.awt.Component.requestFocusInWindow(Component.java:7151)
 at java.awt.DefaultKeyboardFocusManager.dispatchEvent(DefaultKeyboardFocusManager.java:361)
 at java.awt.Component.dispatchEventImpl(Component.java:4373)
 at java.awt.Container.dispatchEventImpl(Container.java:2081)
 at java.awt.Window.dispatchEventImpl(Window.java:2458)
 ...

"AWT-Windows" Id=10 RUNNABLE
 at java.awt.KeyboardFocusManager.shouldNativelyFocusHeavyweight(KeyboardFocusManager.java:2386)
 -  locked java.util.LinkedList@ff057f
 at sun.awt.windows.WToolkit.eventLoop(Native Method)
 at sun.awt.windows.WToolkit.run(WToolkit.java:291)
 at java.lang.Thread.run(Thread.java:619)

"AWT-Shutdown" Id=11 WAITING on java.lang.Object@b8deef
 at java.lang.Object.wait(Native Method)
 -  waiting on java.lang.Object@b8deef
 at java.lang.Object.wait(Object.java:485)
 at sun.awt.AWTAutoShutdown.run(AWTAutoShutdown.java:259)
 at java.lang.Thread.run(Thread.java:619)

"Java2D Disposer" Id=9 WAITING on java.lang.ref.ReferenceQueue$Lock@1342ba4
 at java.lang.Object.wait(Native Method)
 -  waiting on java.lang.ref.ReferenceQueue$Lock@1342ba4
 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:116)
 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:132)
 at sun.java2d.Disposer.run(Disposer.java:125)
 at java.lang.Thread.run(Thread.java:619)

"Attach Listener" Id=5 RUNNABLE

"Signal Dispatcher" Id=4 RUNNABLE

"Finalizer" Id=3 WAITING on java.lang.ref.ReferenceQueue$Lock@10a6ae2
 at java.lang.Object.wait(Native Method)
 -  waiting on java.lang.ref.ReferenceQueue$Lock@10a6ae2
 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:116)
 at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:132)
 at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159)

"Reference Handler" Id=2 WAITING on java.lang.ref.Reference$Lock@ef2c60
 at java.lang.Object.wait(Native Method)
 -  waiting on java.lang.ref.Reference$Lock@ef2c60
 at java.lang.Object.wait(Object.java:485)
 at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116)

"main" Id=1 RUNNABLE
 at sun.management.ThreadImpl.dumpThreads0(Native Method)
 at sun.management.ThreadImpl.dumpAllThreads(ThreadImpl.java:374)
 at management.DumpStackTrace.dumpStack(DumpStackTrace.java:26)
 -  locked management.DumpStackTrace@4a6cbf
 at management.DumpStackTrace.main(DumpStackTrace.java:17)

Simple and works a treat, just merging this code into our abbot runner.

Update 27 Jan 2011: One of the annoying problems with this solution is that it only prints out the first 8 items of the stack trace which might not be enough, turns out it is easy enough to trace out the rest

package management;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;

import javax.swing.JFrame;


public class DumpStackTrace {

    public static void main(String[] args) {

        JFrame frame = new JFrame();
        frame.setVisible(true);

        new DumpStackTrace().dumpStack(System.out);
        
        System.exit(0);
    }
    
    public synchronized void  dumpStack(PrintStream ps) {
        
        ThreadMXBean theadMxBean = ManagementFactory.getThreadMXBean();
        
        for (ThreadInfo ti : theadMxBean.dumpAllThreads(true, true)) {
            System.out.print(ti.toString());

           // ThreadInfo only prints out the first 8 lines, so make sure
           // we write out the rest
           StackTraceElement ste[] = ti.getStackTrace();
           if (ste.length > 8)
           {
             ps.println("[Extra stack]");
             for (int element = 8; element < ste.length; element++)
             {
                ps.println("\tat " + ste[element]);
                for (MonitorInfo mi : ti.getLockedMonitors()) {
                   if (mi.getLockedStackDepth() == element) {
                       ps.append("\t-  locked " + mi);
                       ps.append('\n');
                   }
                }
             }
             ps.println("[Extra stack]");
           }
        }
    }
}

Validating annotations at compile time, example using JAX-RS

I am in the process of re-reading Effective Java now that I have gotten around to buying the second edition. I always enjoy reading anything that Bloch puts out and I always learn something new. I was working my way through item 35 "Prefer Annotations to Naming conventions" when I noticed the following statemen that was talking about validating the annotations:

"... It would be nice if the compiler could enforce this restriction, but it can't. There are limits to how much error checking the compiler can do...."

Now it is normally very hard find something that you think that Mr Bloch has got wrong, and also be right; but I think this validation is very possible. Recently I have been looking at Project Lombok which does interesting things with the annotation processor. The general idea behind the annotation processors is that they give the ability to generate new code; but it occurred to me that an annotation processor can just check source files for errors. This allows you to extend the java compiler to do interesting non trivial annotation validation.

Rather than deal with the simple @Test example in the book, I have a solution for that one if anybody in interested, lets instead look at a real world examples from JAX-RS web services:

package restannotationtotest;

import javax.ws.rs.GET;
import javax.ws.rs.QueryParam;

public class ExampleResource {

    @GET
    public String goodGetNoParam() {
        return "Hello";
    }

    @GET
    public String goodGetParam(@QueryParam("name")String name) {
        return "Hello " + name;
    }
    
    // This annotation will fail at deploy time    
    @GET
    public String badGet(String name) {
        return "Hello " + name;
    }
}

The last method will fail at deploy time as a HTTP GET request cannot have a method body as implied by having a method parameter that is not otherwise consumed by the framework. This is a mistake I kept on making when I started with Jersey so I though it was worth starting with. So our first goal is to flag this last method up at compile time as being in error.

In order for an annotation processor to work you need a jar file with an entry in the META-INF/services path called javax.annotation.processing.Processor. This contain a list of fully qualified processor class names in plain text. In some tools you might find that the javax.annotation.processing.Processor file is not copied to the classpath as you need to add ".Processor" to the list of file extensions copied at compile time. You also of course need a class that implements the Processor interface so my project looks like this:

First of all lets look at the basic house keeping parts of the JaxRSValidator class without worry about the meat of the class. We extend the AbstractProcessor rather than implementing the Processor interface both to gain some basic functionality and to future proof the code against interface evolution. We could have used the @SupportedAnnotationTypes annotation rather than implement the corresponding method; but for later it was handy to have a list of class literals. As you can see there is not much to the configuration:

package com.kingsfleet.rs;


import java.lang.annotation.Annotation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.Resource;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedSourceVersion;

import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.ElementScanner6;
import javax.lang.model.util.Types;

import javax.tools.Diagnostic;

import javax.ws.rs.CookieParam;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.MatrixParam;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;


@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class JaxRSValidator extends AbstractProcessor {

    private static final List<Class<? extends Annotation>> PARAMETER_ANNOTATIONS
                     = Arrays.asList(
                           CookieParam.class,
                           FormParam.class,
                           HeaderParam.class,
                           MatrixParam.class,
                           PathParam.class,
                           QueryParam.class, 
                           Context.class,
                           Resource.class);
     
    private static final List<Class<? extends Annotation>> METHOD_ANNOTATIONS
                    = Arrays.asList(
                          GET.class,
                          POST.class,
                          PUT.class,
                          DELETE.class,
                          HEAD.class,
                          OPTIONS.class);

    private static final Set<String> MATCHING_ANNOTATIONS_AS_STRING;
    static {
        Set<String> set = new HashSet<String>();
        for (Class a : METHOD_ANNOTATIONS) {
            set.add(a.getName());
        }
        // We care about path as well
        //
        set.add(Path.class.getName());
        //
        MATCHING_ANNOTATIONS_AS_STRING = Collections.unmodifiableSet(
                                           set);
    }


    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return MATCHING_ANNOTATIONS_AS_STRING;
    }

  
    // Implementation of processor
    //

}

The actual implementation of the processor is in the process method, you will notice that for most annotation processors all the work is done using a visitor or the concrete scanner implementations.

The scanner we have implemented looks for an ExecutableElement that is of type "Method". The javax.lang.model is a little bit obscure in its terminology so it did take me a little bit of time to work out that this is the correct structure. Another limitation is that you cannot drill down into the finer details of the code structure without casting this a class from com.sun.tools.javac.code.Symbol: with attendant maintenance issues of using a com.sun.* package. Fortunately for the validation I want to do I can stick with the public APIs. Hopefully a future version of Java will expand on this API.

It is a relatively simple matter after that to process each method in turn looking for a one with the @GET annotation that would suggest a message body of some kind.

    private static ElementScanner6<Void, ProcessingEnvironment> SCANNER =
        new ElementScanner6<Void, ProcessingEnvironment>() {
        @Override
        public Void visitExecutable(ExecutableElement e,
                                    ProcessingEnvironment processingEnv) {

            final Messager log = processingEnv.getMessager();
            final Types types = processingEnv.getTypeUtils();

            // Make sure for a GET all parameters are mapped to
            // to something sensible
            //

            if (e.getKind() == ElementKind.METHOD) {
                
                // GET no body
                verifyNoBodyForGET(e, log);
            }
            return null;
        }


        /**
         * Check that if we have a GET we should have no body. (Should
         *   also process OPTIONS and others)
         */
        private void verifyNoBodyForGET(ExecutableElement e,
                                        final Messager log) {
            if (e.getAnnotation(GET.class) != null) {
                
                // For each parameter check for the standard annotations
                found : for (VariableElement ve : e.getParameters()) {
                    for (Class<? extends Annotation> c : PARAMETER_ANNOTATIONS) {
                        if (ve.getAnnotation(c)!=null) {
                            break found;
                        }
                    }
                    
                    log.printMessage(Diagnostic.Kind.ERROR, 
                                     "Parameters on a @GET cannot be mapped to a request body, try one of the @*Param annotations",
                                     e);
                }
            }
        }
    };

    public boolean process(Set<? extends TypeElement> annotations,
                           RoundEnvironment roundEnv) {

        for (TypeElement annotation : annotations) {
            for (Element e : roundEnv.getElementsAnnotatedWith(annotation)) {
                SCANNER.scan(e, processingEnv);
            }
        }

        // Allow other processors in
        return false;
    }

It is important when you use the log.printMessage(...) method to include the Element as the final parameter as this allows tooling to correctly display the error/warning message location. So it is a simple matter to build this project into a jar file using the tool of your choice and then have it on the the classpath when you build ExampleResource that we defined earlier. (JDeveloper users note my previous post on Lombok on running javac "Out of Process" to get this working). Depending on your tool you should get an error message that looks something like this:

Lets look at a more complicated example from Jersey to build on our code. In this example we need the parameters in the @Path annotation to match @PathParam parameters on the matching method. In this case here are two that are fine and two that might fail at some point later on. Neither of the problems can be picked up by the compiler.

package restannotationtotest;

import javax.ws.rs.Path;
import javax.ws.rs.PathParam;

public class PathResource {
    
    // Fine
    @Path("{param}")
    public ExampleResource getOneParam(@PathParam("param") String param) {
        return null;
    }

    // Fine
    @Path("{param}/someotherText/{param1}")
    public ExampleResource getOneParam(@PathParam("param") String param, @PathParam("param1") String param1) {
        return null;
    }

    // Suspect
    @Path("{param}")
    public ExampleResource getUnusedParam() {
        return null;
    }
    
    // Definitely broken
    @Path("{param}")
    public ExampleResource getMissMatch(@PathParam("paramMissMatch") String param) {
        return null;
    }
}

Our original code already visits all the methods so we need to simply extend the code to check that the parameters match entries on the @Path. We can also check for a zero length @PathParam argument which again is something that compiler can't do for free.

    private static ElementScanner6<Void, ProcessingEnvironment> SCANNER =
        new ElementScanner6<Void, ProcessingEnvironment>() {
        @Override
        public Void visitExecutable(ExecutableElement e,
                                    ProcessingEnvironment processingEnv) {

            final Messager log = processingEnv.getMessager();
            final Types types = processingEnv.getTypeUtils();

            // Make sure for a GET all parameters are mapped to
            // to something sensible
            //

            if (e.getKind() == ElementKind.METHOD) {
                
                // GET no body
                verifyNoBodyForGET(e, log);
                
                // Try to match path param to @Path
                verifyPathParamMatches(e, log);
            }
            return null;
        }

        /**
         * Check that if we have path param we have all the matching
         * path elements consumed.
         */
        private void verifyPathParamMatches(ExecutableElement e,
                                            final Messager log) {
            
            // Verify that we have a method that has resource
            //
            
            Path p = e.getAnnotation(Path.class);
            if (p!=null && p.value()!=null) {
                
                // Hack the resources out of the string, verify
                // path parameters, TODO write regex
                //
                List<String> resources = new ArrayList<String>();
                String path = p.value();
                final String[] splitByOpen = path.split("\\{");
                for (String bit : splitByOpen) {
                    String moreBits[] = bit.split("}");
                    if (moreBits.length >= 1 && moreBits[0].length() !=0) {
                        resources.add(moreBits[0]);
                    }
                }
                
                // If we have resource try to find path params to match
                if (resources.size() > 0) {
                    found : for (VariableElement ve : e.getParameters()) {

                        PathParam pp = ve.getAnnotation(PathParam.class);
                        String mappedPath = pp.value();
                        if (mappedPath==null || mappedPath.length()==0) {
                            log.printMessage(Diagnostic.Kind.ERROR, 
                                             "Missing or empty value",
                                             ve);
                        }
                        else if (!resources.contains(mappedPath)) {
                            log.printMessage(Diagnostic.Kind.WARNING, 
                                             "Value " + mappedPath + " doesn't map to path",
                                             ve);
                        }
                        else {
                            // Make this as processed
                            resources.remove(mappedPath);
                        }
                    }
                    
                    if (resources.size() > 0) {
                        log.printMessage(Diagnostic.Kind.WARNING, 
                                         "Unmapped path parameters " + resources.toString(),
                                         e);
                    }
                }
            }
        }

        /**
         * Check that if we have a GET we should have no body. (Should
         *   also process OPTIONS and others)
         */
        private void verifyNoBodyForGET(ExecutableElement e,
                                        final Messager log) {
            ...
        }
    };

Again you can simple compile the project with ExampleResource and PathResources using your favorite build tool and you should see something like:

Note the second method gets two warnings, the line numbers are the same.

So this contrary to the original statement it is possible to get the compilers to perform quite complex validation of annotations. I wonder if we could convince some of the annotation based JEE projects and libraries to come with a validation jar file that contains a matching processor to validate against the spec. It would save a lot of confusion. Tools developers like myself would also be able to harness the same code in code editors to provide in line error feedback in a consistent way. Interesting stuff.

Friday, September 4, 2009

Project lombok, interesting bean shortcuts with the annotation processor

I came across a mention of project lombok recently whilst looking for something else. It is a combination of annotations and annotation processors to make bean programming more declarative. For the purposes of this blog I am going to use a quick example using the @Data annotation. So for this simple class the attention of the @Data annotation will at compile time add in getters/settings equals/hashCode and a nice toString implementation.


import lombok.Data;

@Data
public class DataExample {
  
  
   private String name;
   private double score;
   private String tags[];
  
}

Now lombok has integration with Eclipse and javac, since we are using JDeveloper I am stuck with the latter. In theory all you need to do is to make sure that JDeveloper compiles with "javac" with the lombok.jar on your classpath. It turns out that at least in the more recent versions of JDeveloper we use the compiler API directly which doesn't appear to properly invoke the annotation processors in the library. (Look under lombok.jar!/META-INF/services to see how this is wired up) The trick is to configure JDeveloper to run javac "out of process":

Now before you compile JDeveloper might complain that it can't find any of the new methods; but as soon as the classpath is populated even code insight works correctly (although not inside of the DataElement class):

Using the above data you can see how the toString() method is puts together some nice tidy output:

DataExample(name=Bob, score=4.0, tags=[Fun, Tall])

I wonder if the ADF team could use something similar to reduce the amount of boilerplate code that they have to generate. Also with a little bit of work you could get far cleaner JAXB and EJB artifacts. It would also be good if this project was extended to support bound attributes transparently and perhaps create a builder which always takes far too much time in my book.