Hi,
(WARNING: Longish email on how to do REST properly)
Based on my experience with REST API design in Streamflow, I want to
investigate a redesign of how we do REST client and server libraries,
specifically in order to enable Level 3 REST API's, meaning full support
for hypermedia to drive state transitions in an application.
The server part has already been reasonably explored in Streamflow, and
the result is that use cases should be exposed, rather than the
traditional domain objects. This makes it possible to make the client
very very thin, and all business rules remain on the server where they
belong. The current REST library code in Git relies on DCI being used,
but I intend to factor this out so that you can choose to do the
traditional "service+anemic domain" if you want to. The main point is to
support exposing REST resources that focus on use cases and hypermedia,
whether or not DCI is used.
On the client, I have been thinking a lot about what Roy wrote in his
thesis, which boils down to "client makes GET request, based on relation
of link invoked and response (headers+hypermedia) make a decision, and
then follow links or invoke forms to perform state transitions". It's a
state machine. This is different from your average REST client today in
that this model explicitly says that you need to do a GET first
(otherwise you have nothing to react to), that the "rel" of the link you
followed is important (otherwise context is unknown), and that the
client should not make assumptions about what comes back (otherwise you
cannot deal with exceptions, on system and application level).
The current REST client approach which is imperative:
result = client.get(somelink)
does not allow for any of the above. Instead, the client code should
first register handlers for what to do in various circumstances, and
then simply perform one operation: "refresh". This will trigger the
first GET to the bookmarked URL, and after that the handlers will do all
the work, based on the result. A handler may continue the work by
invoking new requests, or it may abort. "refresh" does not return any
value, and usually does not throw any exceptions.
Example:
crc.onResource( new ResultHandler<Resource>()
{
@Override
public void handleResult( Resource result, ContextResourceClient
client )
{
// This may throw IAE if no link with relation
// "querywithoutvalue" is found in the Resource
client.query( "querywithoutvalue", null );
}
} ).
onQuery( "querywithoutvalue", TestResult.class, new
ResultHandler<TestResult>()
{
@Override
public void handleResult( TestResult result, ContextResourceClient
client )
{
Assert.assertThat( result.xyz().get(), CoreMatchers.equalTo(
"bar" ) );
}
} );
crc.refresh();
---
The client first builds up the set of handlers, and describe what they
should react to. The client then invokes "refresh" which will trigger
the first GET on the bookmark URL. This will return a representation of
that context as a Resource, and the handler for that is invoked. This
then invokes the link with relation "querywithoutvalue" with no input
(no request parameters needed). The result of that is then handled by
another handler, and the invocation of "refresh" then returns
successfully. Note that the first handler may not directly handle the
"result" of client.query("querywithoutvalue, null) as it cannot be
assumed what happens next. All you know is that you are following a link.
On crc (ContextResourceClient) it is also possible to registers handlers
that are always applied, such as error handlers. Here is the setup of crc:
// Create Restlet client and bookmark Reference
Client client = new Client( Protocol.HTTP );
Reference ref = new Reference( "http://localhost:8888/" );
ContextResourceClientFactory contextResourceClientFactory =
module.newObject( ContextResourceClientFactory.class, client, new
NullResponseHandler() );
contextResourceClientFactory.setAcceptedMediaTypes(
MediaType.APPLICATION_JSON );
// Handle logins
contextResourceClientFactory.setErrorHandler( new
ErrorHandler().onError( ErrorHandler.AUTHENTICATION_REQUIRED, new
ResponseHandler()
{
// Only try to login once
boolean tried = false;
@Override
public void handleResponse( Response response,
ContextResourceClient client )
{
// If we have already tried to login, fail!
if (tried)
throw new ResourceException( response.getStatus() );
tried = true;
client.getContextResourceClientFactory().getInfo().setUser(
new User("rickard", "secret") );
// Try again
client.refresh();
}
} ).onError( ErrorHandler.RECOVERABLE_ERROR, new ResponseHandler()
{
@Override
public void handleResponse( Response response,
ContextResourceClient client )
{
// Try to restart this scenario
client.refresh();
}
} ) );
crc = contextResourceClientFactory.newClient( ref );
---
These general handlers cover what to do for login and error handling,
for example. In the traditional REST client this is not as easy to do,
as you are more or less assuming a "happy path" all the time. In the
above scenario there could be any number of steps between doing
"refresh" and getting to the meat of the use case, such as doing a
signup for the website, login, redirects to other servers, error
handling and retries, etc. It becomes possible to blend general
application and error handling logic with use case specific handlers.
That's basically it. This is where I want to go with support for REST,
as a way to truly leverage the REST ideas and make it very easy to do
REST applications *and* clients based on Qi4j, by keeping the
application logic on the server. In the long run there would also be a
JavaScript version of the client, with the same characteristics, so that
you can easily build a jQuery UI for Qi4j REST apps.
Thoughts on that?
/Rickard
_______________________________________________
qi4j-dev mailing list
[email protected]
http://lists.ops4j.org/mailman/listinfo/qi4j-dev