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

6 Likes

@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.

Hello everyone!
First of all sorry for my broken English.
I don’t want to create a new topic, so i’ll ask here…

Faced with strange behavior. I used the implementation of the global event publisher (which is present here) with minor changes. I have made it so that notifications published through it are sent only to administrators and the user on whose behalf the event was generated.

So, i catch this custom event with method which is annotated by EvenListener annotation in screen object. And if i have opened admin screen with browse entities in one browser and make changes which generates event behalf regular user in other browser then admin’s screen dataLoader loads changes like if it was performed with RowLevel regular user role (which bounds selecting jpa query by client_id) which in turn generated this event.

Changed part of publisher publish method:

...
    WeakReference<VaadinSession> reference = iterator.next();
    VaadinSession session = reference.get();
    if (session != null) {
        WrappedSession wrappedSession = session.getSession();
        if (wrappedSession != null) {
            Object securityContext = wrappedSession.getAttribute("SPRING_SECURITY_CONTEXT");
            if (securityContext != null) {
                SecurityContext springSecurityContext = (SecurityContext) securityContext;

                boolean isFullAccess = springSecurityContext.getAuthentication()
                    .getAuthorities()
                    .stream()
                    .anyMatch(grantedAuthority -> grantedAuthority.getAuthority()
                        .equals(FullAccessRole.CODE));

                boolean selfSourceUser = ((User) springSecurityContext.getAuthentication()
                    .getPrincipal()).getId()
                    .equals(((TargetUserEvent) event).getSourceUserId());

                if (isFullAccess || selfSourceUser) {
                    targetUserSessions.add(session);
                }
            }
        }
...

Catching event and call load method of DataLoader:

@EventListener
    public void BankAccountsChangedAdminEventListener(TargetUserEvent event) {
        bankAccountsDl.load();
    }

I understands, that i missing from view something, but i can’t to realize what exactly…

Video with problem.

I will be grateful for any help or hint…

I have noticed, that in screen’s object in depending of user context changing not all injected dependencies. Particulary the CurrentAuthorization bean not changed, but dataloaders and containers yes. So, I decided to remove rowlevel role and set select query for browsing entites programmaticaly (just have added bounding ‘where’) in depends with authorites. And all works fine now.

Result

That’s the expected behavior, because the execution thread contains the security context of the sending user.
If you need to perform any operations from another user, use System Authentication, for example:

@Autowired
private SystemAuthenticator systemAuthenticator;

@EventListener
public void BankAccountsChangedAdminEventListener(TargetUserEvent event) {
    systemAuthenticator.runWithSystem(() ->
        bankAccountsDl.load()
    );
}
2 Likes

Hello, thanks for the answer!
I suspected I was missing something about the execution context. I’ll read about this component of the system! Thanks!

@krivopustov

any plans on making something like this GlobalUiEventPublisher part of the framework?

Hi @tom.monnier
No plans currently, feel free to create an issue: Issues · jmix-framework/jmix · GitHub

@krivopustov done

3 Likes