I've written two HATEOAS clients, once in Java and once in Ruby and I share your frustration. On both occasions there was a complete lack of lack of tooling support for what I was doing. For example, the REST API I was using would tell me what HTTP method to use for each hypertext control, but HttpClient doesn't let you pass in the method, so I ended up with the following ugly code (BTW all the code lives within a custom Ant task, hence the BuildException
s):
private HttpMethod getHypermediaControl(Node href, Node method,
NodeList children) {
if (href == null) {
return null;
}
HttpMethod control;
if (method == null || method.getNodeValue().equals("")
|| method.getNodeValue().equalsIgnoreCase("GET")) {
control = new GetMethod(href.getNodeValue());
} else if (method.getNodeValue().equalsIgnoreCase("POST")) {
control = new PostMethod(href.getNodeValue());
} else if (method.getNodeValue().equalsIgnoreCase("PUT")) {
control = new PutMethod(href.getNodeValue());
} else if (method.getNodeValue().equalsIgnoreCase("DELETE")) {
control = new DeleteMethod(href.getNodeValue());
} else {
throw new BuildException("Unknown/Unimplemented method "
+ method.getNodeValue());
}
control.addRequestHeader(accept);
return control;
}
This ended up being the basis for a REST client utility methods that I use.
private HttpMethod getHypermediaControl(String path, Document source)
throws TransformerException, IOException {
Node node = XPathAPI.selectSingleNode(source, path);
return getHypermediaControl(node);
}
private HttpMethod getHypermediaControl(Node node) {
if (node == null) {
return null;
}
NamedNodeMap attributes = node.getAttributes();
if (attributes == null) {
return null;
}
Node href = attributes.getNamedItem("href");
Node method = attributes.getNamedItem("method");
HttpMethod control = getHypermediaControl(href, method,
node.getChildNodes());
return control;
}
private Document invokeHypermediaControl(HttpClient client, Document node,
final String path) throws TransformerException, IOException,
HttpException, URIException, SAXException,
ParserConfigurationException, FactoryConfigurationError {
HttpMethod method = getHypermediaControl(path, node);
if (method == null) {
throw new BuildException("Unable to find hypermedia controls for "
+ path);
}
int status = client.executeMethod(method);
if (status != HttpStatus.SC_OK) {
log(method.getStatusLine().toString(), Project.MSG_ERR);
log(method.getResponseBodyAsString(), Project.MSG_ERR);
throw new BuildException("Unexpected status code ("
+ method.getStatusCode() + ") from " + method.getURI());
}
String strResp = method.getResponseBodyAsString();
StringReader reader = new StringReader(strResp);
Document resp = getBuilder().parse(new InputSource(reader));
Node rval = XPathAPI.selectSingleNode(resp, "/");
if (rval == null) {
log(method.getStatusLine().toString(), Project.MSG_ERR);
log(method.getResponseBodyAsString(), Project.MSG_ERR);
throw new BuildException("Could not handle response");
}
method.releaseConnection();
return resp;
}
With this little bit of code, I can fairly easily write clients that will traverse the hypermedia controls in the documents that are returned. The main bit that is missing is support for form parameters. Fortunately for me all of the controls I'm using are parameterless except one (I follow the rule of three in regards to refactoring). For completeness here is what that code snippet looks like:
HttpMethod licenseUpdateMethod = getHypermediaControl(
"/license/update", licenseNode);
if (licenseUpdateMethod == null) {
log(getStringFromDoc(licenseNode), Project.MSG_ERR);
throw new BuildException(
"Unable to find hypermedia controls to get the test suites or install the license");
} else if (license != null) {
EntityEnclosingMethod eem = (EntityEnclosingMethod) licenseUpdateMethod;
Part[] parts = { new StringPart("license", this.license) };
eem.setRequestEntity(new MultipartRequestEntity(parts, eem
.getParams()));
int status2 = client.executeMethod(eem);
if (status2 != HttpStatus.SC_OK) {
log(eem.getStatusLine().toString(), Project.MSG_ERR);
log(eem.getResponseBodyAsString(), Project.MSG_ERR);
throw new BuildException("Unexpected status code ("
+ eem.getStatusCode() + ") from " + eem.getURI());
}
eem.releaseConnection();
}
Now, what is should be doing is looking at the children of /license/update
to figure out what parameters need to be passed, but that will have to wait until I have two more parameterised form that I need to follow.
BTW it after all of the effort, it has been extremely satisfying and easy to modify the server without impacting the client. It felt so good that I'm surprised it isn't outlawed in some states.