Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ProjectionSerializer doesn't react to unwrapping mode correctly [DATAREST-697] #1069

Closed
spring-projects-issues opened this issue Oct 26, 2015 · 5 comments
Assignees
Labels

Comments

@spring-projects-issues
Copy link

@spring-projects-issues spring-projects-issues commented Oct 26, 2015

Adam P opened DATAREST-697 and commented

Previously, it was possible to have a controller method that returned a Resource-wrapped @Projection interface of a domain type. Somewhere between 2.3.2 and 2.4, this functionality was broken.

The below code sample works fine in SDR 2.3.2, but in 2.4 and current SNAPSHOTs it results in the following error:

Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: Can not start an object, expecting field name; nested exception is com.fasterxml.jackson.core.JsonGenerationException: Can not start an object, expecting field name

Testing this out, serializing a Resource-wrapped POJO works fine, a Resource-wrapped domain type works fine, a non-wrapped Projection works fine, but a Resource-wrapped projection fails with the above error. Using the SDR repository controller to return the projection works fine, too.

This feels like a bug?

Below is sample code for reproducing this. Tested with Spring Boot 1.2.7 and SDR 2.3.2 (working) and Spring Boot 1.3.1.RC1 and SDR 2.4.0.RELEASE (not working).

@SpringBootApplication
public class Sandbox {

    public static void main(String[] args) {
        SpringApplicationBuilder builder = new SpringApplicationBuilder(Sandbox.class);
        builder.run(args);

    }
}
public interface PersonRepository extends CrudRepository<Person,Long> {
}
@Projection(types=Person.class,name="test")
public interface PersonProjection {

    String getName();
}
@Entity
@Getter
@Setter
public class Person {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private String phone;

}
@BasePathAwareController
public class TestController {

    private final ProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();

    @Autowired
    private PersonRepository personRepository;

    @RequestMapping(value = "/test/{id}", produces = "application/hal+json")
    public ResponseEntity<Resource<PersonProjection>> getProjection(@PathVariable Long id){
        Person one = personRepository.findOne(id);
        PersonProjection projection = projectionFactory.createProjection(PersonProjection.class, one);
        return ResponseEntity.ok(new Resource<>(projection));
    }

    @RequestMapping(value = "/test", produces = "application/hal+json")
    public ResponseEntity<Resources<Resource<PersonProjection>>> getProjections(){
        Set<Person> people = Sets.newHashSet(personRepository.findAll());
        Resources<Resource<PersonProjection>> wrap = Resources.wrap(people.stream().map(o -> projectionFactory.createProjection(PersonProjection.class, o)).collect(Collectors.toSet()));
        return ResponseEntity.ok(wrap);
    }
@WebAppConfiguration
@SpringApplicationConfiguration(classes = Sandbox.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class TestProjection {

    private MockMvc mockMvc;

    @Autowired
    private WebApplicationContext webApplicationContext;
    
    @Autowired
    private PersonRepository personRepository;

    @Before
    public void before(){
        mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
        Person person = new Person();
        person.setName("Adam");
        person.setPhone("123456");
        personRepository.save(person);
        
    }

    @Test 
    public void testProjection() throws Exception {
        mockMvc.perform(get("/test")).andDo(print()).andExpect(status().is2xxSuccessful()).andExpect(jsonPath("$._embedded.persons[0].name").value("Adam")).andExpect(jsonPath("$._embedded.persons[0].phone").doesNotExist());
    }

    @Test
    public void testProjectionSingleResource() throws Exception {
        mockMvc.perform(get("/test/1")).andDo(print()).andExpect(status().is2xxSuccessful()).andExpect(jsonPath("$.name").value("Adam")).andExpect(jsonPath("$.phone").doesNotExist());
    }

    @Test
    public void testProjectionViaSDR() throws Exception {
        mockMvc.perform(get("/persons?projection=test")).andDo(print()).andExpect(status().is2xxSuccessful()).andExpect(jsonPath("$._embedded.persons[0].name").value("Adam")).andExpect(jsonPath("$._embedded.persons[0].phone").doesNotExist());
    }

    @Test
    public void testProjectionViaSDRSingleResource() throws Exception {
        mockMvc.perform(get("/persons/1?projection=test")).andDo(print()).andExpect(status().is2xxSuccessful()).andExpect(jsonPath("$.name").value("Adam")).andExpect(jsonPath("$.phone").doesNotExist());
    }


}

Affects: 2.4 GA (Gosling)

Attachments:

Issue Links:

  • DATAREST-716 Rest Endpoints produce HttpMessageNotWritableException sometimes after restarts
    ("is duplicated by")

Referenced from: commits 6709d1f, f7cff01

Backported to: 2.4.2 (Gosling SR2)

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Oct 27, 2015

Oliver Drotbohm commented

Thanks for that one Adam. Would you mind adding the complete exception stack trace and wrap up the sample code in some tiny sample executable project, that shows the error?

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Oct 27, 2015

Adam P commented

Apologies, meant to include the stack trace. Will attach sample project shortly.

{"timestamp":1445947947870,"status":500,"error":"Internal Server Error","exception":"org.springframework.http.converter.HttpMessageNotWritableException","message":"Could not write content: Can not start an object, expecting field name; nested exception is com.fasterxml.jackson.core.JsonGenerationException: Can not start an object, expecting field name","trace":"org.springframework.http.converter.HttpMessageNotWritableException: Could not write content: Can not start an object, expecting field name; nested exception is com.fasterxml.jackson.core.JsonGenerationException: Can not start an object, expecting field name
	at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:271)
	at org.springframework.http.converter.AbstractGenericHttpMessageConverter.write(AbstractGenericHttpMessageConverter.java:100)
	at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:202)
	at org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:186)
	at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:80)
	at org.springframework.data.rest.webmvc.ResourceProcessorHandlerMethodReturnValueHandler.handleReturnValue(ResourceProcessorHandlerMethodReturnValueHandler.java:174)
	at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:80)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:126)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:806)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:729)
	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:970)
	at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:622)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:846)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:729)
	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.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
	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.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:87)
	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.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:217)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
	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:518)
	at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1091)
	at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:673)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1500)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1456)
	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: com.fasterxml.jackson.core.JsonGenerationException: Can not start an object, expecting field name
	at com.fasterxml.jackson.core.JsonGenerator._reportError(JsonGenerator.java:1649)
	at com.fasterxml.jackson.core.json.UTF8JsonGenerator._verifyValueWrite(UTF8JsonGenerator.java:949)
	at com.fasterxml.jackson.core.json.UTF8JsonGenerator.writeStartObject(UTF8JsonGenerator.java:314)
	at org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$ProjectionSerializer.serialize(PersistentEntityJackson2Module.java:464)
	at org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$ProjectionSerializer.serialize(PersistentEntityJackson2Module.java:432)
	at com.fasterxml.jackson.databind.ser.impl.UnwrappingBeanPropertyWriter.serializeAsField(UnwrappingBeanPropertyWriter.java:124)
	at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:675)
	at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:157)
	at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:130)
	at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1387)
	at com.fasterxml.jackson.databind.ObjectWriter.writeValue(ObjectWriter.java:889)
	at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.writeInternal(AbstractJackson2HttpMessageConverter.java:264)
	... 53 more
","path":"/test/1"}

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Oct 27, 2015

Adam P commented

[^DATAREST-697.zip] Hi Oliver, please see attached sample project and stack trace. Cheers!

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Dec 2, 2015

Adam P commented

Had a little more time to play with this recently - looks like this was introduced by the changes in DATAREST-521.

The exception is thrown when rendering the content field of the Resource - which is annotated @JsonUnwrapped, but the serialization of the content field is handled by the ProjectionSerializer, which makes a jgen.writeStartObject(); call, which is not going to work.

When attempting to serialize a resource projection via a SDR controller (which works fine), it makes use of the PersistentEntityResourceSerializer and the ProjectionResourceContentSerializer (as you would expect). When attempting to serialize a resource-wrapped projection via any other standard controller, the above two are not used, and instead we just hit the ProjectionSerializer. Is the ProjectionSerializer intended only for use for serializing excerpt projections?

@spring-projects-issues
Copy link
Author

@spring-projects-issues spring-projects-issues commented Dec 10, 2015

Oliver Drotbohm commented

Thanks for the ticket, Adam. The sample project helped to identify the issue. We basically didn't tweak our behavior depending on whether the seriliazer was used in an unwrapping context or not. Even more so, we altered the state of the serializer instance which caused the instance to be tied to unwrapping particular mode once it got set to it once.

This is now fixed by considering the unwrapping mode on serialization as well as making sure we crate a new unwrapping serializer instance on request instead of modifying state on the existing one

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants