Spring MVC 3: return a Spring-Data Page as JSON - issue with PagedResourcesAssembler

json, spring-data, spring-hateoas, spring-mvc

Solution

You've probably solved this by now, but since I have this working I thought I'd add the solution to at least one of your problems for anyone else in a similar boat.

`Type mismatch: cannot convert from ResponseEntity<PagedResources> to HttpEntity<PagedResources<PersonDTO>>`:

To solve this, add an additional type parameter to your return type:

@RequestMapping(value = REQUEST_MAPPING_LIST, method = RequestMethod.GET)
public HttpEntity<PagedResources<PersonDTO>> persons(final Model model, @ModelAttribute final PersonCriteria searchCriteria,
        final Pageable pageable, final PagedResourcesAssembler assembler) {
    ...
}

`com.sun.istack.SAXException2: unable to marshal type "org.springframework.hateoas.Resource" as an element because it is not known to this context.]`

It looks like Spring is trying to produce XML, which I think it does by default if it finds a JAXB implementation on the classpath. If you don't need to produce XML from this method, you could add `produces = {MediaType.APPLICATION_JSON_VALUE}` to its @RequestMapping.

Problem

So, I'm fairly new to spring and to java in general What I try to do is to have on the same rendered view the form to post data to filter the result list displayed under the form. I have a simple domain class as follows: ``` @Entity @Table(name = "SEC_PERSON") public class Person { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_SEC_PERSON") @SequenceGenerator(name = "SEQ_SEC_PERSON", sequenceName = "SEQ_SEC_PERSON") @Column(name = "ID") private Long Id; @Column(name = "CODE", nullable = false) private String code; @Column(name = "FIRSTNAME", nullable = false) private String firstname; @Column(name = "SURNAME", nullable = false) private String surname; @Column(name = "CREATIONDATE") private DateTime creationDate; //getters and setters ``` a DTO because I want my domain to be decoupled from my presentation ``` public class PersonDTO { private Long id; @NotEmpty private String code; @NotEmpty private String firstname; @NotEmpty private String surname; private DateTime creationDate; public PersonDTO() { } //getters and setters ``` a repository extending Jpa and QueryDsl ``` public interface PersonRepository extends JpaRepository<Person, Long>, QueryDslPredicateExecutor<Person> {} ``` a data access class for my search that is null safe (thanks guava) and its equivalent not null safe The person Criteria: ``` public class PersonCriteria { private String code; private String surname; private String firstname; private LocalDate creationDateFrom; private LocalDate creationDateTo; //getters and setters } ``` The null safe version ``` public class NullSafePersonCriteria { private final PersonCriteria personCriteria; public NullSafePersonCriteria(final PersonCriteria personCriteria) { checkArgument(personCriteria != null); this.personCriteria = personCriteria; } public Optional<String> getCode() { return Optional.fromNullable(this.personCriteria.getCode()); } public Optional<String> getSurname() { return Optional.fromNullable(this.personCriteria.getSurname()); } public Optional<String> getFirstname() { return Optional.fromNullable(this.personCriteria.getFirstname()); } public Optional<LocalDate> getCreationDateFrom() { return Optional.fromNullable(this.personCriteria.getCreationDateFrom()); } public Optional<LocalDate> getCreationDateTo() { return Optional.fromNullable(this.personCriteria.getCreationDateTo()); } ``` My predicate to search ``` public class PersonPredicates { public static Predicate PersonLitstQuery(final PersonCriteria personCriteria) { final QPerson person = QPerson.person; final NullSafePersonCriteria nsPersonCriteria = new NullSafePersonCriteria(personCriteria); BooleanExpression criteria = QPerson.person.isNotNull(); if (nsPersonCriteria.getCode().isPresent()) { criteria = criteria.and(person.code.matches(nsPersonCriteria.getCode().get())); } if (nsPersonCriteria.getSurname().isPresent()) { criteria.and(person.surname.startsWithIgnoreCase(nsPersonCriteria.getSurname().get())); } if (nsPersonCriteria.getFirstname().isPresent()) { criteria.and(person.firstname.startsWithIgnoreCase(nsPersonCriteria.getFirstname().get())); } if ((nsPersonCriteria.getCreationDateFrom().isPresent()) && (nsPersonCriteria.getCreationDateTo().isPresent())) { criteria.and(person.creationDate.between(nsPersonCriteria.getCreationDateFrom().get().toDateTimeAtStartOfDay(), nsPersonCriteria.getCreationDateTo().get().toDateTimeAtStartOfDay())); } return criteria; } ``` My Service implementation is as follows: ``` @Service public class PersonServiceImpl implements PersonService{ @Transactional(readOnly = true) @Override public Page<PersonDTO> search(final PersonCriteria criteria, final int pageIndex) { LOGGER.debug("Searching person with set of criterias"); return new PersonPage(this.mapper.map(Lists.newArrayList(this.personRepository.findAll(PersonLitstQuery(criteria))), PersonDTO.class), constructPageSpecification(pageIndex), count(criteria)); } ``` The mapper that I use is just extending a bit DozerMapper: ``` public class DozerMapper{ private final org.dozer.Mapper mapper; public DozerMapper(final org.dozer.Mapper mapper) { this.mapper = mapper; } @Override public <T> T map(final Object source, final Class<T> destinationClass) { return this.mapper.map(source, destinationClass); } @Override public <T> List<T> map(final List<?> sources, final Class<T> destinationClass) { final List<T> result = Lists.newArrayList(); for (final Object source : sources) { result.add(map(source, destinationClass)); } return result; } ``` Now, all of the above works, fine is unit tested and returns the results I want. My problem is with the controller and the views.... I have carefully read Oliver's answer to this question: Spring MVC 3: return a Spring-Data Page as JSON though for some reason I cannot make it work. I've added the following dependencies to my project to use HATEOAS and Spring-data-commons: ``` <dependency> <groupId>org.springframework.hateoas</groupId> <artifactId>spring-hateoas</artifactId> <version>0.7.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-commons</artifactId> <version>1.6.0.RELEASE</version> </dependency> ``` and my controller looks like this: ``` @Controller @SessionAttributes("person") public class PersonController @RequestMapping(value = REQUEST_MAPPING_LIST, method = RequestMethod.GET) public HttpEntity<PagedResources> persons(final Model model, @ModelAttribute final PersonCriteria searchCriteria, final Pageable pageable, final PagedResourcesAssembler assembler) { model.addAttribute(MODEL_ATTRIBUTE_SEARCHCRITERIA, searchCriteria); final Page<PersonDTO> persons = this.personService.search(searchCriteria, searchCriteria.getPageIndex()); return new ResponseEntity<>(assembler.toResource(persons), HttpStatus.OK); } ``` and my jsp: ``` <html> <head> <title>testing</title> <script src="jslinks for jqGrid and jquery" type="text/javascript"></script> </head> <body> <form:form action="person" commandName="searchCriteria" method="POST"> <div> <form:label path="code">Code: </form:label> <form:input path="code" type="text"/> <form:label path="surname">Surname: </form:label> <form:input path="surname" type="text"/> <form:label path="firstname">Firstname: </form:label> <form:input path="firstname" type="text"/> <form:label path="creationDateFrom">Creation Date From: </form:label> <smj:datepicker id="creationDateFrom" name="CreationDateFrom" /> <form:label path="creationDateTo">Creation Date To: </form:label> <smj:datepicker id="creationDateTo" name="CreationDateTo" /> </div> <div> <input type="submit" value="search"/> </div> </form:form> <smjg:grid gridModel="gridModel" id="persons" datatype="\"json\"" url="\'person\'" jsonReader="{root:\"content\", repeatitems: false, records: \"numberOfElements\", total: \"totalPages\"}"> <smjg:gridColumn name="code" /> <smjg:gridColumn name="surname" align="left"/> <smjg:gridColumn name="firstname" align="left"/> </smjg:grid> </body> </html> ``` Explanation: the smj and smjg tags are taglibs that I'm currently working on that are linking jquery to spring mvc. Ex: smjg:grid will create the tag and the javascript that will call the jqgrid function. The first difference from Olivier's answer from this post Spring MVC 3: return a Spring-Data Page as JSON is that If I infer the PersonDTO within my HttpEntity then I get the following compilation error: ``` Type mismatch: cannot convert from ResponseEntity<PagedResources> to HttpEntity<PagedResources<PersonDTO>> ``` the second difference is that it seems I should infer my PersonDTO into the PagedResourcesAssembler, is that correct? The outcome when I call the url directly localhost:8081/app/person I get a http 500 error: ``` org.springframework.http.converter.HttpMessageNotWritableException: Could not marshal [PagedResource { content: [Resource { content: com.app.admin.service.PersonDTO@60a349d0[id=2050,code=TEST2,firstname=ChadsdaTest,surname=Francois,creationDate=<null>], links: [] }, Resource { content: com.app.admin.service.PersonDTO@48462da5[id=5050,code=TESTNEW,firstname=Francois,surname=asdasdx,creationDate=<null>], links: [] }, Resource { content: com.app.admin.crediadmin.service.PersonDTO@5458c9fc[id=51,code=TEST,firstname=Francois,surname=asdawdsx,creationDate=<null>], links: [] }, Resource { content: com.app.admin.service.PersonDTO@de47c70[id=2051,code=TEST3,firstname=Chaqweqasdamsh,surname=Frasda,creationDate=<null>], links: [] }, Resource { content: com.app.admin.service.PersonDTO@7bd2085d[id=3053,code=TEST7,firstname=Francois,surname=Cadsdsx,creationDate=<null>], links: [] }, Resource { content: com.app.admin.service.PersonDTO@14676697[id=3050,code=TESTER,firstname=Francois,surname=CasdadsixChaix,creationDate=<null>], links: [] }, Resource { content: com.app.admin.service.PersonDTO@109de504[id=3051,code=TESTER3,firstname=FrancoisF,surname=Chtest,creationDate=<null>], links: [] }], metadata: Metadata { number: 0, total pages: 2, total elements: 7, size: 5 }, links: [<http://localhost:8081/app/person?page=1&size=5&sort=surname,asc>;rel="next"] }]: null; nested exception is javax.xml.bind.MarshalException - with linked exception: [com.sun.istack.SAXException2: unable to marshal type "org.springframework.hateoas.Resource" as an element because it is not known to this context.] org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter.writeToResult(Jaxb2RootElementHttpMessageConverter.java:99) org.springframework.http.converter.xml.AbstractXmlHttpMessageConverter.writeInternal(AbstractXmlHttpMessageConverter.java:66) org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:179) org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:148) org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:124) org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:69) org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:122) ``` and the root cause: ``` javax.xml.bind.MarshalException - with linked exception: [com.sun.istack.SAXException2: unable to marshal type "org.springframework.hateoas.Resource" as an element because it is not known to this context.] com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:318) com.sun.xml.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:244) ``` I'm not sure of what I do wrong here. Althoug if I call the same url with .json then I get the json output which seems weird as I don't produce the json still.

Original source