Restful is composed with four principles as said in this post:
- The Swamp of POX: You’re using HTTP to make RPC calls. HTTP is only really used as a tunnel. (for WebSocket, change the word ‘HTTP’ by ‘WebSocket’)
- Resources. Rather than making every call to a service endpoint, you have multiple endpoints that are used to represent resources, and you’re talking to them. This is the very beginnings of supporting REST.
- HTTP Verbs. This is the level that something like Rails gives you out of the box: You interact with these Resources using HTTP verbs, rather than always using POST.
- Hypermedia Controls. HATEOAS. You’re 100% REST compliant.
So I tried to transpose these concept to my WebSocket project in order to be fully compliant with these wonder-use-ful concepts.
HATEOAS
To be HATEOAS respectful, you must have one single URI entrypoint for your app, then the server send you the next accessible URI for your current client STATE.
The way to do this with WebSocket is as easy as its HTTP counterpart.
As a programming Stack, I choose Stomp over websocket protocol for the client (StompJS), a Stomp to JMS ActiveMQ Broker ans some Apache Camel routes server endpoint.
Here’s a sample of Javascript Uri Entrypoint:
function getPreAuthUris(client) { var client = client || $('#mainNavbar').data("stompClient"); var idURIsQueue = client.subscribe("/queue/gotEntryPoints", function( message) { if (!heartBeatFilter(message)) { client.unsubscribe(idURIsQueue); var uris = parseUris(JSON.parse(message.body)); // auth entry, auth error, auth success createLoginMenu(uris[0], uris[1], uris[2]); // chat entry, chat topic manageChat(uris[3], uris[4]); } }); client.send("/queue/entryPoint", {}, ""); |
And its Camel counterpart:
from("{{application.entryPoint}}") .filter(header("webSocketMsgType").isNotEqualTo("heartBeat")) .enrich("direct:getEntryPointUrisInternal", uriToXmlObjectAggregationStrategy).marshal().xmljson().log("json Uris sent: ${body}").to("{{application.gotEntryPoint}}"); from("direct:getEntryPointUrisInternal").beanRef("urisBean","getPreAuthenticationUris"); public class UrisBean { @Setter private String authenticationEntryPoint; @Setter private String authenticationErrorEndPoint; @Setter private String authenticationSuccessEndPoint;... public List<String> getPreAuthenticationUris() { List<String> uris = new ArrayList<String>(); log.trace("adding an uri for the new client state: " + authenticationEntryPoint); uris.add(camelToStompUriFormat(authenticationEntryPoint)); log.trace("adding an uri for the new client state: " + authenticationErrorEndPoint); uris.add(camelToStompUriFormat(authenticationErrorEndPoint)); log.trace("adding an uri for the new client state: " + authenticationSuccessEndPoint); uris.add(camelToStompUriFormat(authenticationSuccessEndPoint)); log.trace("adding an uri for the new client state: " + chatEntryPoint); uris.add(camelToStompUriFormat(chatEntryPoint)); log.trace("adding an uri for the new client state: " + chatGeneralTopicEndpoint); uris.add(camelToStompUriFormat(chatGeneralTopicEndpoint)); return uris; } /** * Change a camel formatted uri (jms:...:...) to a stomp uri (/.../...) * @param camelFormat formatted uri * @return stomp formatted uri */ private String camelToStompUriFormat(String camelFormat) { return camelFormat.replaceAll("[^:]*:([^:]+)(:([^:]*?))?","/$1/$3"); } } |
Hopa HATEOAS style!
In the near future, if we have for example to add a new adress to the current user, we’ll use the stomp header to route to the right person:
/*Precedent messages is in the form {"uris":"{adressQueue : {"linkRel": "/queue/adress", "uri": "/user/UserId-2/adress"}}}*/ client.send(precedentMessage.uris.adressQueue.linkRel, {"uri": precedentMessage.uris.adressQueue.uri, "RESTProtocol": "POST"}, JSON.stringify({"street":139,...})); |
And the corresponding route:
from("jms:queue:adress") .setHeader(Exchange.HTTP_METHOD,constant(${header.RESTProtocol})) .setHeader(CxfConstants.CAMEL_CXF_RS_USING_HTTP_API, Boolean.TRUE) .to("cxfrs://http://localhost:9080/${header.uri}?httpClientAPI=true"); |
Thanks to camel CXFRS component!
Content negociation
Simple, CXFRS component comes with content negociation done by header:
setHeader(CxfConstants.CAMEL_CXF_RS_RESPONSE_CLASS, Customer.class); |
Or you can always use JSON, Xstream or JaxB DataFormat to convert your response.
Conclusion
I’m aware that it’s not 100% REST compliant (linkrel is tied to the server queue), but we have also decoupled the url to the performed action (the only limitation is that we can’t change this URL whithout impacting the client).
If you’ve any suggestion on this or think that I’m totally wrong (it’s just my personal way of doing it), feel free to comment!