Thymeleaf webflux: Only one data-driver variable is allowed to be specified as a model attribute, but at least two have been identified
Asked Answered
C

2

6

I'm trying to create a dashboard application where multiple widgets get updates through SSE. My DashboardController looks like:

public class DashboardController
{
    private WidgetService widgetService;

    public DashboardController(WidgetService widgetService)
    {
        this.widgetService = widgetService;
    }

    @GetMapping("/")
    public String index(final Model model)
    {
        for(WidgetInterface<?> widget : widgetService.getAll()) {
            model.addAttribute("widget-data-driver-" + widget.getIdentifier(),
                    new ReactiveDataDriverContextVariable(widget.getInitialData(), 100));
        }
        model.addAttribute("widgets", widgetService.getAll());
        return "index";
    }
}

And my WidgetInterface:

public interface WidgetInterface<T>
{
    public String getIdentifier();
    public String getTitle();
    public String getStreamEndpoint();
    public Flux<T> getInitialData();
    public Map<String, String> getColumns();
    public String getThymeLeafFraction();
    public Date getLastItemDate();
    public Flux<T> getItemsUpdatedSince(Date date);
}

All is working fine for one widget (from the WidgetService). I'm trying to pass a ReactiveDataDriverContextVariable for each widget to Thymeleaf. But I get the following error:

org.thymeleaf.exceptions.TemplateProcessingException: Only one data-driver variable is allowed to be specified as a model attribute, but at least two have been identified: 'widget-data-driver-nufeed' and 'widget-data-driver-weatherapi'

I have two active widgets, one is an RSS feed an one implements weather api. I understand the error, but how to can I set up the reactive variables for multiple widgets?

Update answer based on Angelos comment (2022)

I tried to replace the DashboardController.index method with:

GetMapping("/")
public String index(final Model model, Authentication authentication)
{
     setAuthentication(authentication);
     Flux<WidgetInterface<?>> flux = Flux.just(widgetService.getAll().toArray(new WidgetInterface<?>[0]));
     model.addAttribute("widgets", new ReactiveDataDriverContextVariable(flux));
     return "index";
}

This is working, except for the fact that now I cannot iterate over my widget.getInitialData() in thymeleaf (this is a fraction file as described above):

<div data-th-each="item : ${widget.getInitialData()}"  th:data-id="@{${item.id}}" id="art">
    <a th:data-lightbox="'' + ${widget.identifier}" th:href="${item.image}">
        <img th:src="'https://images.weserv.nl/?url=' + ${item.image} + '&w=200&h=200&fit=cover&a=top'" th:data-lightbox="'' + ${widget.identifier}"
             class="rounded d-block user-select-none" style="max-height:100px;max-width:150px;margin-right:10px;" align="left" />
    </a>
    <h5 th:text="${item.description}" class="card-title"></h5>
</div>

This gives this error:

org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "item.id" (template: "art_fraction" - line 1, col 56)
    at org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:292) ~[thymeleaf-spring5-3.0.15.RELEASE.jar:3.0.15.RELEASE]
Centerboard answered 11/7, 2022 at 10:14 Comment(5)
may you post also your thymeleaf page?Assuming
as far as i can see [thymeleaf.org/apidocs/thymeleaf-spring5/3.0.11.RELEASE/org/… (thymeleaf.org/apidocs/thymeleaf-spring5/3.0.11.RELEASE/org/…) the ReactiveDataDriverContextVariable can contain a Flux of object; so I guess you should change your code by passing just 1 ReactiveDataDriverContextVariable with a stream of objectsAssuming
This is working, except for the fact that I now I cannot iterate over widget.getInitialData() in thymeleaf which is a flux. So I think Thymeleaf expects a ReactiveDataDriverContextVariable for each Flux object, except there can only be one per template. See my question for the error and what I have tried.Centerboard
the error is saying that Spring EL can't evaluate item.id. In the widget interface I see no property id; am I missing anything?Assuming
The widget.getInitialData() returns an entity list, the entity contains the id. I will update the question with a link to a stripped version of my dashboard application so the problem can be seen and analyzed.Centerboard
G
1

SpringWebFluxTemplateEngine used by default in the Spring 6 Thymeleaf integration software version 3.0.3 is the standard implementation of ISpringWebFluxTemplateEngine. It's known limitation is that only one variable can be handled as reactive data-driven. Otherwise an exception will be thrown as can be seen in the source code:

    String dataDriverVariableName = null;
    final Set<String> contextVariableNames = context.getVariableNames();
    for (final String contextVariableName : contextVariableNames) {
        final Object contextVariableValue = context.getVariable(contextVariableName);
        if (contextVariableValue instanceof IReactiveDataDriverContextVariable) {
            if (dataDriverVariableName != null) {
                throw new TemplateProcessingException(
                        "Only one data-driver variable is allowed to be specified as a model attribute, but " +
                        "at least two have been identified: '" + dataDriverVariableName + "' " +
                        "and '" + contextVariableName + "'");
            }
            dataDriverVariableName = contextVariableName;
        }
    }

The workaround provided by Angelo Immediata will not help because you need to read data from the widget.getInitialData() Flux and not from widgetService.getAll().

The only solution I can think of is to implement the dashboard as a portlet. Then you can implement two Spring Boot controllers with separate models. One for RSS feed and one for whether API.

Giffie answered 30/1 at 8:49 Comment(1)
This might indeed be a good workaround. This made me think about another way to load each widget through ajax, so the controller responds with just one widget/ data driver each time. But I see no valid reason in the way thymeleaf has set this up, since one data driver is just too limited.Centerboard
A
0

I'd make your template code little bit simpler. I'd change it in this way:

JAVA SIDE

GetMapping("/")
public String index(final Model model, Authentication authentication)
{
     setAuthentication(authentication);
     Flux<WidgetInterface<?>> flux = Flux.just(widgetService.getAll().toArray(new WidgetInterface<?>[0]));
     model.addAttribute("widgets", new ReactiveDataDriverContextVariable(flux));
     return "index";
}

TEMPLATE SIDE:

        <table class="table table-striped">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Image</th>
                    <th>Identifier</th>
                </tr>
            </thead>
            <tbody>
                <tr data-th-each="myWidget : ${widgets}">
                    <td>[[${myWidget.id}]]</td>
                    <td>[[${myWidget.image}]]</td>
                    <td>[[${myWidget.identifier}]]</td>
                </tr>
            </tbody>
        </table>

Obviously this is a sample... adapt it to your case

Angelo

Assuming answered 19/7, 2022 at 17:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.