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

Time:02-09

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);

CodePudding user response:

There are two parts to this question.

The first one is about asynchronously loading data for a DataProvider in Vaadin. This isn't supported since Vaadin has prioritized the typical case with fetching data straight through JDBC. This means that you end up blocking a thread while the data is loading. Vaadin 23 will add support for doing that blocking on a separate thread instead of keeping the UI thread blocked, but it will still be blocking.

The other half of your problem doesn't seem to be directly related to Vaadin. The exception message says that the Jackson instance used by the REST client isn't configured to support creating instances of org.springframework.data.domain.Page. I don't have direct experience with this part of the problem, so I cannot give any advice on exactly how to fix it.

CodePudding user response:

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.

  •  Tags:  
  • Related