spring-projects / spring-data-rest

Simplifies building hypermedia-driven REST web services on top of Spring Data repositories
https://spring.io/projects/spring-data-rest
Apache License 2.0
916 stars 563 forks source link

Add mapping for converted objects [DATAREST-533] #908

Open spring-projects-issues opened 9 years ago

spring-projects-issues commented 9 years ago

Vasily Kartashov opened DATAREST-533 and commented

I have an entity with an embedded object that is stored as a serialised JSON in the database:

@Entity
public class Unit {
    ...
    @Column
    @Convert(converter = ConnectionStatusConverter.class)
    private ConnectionStatus connectionStatus;
    ...
}

@Converter
public class ConnectionStatusConverter implements AttributeConverter<ConnectionStatus, String> {
    public String convertToDatabaseColumn(ConnectionStatus attribute) { ... }
    public ConnectionStatus convertToEntityAttribute(String dbData) { ... }
}

Everything is fine with serialisation and deserialisation except when I try to PATCH an object with the following payload

{
    "connectionStatus": {
        ...
    }
}

I get NullPointerException. It is caused by the DomainObjectReader.doMerge expecting embedded object to be an entity, and this one (of type ConnectionStatus) is obviously not.

The solution maybe fairly simple, unless I badly misjudge the code: As long as the entity's property is marked with @Column don't try to find an embedded entity, but stick with current one (in my case Unit) while merging the fields.


Affects: 2.3 GA (Fowler)

spring-projects-issues commented 9 years ago

Oliver Drotbohm commented

What's the exact exception you see? What does ConnectionStatus look like? I am not sure I get the connection to the type conversion on the persistence provider level. For Spring Data REST ConnectionStatus is a complex object, not sure what makes you think it's "obviously not". Nothing very obvious here :).

Unfortunately the solution is not that fairly simple, as SD REST does not make any assumptions about the underlying store so that there's no way we can refer to @Column from SD REST code

spring-projects-issues commented 9 years ago

Vasily Kartashov commented

Hi Oliver,

thanks for the reply. ConnectionStatus is a fairly simple value object

public class ConnectionStatus {

    private String status;
    ...

    @JsonCreator
    public ConnectionStatus(@JsonProperty("status") String status, ...) {
        this.status = status;
        ...
    }

    @JsonProperty
    public String getStatus() { return status; }
    ...
}

The exception is

org.springframework.http.converter.HttpMessageNotReadableException: Could not read an object of type class net.observant.beobachter.domain.entities.fieldunit.FieldUnit from the request!; nested exception is org.springframework.http.converter.HttpMessageNotReadableException: Could not read payload!; nested exception is java.lang.NullPointerException
    at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.readPatch(PersistentEntityResourceHandlerMethodArgumentResolver.java:183)
    at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.read(PersistentEntityResourceHandlerMethodArgumentResolver.java:160)
    at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.resolveArgument(PersistentEntityResourceHandlerMethodArgumentResolver.java:125)
    at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:77)
    at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:162)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:129)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandleMethod(RequestMappingHandlerAdapter.java:776)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:705)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:966)
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:839)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:725)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    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:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:85)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:142)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:516)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1086)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:659)
    at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:223)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1558)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1515)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
Caused by: org.springframework.http.converter.HttpMessageNotReadableException: Could not read payload!; nested exception is java.lang.NullPointerException
    at org.springframework.data.rest.webmvc.json.DomainObjectReader.read(DomainObjectReader.java:90)
    at org.springframework.data.rest.webmvc.config.JsonPatchHandler.applyMergePatch(JsonPatchHandler.java:129)
    at org.springframework.data.rest.webmvc.config.JsonPatchHandler.apply(JsonPatchHandler.java:99)
    at org.springframework.data.rest.webmvc.config.PersistentEntityResourceHandlerMethodArgumentResolver.readPatch(PersistentEntityResourceHandlerMethodArgumentResolver.java:181)
    ... 43 common frames omitted
Caused by: java.lang.NullPointerException: null
    at org.springframework.data.rest.webmvc.json.DomainObjectReader.getJacksonProperties(DomainObjectReader.java:204)
    at org.springframework.data.rest.webmvc.json.DomainObjectReader.doMerge(DomainObjectReader.java:157)
    at org.springframework.data.rest.webmvc.json.DomainObjectReader.doMerge(DomainObjectReader.java:185)
    at org.springframework.data.rest.webmvc.json.DomainObjectReader.read(DomainObjectReader.java:88)
    ... 46 common frames omitted

I see now that the PersistenceEntity is not really a JPA entity, that might be the core misunderstanding on my part. Would some annotation like @ComplexProperty or something along those lines be an option?

spring-projects-issues commented 9 years ago

Oliver Drotbohm commented

What is FieldUnit? ConnectionStatus is not even mentioned in the stack trace. It seems that our metamodel detects ConnectionStatus as managed entity but fails to lookup a PersistentEntity for it later on. Any chance you create a tiny reproducing sample project to investigate?

Let's start thinking about a solution once we clearly understood what the problem is ;)

spring-projects-issues commented 9 years ago

Vasily Kartashov commented

I've created the sample project here: https://github.com/vasily-kartashov/spring-data-rest-converters

The README contains the steps necessary to reproduce the exception. Interesting enough, that the first PATCH gets through as the connectionStatus is null

spring-projects-issues commented 9 years ago

Oliver Drotbohm commented

It seems like there's a POM missing. Any chance you create a test case that reproduces the error? Ideally mvn clean test shows a failing test

spring-projects-issues commented 9 years ago

Vasily Kartashov commented

Hi Oliver,

Oops, missed the POM. I've updated the code and added a unit test that shows that the second PATCH is failing.

https://github.com/vasily-kartashov/spring-data-rest-converters