In my two previous blog posts I have introduced Ratpack and the lightweight ODM layer available in our Java SDK. This post will build up on them and showcase a REST API to manage users through the Couchbase repository.
I want my API to support the four basic HTTP verbs and to support content-type. Everything has been thought out for that in Ratpack. The Chain object is like a builder for composing handlers. So I wrote a new Action class to bind a chain to the 'user' URL prefix. This will simplify my main method a lot:
1 2 3 4 5 6 |
public static void main(String... args) throws Exception { RxRatpack.initialize(); RatpackServer.start(server -> server.registry(Guice.registry(b -> b.module(Config.class))) .handlers(chain -> chain.prefix("user", UserHandler.class))); } |
To make sure my UserHandler instance can be found, I also need to add it to my Config Guice module:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class Config extends AbstractModule { protected void configure() { CouchbaseCluster cc = CouchbaseCluster.create(); Bucket bucket = cc.openBucket(); bind(AsyncBucket.class).toInstance(bucket.async()); bind(AsyncRepository.class).toInstance(bucket.repository().async()); bind(UserHandler.class); bind(UserRenderer.class); } } |
I also add the UserRenderer class that will be used to handle the different content-types.
Custom Renderer
In Ratpack you can register new RendererSupport classes. Their goal is to specify a way to render your T object based on the Context. In my case I want to render a User object based on the context content-type. The Context object gives you a byContent method that allows to compose rendering based on the content-type set in the request. Usual types are predefined already. In my case I just want to support json and text:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class UserRenderer extends RendererSupport<User> { @Override public void render(Context context, User user) throws Exception { context.byContent(byContentSpec -> byContentSpec .json(() -> context.render(Jackson.json(user))) .plainText(() -> context.render(user.toString())) ); } } |
The text rendering is just a simple toString(). The JSON version uses the Jackson object available by default in Ratpack. It gives you access to shortcut for JSON/Object conversions. A call to the json method giving the User object as argument will do. Jackson is also used internally by our Java SDK. But there is a trick. The @Field annotation I used for my Couchabse ODM is not picked up by the default Jackson mapper used in Ratpack. So I need to add the Jackson @JsonProperty annotation to compensate. I would not have had to add anything if I did not use the @Field annotation in the first place. I will try to find a better way to make this work but in the meantime it works just fine.
1 2 3 4 5 6 7 8 |
@Field("fName") @JsonProperty("fName") private String firstName; @Field("lName") @JsonProperty("lName") private String lastName; |
Composing the API with Handlers
I am now in a good position to start working on the API. GET, PUT and DELETE need a user id to work. So the first thing I am doing is checking if there is something after the /user/ URL. Handlers are all chained together and executed in the order you declare them. Once you get in a handler, the chain stops. So you want to make sure you declare /user/:userId before /user/. Path binding uses regex, you'll find examples in the Chain documentation.
Calling the path
method allow me to give the path regex and a handler as argument. In the Handler I start by getting the Couchbase repository and the userId from the path tokens. Then I call the byMethod method to define a function for each HTTP verb I need to support. Here I will return a user for GET, update or create a user for PUT and remove the user for REMOVE.
The most interesting verb here is PUT as it requires content form the request. The Context parse
method takes a Parse object as argument and returns a Promise. Here I want to parse the JSON from the request and map it to a User object. So it returns a Promise. Since I am an RxJava user I will map that Promise to an Observable, then map the User object to an EntityDocument to finally save it with the Couchbase repository. Than I convert that observable back to a ratpack promise and send back a simple OK string. You might want to do something smarter in real life se cases :)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
chain.path(":userId", ctx -> { AsyncRepository repo = ctx.get(AsyncRepository.class); String userId = ctx.getPathTokens().get("userId"); ctx.byMethod(methodSpec -> { methodSpec.get(() -> { Observable<EntityDocument<User>> user = repo.get(userId, User.class); RxRatpack.promise(user).then(u -> ctx.render(u.get(0).content())); }).put(() -> { Promise<User> promise = ctx.parse(Jackson.fromJson(User.class)); Observable<EntityDocument<User>> observable = RxRatpack.observe(promise) .map(user -> EntityDocument.create(user)).flatMap(doc -> repo.upsert(doc)); RxRatpack.promise(observable).then(doc -> ctx.render("OK")); }).delete(() -> { Observable<EntityDocument<User>> user = repo.remove(userId, User.class); RxRatpack.promise(user).then(u -> ctx.render("OK")); }); }); }); |
Once the HTTP verbs that needs a userId are implemented, I can finish with all the others. And I can do that easily with the method all
. Here I get the repository and the bucket from the registry. Then for the GET method, since it has no user id, I return the full list of Users. To do so I run a basic N1QL query. Finally the POST is identical to the PUT method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
chain.all(ctx -> { AsyncBucket bucket = ctx.get(AsyncBucket.class); AsyncRepository repo = ctx.get(AsyncRepository.class); ctx.byMethod(methodSpec -> { methodSpec.get(() -> { N1qlQuery query = N1qlQuery.simple("SELECT Meta().id as username,age,fName,lName FROM default WHERE type = 'user'"); Observable<User> observable = bucket.query(query) .flatMap(qRes -> qRes.rows()).map(row -> { JsonObject jo = row.value(); String username = jo.getString("username"); Integer age = jo.getInt("age"); String fName = jo.getString("fName"); String lName = jo.getString("lName"); return new User(username, age, fName, lName); }); RxRatpack.promise(observable).then(users -> ctx.render(Jackson.json(users))); }).post(() -> { Promise<User> promise = ctx.parse(Jackson.fromJson(User.class)); Observable<EntityDocument<User>> observable = RxRatpack.observe(promise) .map(user -> EntityDocument.create(user)).flatMap(doc -> repo.upsert(doc)); RxRatpack.promise(observable).then(doc -> ctx.render("OK")); }); }); }); |
This may not be a totally RESTFUL API, but should give you a good idea on how easy it is to craft one with Ratpack, Couchbase and RxJava. Please let me know if you want to see more stuff happening around Ratpack, like a Couchbase Module.