Search code examples
jsfjakarta-eewebsocketwildflycdi

How close the gab between websocket server endpoint and session scoped backing beans


In short, I don't know how I notify a session scoped bean from a application scoped bean. I found a dirty hack that work, but I'm not sure there is a better way to solve this problem (or my dirty hack is not dirty :-D ).

Every websocket in java ee has a sessionid. But this sessionid is not the same like in jsf, and there is no way for a easy mapping. In my environment I have a jsf webpage, an underlying sessionscoped backing bean and a websocket, that is connected to an external service via jms. When the jsf page is loaded, and the websocket is also connected to the browser, the backing bean sends a request to the external service. When I got a async answer message via jms I dont know which websocket is connected with the jsf page/backing bean that send the request.

To solve this problem with a partly dirty hack, I wrote an application scoped mediator class.

@Named
@ApplicationScoped
public class WebsocketMediator {

    @Inject
    private Event<UUID> notifyBackingBeans;

    private Integer newSequenceId=0;

    // I need this map for the dirty hack
    private Map<UUID, BackingBean> registrationIdBackingBeanMap;

    private Map<Integer, UUID> sequenceIdRegistrationIdMap;
    registrationIdWebSocketMap = new ConcurrentHashMap<>();

    public UUID register(BackingBean backingBean) {
        UUID registrationId = UUID.randomUUID();

        registrationIdSequenceMap.put(registrationId, new HashSet<>());
        registrationIdBackinBeanMap.put(registrationId, backingBean);
    }

    public Integer getSequenceId(UUID registrationId) {
        sequenceId++;
        sequenceIdRegistrationIdMap.put(sequenceId, registrationId);
        registrationIdSequenceMap.get(registrationId).add(sequenceId);
        return sequenceId;
    }

    // this is called from the ws server enpoint
    public void registerWebsocket(UUID registrationId, Session wsSession) {
        registrationIdWebSocketMap.put(registrationId, wsSession);
        websocketRegistrationIdMap.put(wsSession.getId(), registrationId);
        notifyBackingBeans.fire(registrationId); // This does not work

        SwitchDataModel switchDataModel = registrationIdSwitchDataModelMap.get(registrationId);
        if (backingBean != null) {
            backingBean.dirtyHackTrigger();
        }
    }

    public void unregisterWebsocket(String wsSessionId) {
         ...
    }
}

The backing bean calls a registration method and gets a uniq random registration id (uuid) . The registration id is placed in a jsf table as a hidden data attribute (f:passTrough). When the websocket is connected, the ws.open function is called in the browser and send the registration id via the websocket to the websocket server endpoint class. The Server endpoint class call the public void registerWebsocket(UUID registrationId, Session wsSession) method in the mediator and the registration id is mapped. When the backing bean is timed out, I call a unregister method from an @PreDestroyed annotated method. Every time, when the external system is called via jms, I put a sequence id into the payload. The Sequence Id is registered in the Mediator class. Every time when the external system sends a message I can lookup the correct websocket in the mediator to bypass the message via the websocket to the correct browser.

Now the system is able to receive async events via the external system, but the backing bean doesn't know that. I tried to send the registration id in a cdi event to the session scoped backing bean, but the event never reach an observer in a session scoped backing bean. So I realized this gab with a dirty hack. I put the instance of every backing bean with the registration id as key into a map in the mediator in the registration method. I placed in the public void registerWebsocket(UUID registrationId, Session wsSession) a dirty hack trigger call of the backing bean. Is there a better solution?

I use Wildfly 13 with CDI 1.2.

Thanks in advance!


Solution

  • I found a solution. When the webpage is called, the @SessionScoped bean is created. I calculate a unique registration id and put it as attribute in the HttpSession:

    @PostConstruct
    public void register() {
        registrationId = switchPortMediator.register(this);
        HttpSession session = (HttpSession) facesContext.getExternalContext().getSession(true);
        session.setAttribute("switchRegistrationId", registrationId);
        log.debug("Registered at mediator - ID=" + registrationId + " http session = "+ session.getId());
    }
    

    At the @ServerEndpoint annotated web socket endpoint, is the @OnOpen annotated method. This method is called when the webpage is loaded and the websocket is established. The websocket endpoint class is @ApplicationScoped. The attribute map, from the @SessionScoped bean, where the registration id is stored, is accessable in the EndpointConfig of the websocket endpoint. Here is the @OnOpen annotated method:

    @OnOpen
    public void onOpen(Session wsSession, EndpointConfig config) {
        UUID registrationId = (UUID) config.getUserProperties().get("switchRegistrationId");
       websocketRegistrationIdMap.put(registrationId, wsSession);
    }
    

    When the JSF page is loaded, the @SessionScoped bean calculate the registration id, put it in the attribute map and sent an async message via jms to an external system. The external system send an answer message, with the registration id inside and a payload. When the external message arrives via JMS in the websocket endpoint class, the resulting wsSession can be retrived from the websocketRegistrationIdMap and the payload can be send via the websocket to the browser, who initiate the async message. The dom updates in the website are processed by javascript.