A Java library for accessing a JSON+HAL REST API, supporting the mapping of a client-side model to HTTP resources with automatic link traversal into associated resources.
The motivation for this library was to make it easier to write clients for Spring Data REST-exposed JPA repositories, supporting lazy-loading of associations in a similar style to JPA.
Built on Spring HATEOAS and Jackson.
Add the Maven dependency:
<dependency>
<groupId>uk.co.blackpepper.bowman</groupId>
<artifactId>bowman-client</artifactId>
<version>{release version}</version>
</dependency>
Given the following annotated model objects:
import uk.co.blackpepper.bowman.annotation.RemoteResource;
import uk.co.blackpepper.bowman.annotation.ResourceId;
@RemoteResource("/people")
public class Person {
private URI id;
private String name;
public Person() {}
public Person(String name) { this.name = name; }
@ResourceId public URI getId() { return id; }
public String getName() { return name; }
}
and
import uk.co.blackpepper.bowman.annotation.LinkedResource;
import uk.co.blackpepper.bowman.annotation.RemoteResource;
import uk.co.blackpepper.bowman.annotation.ResourceId;
@RemoteResource("/greetings")
public class Greeting {
private URI id;
private Person recipient;
private String message;
public Greeting() {}
public Greeting(String message, Person recipient)
{ this.message = message; this.recipient = recipient; }
@ResourceId public URI getId() { return id; }
@LinkedResource public Person getRecipient() { return recipient; }
public String getMessage() { return message; }
}
Client instances can be constructed and used as demonstrated below.
The HTTP requests/responses corresponding to each instruction are shown in a comment beneath.
import uk.co.blackpepper.bowman.Client;
import uk.co.blackpepper.bowman.ClientFactory;
import uk.co.blackpepper.bowman.Configuration;
...
ClientFactory factory = Configuration.builder().setBaseUri("http://...").build()
.buildClientFactory();
Client<Person> people = factory.create(Person.class);
Client<Greeting> greetings = factory.create(Greeting.class);
URI id = people.post(new Person("Bob"));
// POST /people {"name": "Bob"}
// -> Location: http://.../people/1
Person recipient = people.get(id);
// GET /people/1
// -> {"name": "Bob", "_links": {"self": {"href": "http://.../people/1"}}}
assertThat(recipient.getName(), is("Bob"));
id = greetings.post(new Greeting("hello", recipient));
// POST /greetings {"message": "hello", "recipient": "http://.../people/1"}}
// -> Location: http://.../greetings/1
Greeting greeting = greetings.get(id);
// GET /greetings/1
// -> {"message": "hello", "_links": {"self": {"href": "http://.../greetings/1"},
// "recipient": {"href": "http://.../people/1"}}}
assertThat(greeting.getMessage(), is("hello"));
recipient = greeting.getRecipient();
// GET /people/1
// -> {"name": "Bob", "_links": {"self": {"href": {"http://.../people/1"}}}
assertThat(recipient.getName(), is("Bob"));
Clients are created through ClientFactory.create(clazz)
. ClientFactory instances are created through Configuration.builder().getClientFactory()
with the configuration builder allowing various further configuration.
Clients support:
get(URI id)
- GET the item with the given IDgetAll()
- GET all items from the collection resourcegetAll(URI location)
- GET all items from the given endpointpost(T object)
- POST the item to the collection resourceput(T object)
- PUT the item to its resourcepatch(URI id, P patch)
- PATCH the item with the given ID with a set of changesdelete(URI id)
- DELETE the item with the given ID
PUT is supported with caveats: there is currently a whole category of Spring Data REST limitations interacting via PUT/PATCH with JPA repositories due to attempts to replace persistent collections and state merge occurring outside of a transaction.
Annotate your model classes with @RemoteResource(path)
. path
is the path of the class's collection resource, relative to the base URI set when building the ClientFactory
.
@RemoteResource("/things")
public class Thing { ... }
Use @ResourceId
to mark a java.net.URI
accessor as the resource ID. This is the canonical URI for the resource - its 'self' link.
private URI id;
@ResourceId public URI getId() { return id; }
Simple properties (Strings, primitives) will be mapped to JSON automatically.
Mark a resource as linked with @LinkedResource
on its accessor. Invoking this accessor will automatically query its associated linked remote resource to populate the model.
private Related related;
private Set<Related> relatedSet = new HashSet<>();
@LinkedResource public Related getRelated() { return related; }
@LinkedResource public Set<Related> getRelatedSet() { return relatedSet; }
Mark a resource as inline with the InlineAssociationDeserializer
Jackson deserializer. Invoking this accessor will create and return a proxy that is aware of the inline object's links, and so is able to resolve nested linked resources.
private Related related;
private Set<Related> relatedSet = new HashSet<>();
@JsonDeserialize(using = InlineAssociationDeserializer.class)
public Related getRelated() { return related; }
@JsonDeserialize(contentUsing = InlineAssociationDeserializer.class)
public Set<Related> getRelatedSet() { return relatedSet; }
Subresources are loaded from the _embedded
property of a HAL response when querying a collection resource. For single-valued resources, embedded resources are currently disregarded: PRs welcome!
Use @ResourceTypeInfo
to declare a type's subtypes. On deserialization, the type of the resource will be determined using the self
link of the resource.
@ResourceTypeInfo(subtypes = {OneThing.class, AnotherThing.class})
class Thing { }
class OneThing extends Thing { }
class AnotherThing extends Thing { }
Alternatively you can register your own TypeResolver
to provide custom subtype resolution, perhaps using alternative resource links.
@ResourceTypeInfo(typeResolver = MyTypeResolver.class)
class Thing { }
class MyTypeResolver implements TypeResolver {
Class<?> resolveType(Class<?> declaredType, Links resourceLinks, Configuration configuration) {
// own type resolution code here...
}
}