Friday, February 13, 2009

CXF JAXRS Client API Preview

We've worked very hard recently on prototyping the client API for CXF JAXRS. We've tried our best to come up with something which will offer at least something new. It has not been easy but hopefully you'll acknowledge some differences. So without further ado here's the first overview of the CXF JAXRS Client API.


The proxies : design your resource classes and use them everywhere.

Proxy-based API is very familiar to CXF JAXWS users. As I've mentioned a number of times before it's our top priority to ensure JAXWS users can start playing with RESTful services similarly to how they do it with SOAP services. I can already hear people saying - sure, that RPC stuff is here again - but hold one please, proxy-based API can be made quite robust and I will dedicate a separate post to the comparison of different forms of client API that CXF JAXRS supports and talk about their pros and cons. And you know what, CXF JAXRS proxies are http-centric too. So here are some examples for a start.


ProviderFactory.getInstance().register(
new ResponseToBookNotFoundMapper());
String base = "http://localhost";
BookInterface bp = JAXRSClientFactory.create(base,
BookInterface.class);
try {
Book b = bp.getBook(123);
} catch (BookNotFound ex) {}
BookSubresource bs = b.getSubresource(1, 2);
bs.getBooks();



BookInterface and BookSubresource are proxies created by JAXRSClientFactory. You've already spent some time designing your service resource classes - no need to do it again for a client side. Proxies 'navigate' with the help of JAXRS annotations and do the remote invocations. The power of UriBuilder makes the replacement of URI template variables along the way quite straightforward really. All the JAXRS parameter annotations are handled such that the receiving end will work as expected : path, query and matrix parameters will find its way into a resulting URI while HttpHeader and Cookie will go to request headers.

The values will be encoded by default unless Encoded annotation is available - if you do plan to decode yourself then it's reasonable to expect you encode yourself in such cases.

FormParams and MultivaluedMap parameters are handled like any other parameters representing a request body - they're serialized with the help of JAXRS MessageBodyWriters. Likewise, return values are handled by JAXRS MessageBodyReaders.
Registered ResponseExceptionMappers will be given a chance to handle exceptional JAXRS Responses, othewise JAXRS WebApplicationException containing the Response will be thrown.

Consumes and Produces values control Content-Type and Accept headers.

Note that in the above example the root proxy is created on an interface. It might not always be possible to refactor your resource class to get the JAXRS annotations inherited from an interface. No worries, CGLIB proxies will be created in such cases, for root and sub-resource classes - you'll just need to be a little bit careful and avoid doing some initialization with side effects in default constructors.

With proxies you can't have Context parameters in your method signatures - but may be it's a good thing indeed. You can't also have Objects returned as sub-resource instances - I know JAXRS lets you do it and indeed it's a great way to test how well the dynamic resolution on sub-resources works :-), but it's Java afterall :-), not a big deal I think.

Proxies are HTTP clients

Every proxy is Client which represents the common capabilities for both proxy and http-centric clients. JAXRSClientFactory has few methods for common cases when headers need to be customized but here's how you can control what the proxy sends and receives :


BookProxy ps = JAXRSClientFactory.create(uri);
WebClient.client(ps).type("text/xml").accept(text/xml)
.modified(date, false);
Book b = ps.getBook();
if (b == null) {
Response r = WebClient.client(ps).getResponse();
if (r.getStatus() == 204) {
// check what the story is with
// the http-centric client
WebClient wc =
new WebClient(WebClient.client(ps), true);
Response r2 = wc.get();
// create a new proxy now
BookProxy ps2 =
JAXRSClientProxy.fromClient(wc, true);
assertEquals(
WebClient.client(ps).getCurrentURI(),
WebClient.client(ps2).getCurrentURI())
}
}


The headers set explicitly will be chosen instead of those derived from Consumes and Provides (for Content-Type and Accept). If no information is available then most likely application/xml will be set - after all, it's all mostly about XML services.
The code above also shows few more details. If Book instance is null then most likely the conditional GET has worked but we'd like to do more checks just in case, first by checking the actual Response and then using a WebClient http centric client which is initialized with our proxy's current URI and existing headers. Then we create a new proxy from a WebClient instance by inheriting its headers too.

HTTP-centric clients

WebClient represents HTTP-centric clients. I do think it's unfortunate we have no standard client API. We tried to apply the same builder pattern used by JAXRS ResponseBuilder. All the header-related methods have been done after checking the section 14 of HTTP 1.1, plus JAXRS JAXRS HttpHeaders and ResponseBuilder. Here are some examples.


WebClient wc = new WebClient("http://foo");
WebClient wc2 = new WebClient(wc.getBaseURI());
Response r = wc2.accept("text/plain").get();
Response r = wc2.post(new Book());
Book b = wc2.get(Book.class);
Book b2 = wc2.invoke("myhttpmethod",
body, Book.class);
//etc


You can get a Response or a typed object, you choose. WebClient is a Client, but the builder pattern works thanks to the covariance support.


WebClient acts like a browser




WebClient wc = new WebClient("http://foo");
// submit form to http://foo/bar/foo/baz
wc.path("bar").path("foo/baz").
form(new Form().set("a", "b")
.set("c", "d"));
wc.back();
assert(wc.getCurrentURI(),
"http://foo/bar/foo");
// get back to base URI
wc.back(fast);

assert(wc.getCurrentURI(),
"http://foo");

wc.to("http://newhost");


The power of XPath and XSLT

Sometimes you just need to look into a resulting XML, either when dealing with unexpected changes or when retrieving a subnode. So we've gone ahead and created a utility class XMLSource, to be optimized and enhanced later. You'll be able to easily get to a required XML piece in a number of ways, initial example :


XMLSource xs = new XMLSource(
(InputStream)client.get().getEntity());
Book b = xs.getNode("/books/book[1]", Book.class);


There are quite a few more things to say about what we have but I guess it's enough for now.
It's not yet finished at all - stay tuned for more updates. And we'd welcome a lot any constructive feedback - please send the comments to CXF lists or comment here or contact me directly.

And yes - thanks to JAXRS and RI (Jersey) for being innovative and inspiring.

Enjoy.

3 comments:

Unknown said...

Hi Sergey,

Nice to see CXF's own Client API. Now we have 4 different JAXRS implementations and 5 different client APIs (including the HttpClient API) :)

It would be nice if the JAXRS Client API is standardized one day (may be in JAXRS 2.0).

The ability to do Xpath query on the entity seems to be real cool.
I did not get a chance to play with your Client API yet. Looks like, apache svn is down.

Btw, do you know when CXF 2.2 would get released?

Keep up the good work.

Cheers,
Arul

Sergey Beryozkin said...

Hi Arul.
Thanks...
I think we're talking the first half of March - but I can't say it for sure

Cheers, Sergey

Anonymous said...

Hi Sergey,
I am trying to use WebClient to invoke my REST web service, but I keep getting an javax.ws.rs.WebApplicationException. When I type the same service url in a browser, it works fine and prints out the output. Am I missing something ?

Here is my client code:
WebClient wc = WebClient.create("http://localhost:8080/cxf-webservices/webservices/customerservice");
Response getresponse = wc.path("/customers").get();

Thanks
Kiran