Communication between core application and backoffice ui

Hi,

I really like the new jmix and can’t wait for 1.0. I just stumbled across the following problem:

What would be the recommended way to communicate between core (spring boot application, etc.) and backoffice ui? I learned that you can not just inject a screen there (vaadin exception).

How would you notify a screen controller from an EntityChangedEvent or a Spring Boot module like Spring-REST? Currently I just write to an Entity and poll DataManager from the screen controller every second - but that does not seem to be very elegant. Is there something easier than Messaging API, RabbitMQ, Kafka, etc.?

Thank you very much

Martin

Hi Martin,

Sure, there is a solution to notify screens from business logic and persistence layers. It’s based on Spring’s application events and works within a single server instance.

Firstly, let’s consider a simple solution with the UiEventPublisher bean provided by the framework. It delivers events to screens opened by the current user in a web browser tab. In other words, if a user clicks some button in a browser tab, and this button click eventually invokes the code that sends an event, the event will be delivered to screens opened in that tab.

Create an event class:

package com.company.app.entity;

import org.springframework.context.ApplicationEvent;

public class FooChangedEvent extends ApplicationEvent {

    public FooChangedEvent(Object source) {
        super(source);
    }
}

Create an event listener in the screen which should be notified. In the example below it’s the main screen:

public class MainScreen extends Screen implements Window.HasWorkArea {
// ...
    @Autowired
    private Notifications notifications;

    @EventListener
    private void fooChanged(FooChangedEvent event) {
        notifications.create().withCaption("A Foo instance has been changed").show();
    }

Send the event using the UiEventPublisher:

@Component
public class FooEventListener {

    @Autowired
    private UiEventPublisher uiEventPublisher;

    @TransactionalEventListener
    public void onFooChangedAfterCommit(EntityChangedEvent<Foo> event) {
        uiEventPublisher.publishEvent(new FooChangedEvent(this));
    }
}

If you want to deliver an event to all currently connected users and to all their browser tabs, you need to create a more complex publisher in your project. Below is a working example - it’s a bean that registers all Vaadin sessions created on the server and iterates through them when sending events:

package com.company.app;

import com.vaadin.server.VaadinSession;
import io.jmix.ui.App;
import io.jmix.ui.AppUI;
import io.jmix.ui.event.AppInitializedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

@Component
public class GlobalUiEventPublisher {

    private static final Logger log = LoggerFactory.getLogger(GlobalUiEventPublisher.class);

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private final List<WeakReference<VaadinSession>> sessions = new ArrayList<>();

    @EventListener
    public void onAppStart(AppInitializedEvent event) {
        lock.writeLock().lock();
        try {
            sessions.add(new WeakReference<>(VaadinSession.getCurrent()));
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void publishEvent(ApplicationEvent event) {
        ArrayList<VaadinSession> activeSessions = new ArrayList<>();

        int removed = 0;
        lock.readLock().lock();
        try {
            for (Iterator<WeakReference<VaadinSession>> iterator = sessions.iterator(); iterator.hasNext(); ) {
                WeakReference<VaadinSession> reference = iterator.next();
                VaadinSession session = reference.get();
                if (session != null) {
                    activeSessions.add(session);
                } else {
                    lock.readLock().unlock();
                    lock.writeLock().lock();
                    try {
                        iterator.remove();
                        lock.readLock().lock();
                    } finally {
                        lock.writeLock().unlock();
                    }
                    removed++;
                }
            }
        } finally {
            lock.readLock().unlock();
        }

        if (removed > 0) {
            log.debug("Removed {} Vaadin sessions", removed);
        }
        log.debug("Sending {} to {} Vaadin sessions", event, activeSessions.size());

        for (VaadinSession session : activeSessions) {
            // obtain lock on session state
            session.access(() -> {
                if (session.getState() == VaadinSession.State.OPEN) {
                    // active app in this session
                    App app = App.getInstance();

                    // notify all opened web browser tabs
                    List<AppUI> appUIs = app.getAppUIs();
                    for (AppUI ui : appUIs) {
                        if (!ui.isClosing()) {
                            // work in context of UI
                            ui.accessSynchronously(() -> {
                                ui.getUiEventsMulticaster().multicastEvent(event);
                            });
                        }
                    }
                }
            });
        }
    }
}

Use this publisher instead of the built-in one:

@Component
public class FooEventListener {

    @Autowired
    private GlobalUiEventPublisher globalUiEventPublisher;

    @TransactionalEventListener
    public void onFooChangedAfterCommit(EntityChangedEvent<Foo> event) {
        globalUiEventPublisher.publishEvent(new FooChangedEvent(this));
    }
}

Regards,
Konstantin

1 Like

@krivopustov, I thank you for your perfect reply. This solves my issue as well. Great job!

Thank you very much for the detailed answer. I will just implement it.