Exposing a service with a linked resource
Consider two simple JPA based entities, Course and Teacher:
@Entity @Table(name = "teachers") public class Teacher { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Long id; @Size(min = 2, max = 50) @Column(name = "name") private String name; @Column(name = "department") @Size(min = 2, max = 50) private String department; ... } @Entity @Table(name = "courses") public class Course { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id") private Long id; @Size(min = 1, max = 10) @Column(name = "coursecode") private String courseCode; @Size(min = 1, max = 50) @Column(name = "coursename") private String courseName; @ManyToOne @JoinColumn(name = "teacher_id") private Teacher teacher; .... }
essentially the relation looks like this:
Now, all it takes to expose these entities as REST resources is adding a @RepositoryRestResource annotation on their JPA based Spring Data repositories this way, first for the "Teacher" resource:
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import univ.domain.Teacher; @RepositoryRestResource public interface TeacherRepo extends JpaRepository<Teacher, Long> { }
and for exposing the Course resource:
@RepositoryRestResource public interface CourseRepo extends JpaRepository<Course, Long> { }
With this done and assuming a few teachers and a few courses are already in the datastore, a GET on courses would yield a response of the following type:
{ "_links" : { "self" : { "href" : "http://localhost:8080/api/courses{?page,size,sort}", "templated" : true } }, "_embedded" : { "courses" : [ { "courseCode" : "Course1", "courseName" : "Course Name 1", "version" : 0, "_links" : { "self" : { "href" : "http://localhost:8080/api/courses/1" }, "teacher" : { "href" : "http://localhost:8080/api/courses/1/teacher" } } }, { "courseCode" : "Course2", "courseName" : "Course Name 2", "version" : 0, "_links" : { "self" : { "href" : "http://localhost:8080/api/courses/2" }, "teacher" : { "href" : "http://localhost:8080/api/courses/2/teacher" } } } ] }, "page" : { "size" : 20, "totalElements" : 2, "totalPages" : 1, "number" : 0 } }
and a specific course looks like this:
{ "courseCode" : "Course1", "courseName" : "Course Name 1", "version" : 0, "_links" : { "self" : { "href" : "http://localhost:8080/api/courses/1" }, "teacher" : { "href" : "http://localhost:8080/api/courses/1/teacher" } } }
If you are wondering what the "_links", "_embedded" are - Spring Data REST uses Hypertext Application Language(or HAL for short) to represent the links, say the one between a course and a teacher.
HAL Based REST service - Using RestTemplate
Given this HAL based REST service, the question that I had in my mind was how to write a client to this service. I am sure there are better ways of doing this, but what follows worked for me and I welcome any cleaner ways of writing the client.
First, I modified the RestTemplate to register a custom Json converter that understands HAL based links:
public RestTemplate getRestTemplateWithHalMessageConverter() { RestTemplate restTemplate = new RestTemplate(); List<HttpMessageConverter<?>> existingConverters = restTemplate.getMessageConverters(); List<HttpMessageConverter<?>> newConverters = new ArrayList<>(); newConverters.add(getHalMessageConverter()); newConverters.addAll(existingConverters); restTemplate.setMessageConverters(newConverters); return restTemplate; } private HttpMessageConverter getHalMessageConverter() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerModule(new Jackson2HalModule()); MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class); halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON)); halConverter.setObjectMapper(objectMapper); return halConverter; }
The Jackson2HalModule is provided by the Spring HATEOS project and understands HAL representation.
Given this shiny new RestTemplate, first let us create a Teacher entity:
Teacher teacher1 = new Teacher(); teacher1.setName("Teacher 1"); teacher1.setDepartment("Department 1"); URI teacher1Uri = testRestTemplate.postForLocation("http://localhost:8080/api/teachers", teacher1);
Note that when the entity is created, the response is a http status code of 201 with the Location header pointing to the uri of the newly created resource, Spring RestTemplate provides a neat way of posting and getting hold of this Location header through an API. So now we have a teacher1Uri representing the newly created teacher.
Given this teacher URI, let us now retrieve the teacher, the raw json for the teacher resource looks like the following:
{ "name" : "Teacher 1", "department" : "Department 1", "version" : 0, "_links" : { "self" : { "href" : "http://localhost:8080/api/teachers/1" } } }
and to retrieve this using RestTemplate:
ResponseEntity<Resource<Teacher>> teacherResponseEntity = testRestTemplate.exchange("http://localhost:8080/api/teachers/1", HttpMethod.GET, null, new ParameterizedTypeReference<Resource<Teacher>>() { }); Resource<Teacher> teacherResource = teacherResponseEntity.getBody(); Link teacherLink = teacherResource.getLink("self"); String teacherUri = teacherLink.getHref(); Teacher teacher = teacherResource.getContent();
Jackson2HalModule is the one which helps unpack the links this cleanly and to get hold of the Teacher entity itself. I have previously explained ParameterizedTypeReference here.
Now, to a more tricky part, creating a Course.
Creating a course is tricky as it has a relation to the Teacher and representing this relation using HAL is not that straightforward. A raw POST to create the course would look like this:
{ "courseCode" : "Course1", "courseName" : "Course Name 1", "version" : 0, "teacher" : "http://localhost:8080/api/teachers/1" }
Note how the reference to the teacher is a URI, this is how HAL represents an embedded reference specifically for a POST'ed content, so now to get this form through RestTemplate -
First to create a Course:
Course course1 = new Course(); course1.setCourseCode("Course1"); course1.setCourseName("Course Name 1");
At this point, it will be easier to handle providing the teacher link by dealing with a json tree representation and adding in the teacher link as the teacher uri:
ObjectMapper objectMapper = getObjectMapperWithHalModule(); ObjectNode jsonNodeCourse1 = (ObjectNode) objectMapper.valueToTree(course1); jsonNodeCourse1.put("teacher", teacher1Uri.getPath());
and posting this should create the course with the linked teacher:
URI course1Uri = testRestTemplate.postForLocation(coursesUri, jsonNodeCourse1);
and to retrieve this newly created Course:
ResponseEntity<Resource<Course>> courseResponseEntity = testRestTemplate.exchange(course1Uri, HttpMethod.GET, null, new ParameterizedTypeReference<Resource<Course>>() { }); Resource<Course> courseResource = courseResponseEntity.getBody(); Link teacherLinkThroughCourse = courseResource.getLink("teacher");
This concludes how to use the RestTemplate to create and retrieve a linked resource, alternate ideas are welcome.
If you are interested in exploring this further, the entire sample is available at this github repo - and the test is here
References:
Hypertext Application Language(or HAL for short)HAL Specification
Spring RestTemplate
Thank you for this post.
ReplyDelete-SGB
Hi, I downloaded the example, then from Eclipse run as Maven Test. I am getting connection refused. What to do? Thanks, Xena.
ReplyDeletedone changing port as:
Delete@Value("${local.server.port}")
private int port;
Regards,
Tsena
when i follow this for my project I get a 409 conflict error.
ReplyDeleteorg.springframework.web.client.HttpClientErrorException: 409 Conflict....
Where is HAL_JSON coming from?
ReplyDeleteTry MediaTypes.HAL_JSON in org.springframework.hateoas
Delete