Communication with REST & JAX-RS
Where we learn how to talk to Map via REST & JAX-RS
Overview
In this adventure, we’ll learn about the Representational State Transfer (REST) approach for defining services. We’ll explore how the JAX-RS specification simplifies working with REST endpoints by adding simple client capabilities to a Game On room.
Why REST ?
REST has become pervasive as a way to communicate between services, defining an easy and simple way to invoke an action against a remote endpoint.
From a microservice perspective REST is one the most important basic tools you will need, to expose your own services for others to invoke, and to call on other services yourself.
Within this tutorial we’ll look at how you can use JAX-RS, a Java API for Rest, to create a client to talk to the Game On Map Service, to have your room query it’s own information.
Prerequisites
This walkthrough starts after you have completed the Create a Room adventure, and expanding on the Java Sample Room project.
No additional accounts or services are required.
Walkthrough
Server Configuration
We’re going to create a JAX RS Client to talk to the Map service. To use JAX-RS within Liberty we need to cmake sure the server is configured to use both the jaxrs-2.0 and the cdi-1.2 features.
To do that, opensample-room-java/src/main/liberty/config/server.xml, and look for:
<feature>jaxrs-2.0</feature>
<feature>cdi-1.2</feature>
The order of these two elements is not important, they just have to be there.
Creating the client
We’ll create our MapClient as a bean we can inject via CDI.
Lets start with a simple skeleton from which we’ll build the client capability.
@ApplicationScoped
public class MapClient {
public static final String DEFAULT_MAP_URL = "https://gameontext.org/map/v1/sites";
private WebTarget queryRoot;
@PostConstruct
public void initClient() {
try {
Client queryClient = ClientBuilder.newBuilder().build();
// create the jax-rs 2.0 client
this.queryRoot = queryClient.target(DEFAULT_MAP_URL);
} catch ( Exception ex ) {
Log.log(Level.SEVERE, this, "Unable to initialize map service client", ex);
}
}
public String getMapData(String roomId) {
}
}
Here we’re creating a simple bean, and using the @PostConstruct
method to configure
a JAX RS WebTarget that’s pointing at the Game On Map Service.
With the WebTarget initialised, lets have a look at the code for getMapData
public String getMapData(String siteId) {
WebTarget target = this.queryRoot.path(siteId);
Response r = null;
try {
r = target.request(MediaType.APPLICATION_JSON).get();
if (r.getStatusInfo().getFamily().equals(Response.Status.Family.SUCCESSFUL)) {
String data = r.readEntity(String.class);
return data;
}
return null;
} catch (ResponseProcessingException rpe) {
Response response = rpe.getResponse();
Log.log(Level.FINER, this, "Exception fetching room uri: {0} resp code: {1} ",
target.getUri().toString(),
response.getStatusInfo().getStatusCode()
+ " "
+ response.getStatusInfo().getReasonPhrase());
Log.log(Level.FINEST, this, "Exception fetching room ", rpe);
} catch (ProcessingException e) {
Log.log(Level.FINEST, this, "Exception fetching room ("
+ target.getUri().toString()
+ ")",
e);
} catch (WebApplicationException ex) {
Log.log(Level.FINEST, this, "Exception fetching room ("
+ target.getUri().toString()
+ ")",
ex);
}
// Unable to obtain the room.
return null;
}
First impressions are that’s an awful lot of exception handlers for such a simple request.
We want to issue a GET
to the Map service using the url:
https://gameontext.org/map/v1/sites/{siteId}
The first line of the method, this.queryroot.path(siteId)
adds our siteId
argument
to our URL. Then we issue the request:
target.request(MediaType.APPLICATION_JSON).get()
That sends the HTTP GET request to the target URL, and returns us a Response
object. At this
stage, we do not know if the request was succesful, or if the Map service reported an error.
It’s important to understand that just because you get a Response, does not mean the request
was successful. For example, if the siteId is not found, you will recieve a 404 Response from
the Map service.
Once we have the Response, we test it to see if the Response says the request was carried out successfully. If so, then we can proceed to read the data from the Response.
There are various other ways you can end up in the Exception blocks, if the host name isn’t known, or if the connection was refused, or other network related issues. In each case, we just log the error, and return null.
If we print the string we get back from the Response, we’ll see that Map sends us a block of
JSON for the room. Here’s the Response for one of the standard rooms, RecRoom
{
"info": {
"name":"RecRoom",
"fullName":"Rec Room",
"description":"A dimly lit shabbily decorated room, that appears tired and dated. It looks like someone attempted to provide kitchen facilities here once, but you really wouldn't want to eat anything off those surfaces!",
"doors":{
"n":"A dark alleyway, with a Neon lit sign saying 'Rec Room', you can hear the feint sounds of a jukebox playing.",
"w":"The doorway has a sign saying 'Rec Room' beneath it, about halfway down the door, someone has written 'No Goblins' in crayon.",
"s":"Hidden behind piles of trash, you think you can make out the back entrance to the Rec Room.",
"e":"The window on the wall of the Rec Room looks large enough to climb through."}
},
"exits":{
"n":{"name":"creepyroom",
"fullName":"Creepy Room",
"door":"A steel door with a coffee cup.",
"_id":"edb77e1c506243ffa2dc496de6970b13"},
"w":{"name":"First Room",
"fullName":"The First Room",
"door":"A fake wooden door with stickers of friendly faces plastered all over it",
"_id":"firstroom"},
"s":{"name":"REAL",
"fullName":"rEaLItY",
"door":"A very very very very very very very very very very very very normal door",
"_id":"f9ec231dc64379be70d081e04d340f81"},
"e":{"name":"room14",
"fullName":"David o",
"door":"See 'Try East' close by",
"_id":"e784d7f9eaff39fde4b6607116bb2c16"}
},
"owner":"game-on.org",
"createdOn":"2017-02-23T21:29:53.548Z",
"assignedOn":"2017-02-23T21:29:53.549Z",
"coord":{"x":1,"y":0},
"type":"room",
"_id":"658aa51512b7cbbc3ee5d0f502525545",
"_rev":"17-547f06f5dbfa4c98e959d6978353fcaf"
}
Here you can see JSON returned containing the information supplied when the room was registered. Along with additional information related to it’s current location within the Map; coordinates, adjoining rooms, and creation timestamps.
With a little effort, we can write some code to retrieve the parts we are interested in, and
then return that from our MapClient getMapData
method as a typed object, rather than as a JSON String.
We’re only really after the name/fullname/description for our room. Lets create a bean to hold the data, so we have an object to return. This is just a really simple POJO, nothing to be amazed at ;)
public class MapData {
private String name;
private String fullName;
private String description;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
Lets update the MapClient getMapData
method to parse the JSON and populate the POJO.
Change the return type of the method to be the new MapData
class, and then remove the
line return data;
and substitute this block of code to process the returned data.
try {
rdr = Json.createReader(new StringReader(data));
JsonObject returnedJson = rdr.readObject();
JsonObject info = returnedJson.getJsonObject("info");
MapData mapData = new MapData();
mapData.setName(info.getString("name",null));
mapData.setFullName(info.getString("fullName",null));
mapData.setDescription(info.getString("description",null));
return mapData;
} finally {
if (rdr != null) {
rdr.close();
}
}
That’s enough to get us a basic functional MapClient that we can use to retrieve the name/fullName/description for any room.
Using the client
Now let’s look at wiring that client to our Room. We’ll have our room look up it’s data from the map, and have it use that, instead of the data we’ve supplied as defaults within RoomDescription.
Our first challenge is discovering our room id, we could cut & paste it
into the code manually from the room registration. Or we could inject it
via an environment variable (then via jndi, and `@Resource
or @Inject
).
There’s a third, simpler option. We can use the id as sent to us in each Game On message sent to our room.
Every time Game On sends a message to a room, it includes the id of the room it’s talking to as part of the routing information in the message.
One of the first messages the room receives is roomHello
, to which we would
normally respond with the location
message that supplies Game On with the
room description etc.
We’ll update the logic so that once we receieve our roomHello
we’ll make a
quick call to Map to retrieve the description, and then use that data to give
back to Game On.
The roomHello
handler today lives over in RoomImplementation
and looks
like this.
case roomHello:
// roomHello,<roomId>,{
// "username": "username",
// "userId": "<userId>",
// "version": 1|2
// }
// See RoomImplementationTest#testRoomHello*
// Send location message
endpoint.sendMessage(session, Message.createLocationMessage(userId, roomDescription));
// Say hello to a new person in the room
endpoint.sendMessage(session,
Message.createBroadcastEvent(
String.format(HELLO_ALL, username),
userId, HELLO_USER));
break;
If we look a little above the block, we can see the switch statement, using message.getTarget
to obtain the message type for evaluation. The message
object offers another method,
getTargetId
which will return us the roomId for the recieved message.
Lets start by injecting the MapClient to the RoomImplementation
. Add a field declaration
with an @Inject
annotation like this.
@Inject
MapClient mapClient;
That will cause CDI to inject an instance of the MapClient
class into RoomImplementation
,
which we’ll use to lookup our room details.
Revisit the roomHello
block we identified earlier, and before sending the location
message, add this code;
String roomId = message.getTargetId();
MapData data = mapClient.getMapData(roomId);
if(data!=null){
roomDescription.setDescription(data.getDescription());
roomDescription.setName(data.getName());
roomDescription.setFullName(data.getFullName());
}
You can verify this now if you deploy the room, edit the room description using
the room registration user interface, and then visit your room. When you enter the room
will use the description from the data registered
in map, rather than the hardcoded defaults in the RoomDescription
class.
Improving the usage
Great, except now we’re making a request to update that info every time anyone enters the room, and we really should consider caching that information, as its unlikely it changes frequently.
Lets add a field to store the MapData within the RoomImplementation
class. Near where
you added the MapClient
injection, add..
MapData data = null;
Then, update the block we just added to only perform the get if we haven’t done one yet.
String roomId = message.getTargetId();
if(data==null){
data = mapClient.getMapData(roomId);
if(data!=null){
roomDescription.setDescription(data.getDescription());
roomDescription.setName(data.getName());
roomDescription.setFullName(data.getFullName());
}
}
That’s pretty good, we could even add a simple command in the processCommand
block that could wipe the cached data so it can be refreshed;
case "/clearcache":
data = null;
endpoint.sendMessage(session,
Message.createSpecificEvent(userId, "Cache Cleared."));
break;
Now when you connect to the room, you can issue /clearcache
and exit & re-enter
the room to have it pick up changes made via the room registration interface.
Example in github.
In case you just want to see what it can look like when it’s all put together, we’ve got a git repo you may want to check out. (Pun intended.)
Suggested extensions
This has been a simple look at REST, using a single 'GET' operation. The Map API supports many others, and the Player service has a REST API also.
You could try using the Player REST API to track the location of players who were in your room recently.
You could expand your room service to host multiple rooms behind a single endpoint, and use the RoomID from room hello to lookup which description you should return when a user connects. Remember to cache the MapData for each ID!
Conclusion
By following this guide, you have created a basic JAX-RS client, and used it to invoke the REST API of the Map service to look up your rooms details.
Suggested further adventures.
You may want to consider the JSR107 Caching example to see how you could create a cache for the MapData that would automatically expire after a defined period of time.