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 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 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 . This contain a list of fully qualified processor class names in plain text. In some tools you might find that the 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 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 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 that is of type "Method". The 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 : 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 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 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 and using your favorite build tool and you should see something like:
👁 ImageNote 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.
