Search code examples
spring-bootspring-data-rest

How to add "book" to /shelves/1/books


I can't figure out how to do something that should be incredibly simple.

I have two entities: shelf and book. A shelf can have one or more books. Each of these entities has a corresponding JpaRepository exposed as a rest repository using Spring Data Rest. When I run the application, all of the query endpoints work perfectly, but I can't figure out how I can add a book to a shelf.

First I add a shelf by POSTing { "name":"westerns"} to /shelves (works fine).

Method 1: I try to add a book by POSTing { "name":"mybook"} to /shelves/1/books, but I get a 405 "method not allowed" error. Can I only post a book to the /books endpoint (without writing my own controller, I mean)? And if so, would I have to embed the shelf entity that the book belongs to?

Method 2: If I try to add a book by doing a PUT to /shelves/1 using the content { "name":"westerns", "books":[ {"name":"mybook"} ] }, I get the following error message back:

Message: "Could not read JSON: Template must not be null or empty! (through reference chain: org.demo.Shelf["books"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Template must not be null or empty! (through reference chain: org.demo.Shelf["books"])

The statcktrace:

2014-11-06 06:55:18.864 ERROR 9888 --- [nio-8080-exec-5] s.d.r.w.AbstractRepositoryRestController : Could not read JSON: Template must not be null or empty! (through reference chai n: org.demo.Shelf["books"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Template must not be null or empty! (through reference chain: org.demo.Shelf[" books"])

org.springframework.http.converter.HttpMessageNotReadableException: Could not read JSON: Template must not be null or empty! (through reference chain: org.demo.Shelf["books"]); nes ted exception is com.fasterxml.jackson.databind.JsonMappingException: Template must not be null or empty! (through reference chain: org.demo.Shelf["books"]) at org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.readJavaType(MappingJackson2HttpMessageConverter.java:228) at org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.readInternal(MappingJackson2HttpMessageConverter.java:212) at org.springframework.http.converter.AbstractHttpMessageConverter.read(AbstractHttpMessageConverter.java:159) at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.resolveArgument(PersistentEntityResourceHandlerMethodArgumentResolver.java:100 ) at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:79) at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:157) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:124) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:749) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:689) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:83) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:938) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:870) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:961) at org.springframework.web.servlet.FrameworkServlet.doPut(FrameworkServlet.java:874) at javax.servlet.http.HttpServlet.service(HttpServlet.java:649) at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:837) at javax.servlet.http.HttpServlet.service(HttpServlet.java:727) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1070) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:611) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1736) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1695) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:745) Caused by: com.fasterxml.jackson.databind.JsonMappingException: Template must not be null or empty! (through reference chain: org.demo.Shelf["books"]) at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:232) at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:197) at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1420) at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:244) at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:118) at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:2993) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2158) at org.springframework.http.converter.json.MappingJackson2HttpMessageConverter.readJavaType(MappingJackson2HttpMessageConverter.java:225) ... 38 common frames omitted Caused by: java.lang.IllegalArgumentException: Template must not be null or empty! at org.springframework.util.Assert.hasText(Assert.java:162) at org.springframework.hateoas.UriTemplate.(UriTemplate.java:56) at org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$UriStringDeserializer.deserialize(PersistentEntityJackson2Module.java:380) at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:227) at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:204) at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:23) at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:525) at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:99) at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:242) ... 42 common frames omitted

Here is all the relevant code (each class is in its own file, of course).

@Entity
public class Shelf {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private String name;

    @OneToMany
    private List<Book> books;

    public List<Book> getBooks() {
    return books;
    }

    public void setBooks(List<Book> books) {
    this.books = books;
    }

    public String getName() {
    return name;
    }

    public void setName(String name) {
    this.name = name;
    }

}

@Entity
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }


}

@RepositoryRestResource
public interface BookRepository extends JpaRepository<Book, Long>{

}

@RepositoryRestResource
public interface ShelfRepository extends JpaRepository<Shelf, Long>{

}

I'm using Spring Boot 1.1.8.RELEASE


Solution

  • The Book entities have their own REST endpoint and to create one you have to POST to that endpoint.

    To put a book on a shelf that you have to send the URI of that book to the association URI (/shelves/{id}/books). Surprisingly the documentation has it wrong and adding items is not done via POST but PATCH:

    PATCH http://localhost:8080/shelves/1/books
    Content-Type: text/uri-list 
    http://localhost:8080//books/1
    

    You can add more than one book at once, one URI per line. If you don't want to add books, but rather replace all books on the shelf with new ones, i.e. replace the whole collection, you can use PUT instead.