Search code examples
spring-bootvaadinspring-webclientvaadin-flow

How to link a Vaadin Grid with the result of Spring Mono WebClient data


This seems to be a missing part in the documentation of Vaadin...

I call an API to get data in my UI like this:

@Override
public URI getUri(String url, PageRequest page) {
    return UriComponentsBuilder.fromUriString(url)
            .queryParam("page", page.getPageNumber())
            .queryParam("size", page.getPageSize())
            .queryParam("sort", (page.getSort().isSorted() ? page.getSort() : ""))
            .build()
            .toUri();
}

@Override
public Mono<Page<SomeDto>> getDataByPage(PageRequest pageRequest) {
    return webClient.get()
            .uri(getUri(URL_API + "/page", pageRequest))
            .retrieve()
            .bodyToMono(new ParameterizedTypeReference<>() {
            });
}

In the Vaadin documentation (https://vaadin.com/docs/v10/flow/binding-data/tutorial-flow-data-provider), I found an example with DataProvider.fromCallbacks but this expects streams and that doesn't feel like the correct approach as I need to block on the requests to get the streams...

DataProvider<SomeDto, Void> lazyProvider = DataProvider.fromCallbacks(
     q -> service.getData(PageRequest.of(q.getOffset(), q.getLimit())).block().stream(),
     q -> service.getDataCount().block().intValue()
);

When trying this implementation, I get the following error:

org.springframework.core.codec.CodecException: Type definition error: [simple type, class org.springframework.data.domain.Page]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.data.domain.Page` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 1]
grid.setItems(lazyProvider);

Solution

  • I don't have experience with vaadin, so i'll talk about the deserialization problem.

    Jackson needs a Creator when deserializing. That's either:

    1. the default no-arg constructor
    2. another constructor annotated with @JsonCreator
    3. static factory method annotated with @JsonCreator

    If we take a look at spring's implementations of Page - PageImpl and GeoPage, they have neither of those. So you have two options:

    1. Write your custom deserializer and register it with the ObjectMapper instance

    The deserializer:

    public class PageDeserializer<T> extends StdDeserializer<Page<T>> {
    
        public PageDeserializer() {
            super(Page.class);
        }
    
        @Override
        public Page<T> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
            //TODO implement for your case
            return null;
        }
    }
    

    And registration:

    SimpleModule module = new SimpleModule();
    module.addDeserializer(Page.class, new PageDeserializer<>());
    objectMapper.registerModule(module);
    
    1. Make your own classes extending PageImpl, PageRequest, etc. and annotate their constructors with @JsonCreator and arguments with @JsonProperty.

    Your page:

    public class MyPage<T> extends PageImpl<T> {
    
        @JsonCreator
        public MyPage(@JsonProperty("content_prop_from_json") List<T> content, @JsonProperty("pageable_obj_from_json") MyPageable pageable, @JsonProperty("total_from_json") long total) {
            super(content, pageable, total);
        }
    }
    

    Your pageable:

    public class MyPageable extends PageRequest {
    
        @JsonCreator
        public MyPageable(@JsonProperty("page_from_json") int page, @JsonProperty("size_from_json") int size, @JsonProperty("sort_object_from_json") Sort sort) {
            super(page, size, sort);
        }
    }
    

    Depending on your needs for Sort object, you might need to create MySort as well, or you can remove it from constructor and supply unsorted sort, for example, to the super constructor. If you are deserializing from input manually you need to provide type parameters like this:

    JavaType javaType = TypeFactory.defaultInstance().constructParametricType(MyPage.class, MyModel.class);
    Page<MyModel> deserialized = objectMapper.readValue(pageString, javaType);
    

    If the input is from request body, for example, just declaring the generic type in the variable is enough for object mapper to pick it up.

    @PostMapping("/deserialize")
    public ResponseEntity<String> deserialize(@RequestBody MyPage<MyModel> page) {
        return ResponseEntity.ok("OK");
    }
    

    Personally i would go for the second option, even though you have to create more classes, it spares the tediousness of extracting properties and creating instances manually when writing deserializers.