paulcwarren/spring-content

Nested content properties together with FS store do not convert to `@ContentId` type correctly.

vierbergenlars opened this issue · 1 comments

Describe the bug

We have a JPA entity with an @Embeddable object that contains content properties.

Entities and stores
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Contract {
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private UUID id;

	private String number;

	@Embedded
	@AttributeOverride(name="id", column = @Column(name = "content__id"))
	@AttributeOverride(name="length", column = @Column(name = "content__length"))
	@AttributeOverride(name="mimetype", column = @Column(name = "content__mimetype"))
	@AttributeOverride(name="filename", column = @Column(name = "content__filename"))
	private EmbeddedContent content = new EmbeddedContent();

}

@Embeddable
@NoArgsConstructor
@Getter
@Setter
public class EmbeddedContent {
    @ContentId
    private String id;

    @ContentLength
    private long length;

    @MimeType
    private String mimetype;

    @OriginalFileName
    private String filename;

}

@StoreRestResource
interface ContractContentStore extends ContentStore<Contract, String> {
}

The DefaultFilesystemStoreImpl#getResource(S, PropertyPath) generates a new UUID in case none exists yet. I then calls #convertToExternalContentIdType() to conver to the type of the @ContentId-annotated field.

However, BeanUtils.getFieldWithAnnotationType(property,ContentId.class) returns null, because there is no @ContentId-annotated field on the entity itself (only on the nested object). Then TypeDescriptor.valueOf(null) returns Object.class, and the PlacementService converts the generated UUID to Object (so it does nothing).
Then setting the content ID on the embedded object fails, because the UUID can not be assigned to a String (type type of content.id).

The PropertyAccessor machinery then tries to convert UUID to String with its internal machinery and finally throws an exception:

Failed to convert property value of type 'java.util.UUID' to required type 'java.lang.String' for property 'content.id'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.util.UUID' to required type 'java.lang.String' for property 'id': no matching editors or conversion strategy found
org.springframework.beans.ConversionNotSupportedException: Failed to convert property value of type 'java.util.UUID' to required type 'java.lang.String' for property 'content.id'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.util.UUID' to required type 'java.lang.String' for property 'id': no matching editors or conversion strategy found
	at org.springframework.beans.AbstractNestablePropertyAccessor.convertIfNecessary(AbstractNestablePropertyAccessor.java:595)
	at org.springframework.beans.AbstractNestablePropertyAccessor.convertForProperty(AbstractNestablePropertyAccessor.java:609)
	at org.springframework.beans.AbstractNestablePropertyAccessor.processLocalProperty(AbstractNestablePropertyAccessor.java:458)
	at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:278)
	at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:246)
	at internal.org.springframework.content.fs.repository.DefaultFilesystemStoreImpl.setContentId(DefaultFilesystemStoreImpl.java:422)
	at internal.org.springframework.content.fs.repository.DefaultFilesystemStoreImpl.getResource(DefaultFilesystemStoreImpl.java:95)
	at internal.org.springframework.content.commons.repository.factory.StoreImpl.getResource(StoreImpl.java:309)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at internal.org.springframework.content.commons.repository.factory.StoreMethodInterceptor.invoke(StoreMethodInterceptor.java:68)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
	at eu.xenit.contentcloud.userapps.xenit.insurance.store.$Proxy155.getResource(Unknown Source)
	at internal.org.springframework.content.rest.controllers.resolvers.AssociativeStoreResourceResolver.resolve(AssociativeStoreResourceResolver.java:21)
	at internal.org.springframework.content.rest.controllers.ResourceHandlerMethodArgumentResolver.resolveArgument(ResourceHandlerMethodArgumentResolver.java:129)
	at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:122)
	at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:179)
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:146)
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067)
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
	at org.springframework.web.servlet.FrameworkServlet.doPut(FrameworkServlet.java:920)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:668)
	at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:750)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at eu.xenit.contentcloud.thunx.spring.data.rest.AbacRequestFilter.doFilter(AbacRequestFilter.java:41)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:889)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.util.UUID' to required type 'java.lang.String' for property 'id': no matching editors or conversion strategy found
	at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:262)
Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.util.UUID' to required type 'java.lang.String' for property 'id': no matching editors or conversion strategy found

	at org.springframework.beans.AbstractNestablePropertyAccessor.convertIfNecessary(AbstractNestablePropertyAccessor.java:590)
	... 71 more

To Reproduce
Steps to reproduce the behavior:

  1. Use the model from above (any JPA @Entity with an @Embeddable object will dol)
  2. Create a new entity curl -XPOST http://localhost:8080/contracts -H "Content-Type: application/json" -d '{"number":"123"}', use the returned URL.
  3. Perform a PUT to the content property: curl -XPUT http://localhost:8080/contracts/45af1f9b-759d-4fba-9c6d-ab9e6131f774/content -H "Content-Type: text/plain" -d"blablabla"
  4. A HTTP 500 error is returned. No stacktace is returned, but if you put a breakpoint in StoreImpl, you can see the caught exception there.

Expected behavior

I would expect the content just to be set without a problem.

When I change EmbeddedContent to have a UUID id field (and change the type parameter in ContractContentStore as well), setting the content works without any problems.

Additional context

I have seen that the DefaultS3StoreImpl retrieves the type information by using MappingContext, which at first look does implement lookup for nested properties correctly.

Thanks @vierbergenlars, confirmed. I think you are right re DefaultS3StoreImpl too. Will fix soonest.