Real time updates from database using JSF/Java EE
Asked Answered
P

3

30

I have one application running in the following environment.

  • GlassFish Server 4.0
  • JSF 2.2.8-02
  • PrimeFaces 5.1 final
  • PrimeFaces Extension 2.1.0
  • OmniFaces 1.8.1
  • EclipseLink 2.5.2 having JPA 2.1
  • MySQL 5.6.11
  • JDK-7u11

There are several public pages which are lazily loaded from the database. A few CSS menus are displayed on the header of the template page like displaying category/subcategory-wise featured, top seller, new arrival etc products.

The CSS menus are populated dynamically from the database based on various categories of products in the database.

These menus are populated on every page load which is completely unnecessary. Some of these menus require complex/expensive JPA criteria queries.

Currently the JSF managed beans that populate these menus are view scoped. They all should be application scoped, be loaded only once on application start up and be updated only when something in the corresponding database tables (category/subcategory/product etc) is updated/changed.

I made some attempts to understand WebSokets (never tried before, completely new to WebSokets) like this and this. They worked fine on GlassFish 4.0 but they don't involve databases. I'm still not able to understand properly how WebSokets work. Especially when database is involved.

In this scenario, how to notify the associated clients and update the above-mentioned CSS menus with the latest values from the database, when something is updated/deleted/added to the corresponding database tables?

A simple example/s would be great.

Pralltriller answered 20/9, 2014 at 10:28 Comment(4)
To be clear, you've the push part already finished in the JSF side (with websockets) and you're merely asking how to trigger it from JPA side on during an entity change event? So external changes in DB beyond control of JPA do not need to be accounted?Gwenore
Either way, if something is changed in the (associated) database tables, it should be reflected and notified to the associated client(s). (I myself is lacking the actual concept behind it - how can this be done correctly. I never did such things before).Pralltriller
I read this Oracle documentations including those two practical exercises in the question still I do not understand how it actually works. What I wrote in the question are just my primary thoughts. It does not necessarily happen at all in that way.Pralltriller
The link in the previous comment is broken. This is the link to the Oracle tutorials. A good example to start with (true it uses GlassFish and NetBeans but needless to mention that it can be verified in any equivalent environment).Pralltriller
G
54

Preface

In this answer, I'll assume the following:

  • You're not interested in using <p:push> (I'll leave the exact reason in the middle, you're at least interested in using the new Java EE 7 / JSR356 WebSocket API).
  • You want an application scoped push (i.e. all users gets the same push message at once; thus you're not interested in a session nor view scoped push).
  • You want to invoke push directly from (MySQL) DB side (thus you're not interested in invoking push from JPA side using an entity listener). Edit: I'll cover both steps anyway. Step 3a describes DB trigger and step 3b describes JPA trigger. Use them either-or, not both!

1. Create a WebSocket endpoint

First create a @ServerEndpoint class which basically collects all websocket sessions into an application wide set. Note that this can in this particular example only be static as every websocket session basically gets its own @ServerEndpoint instance (they are unlike servlets thus stateless).

@ServerEndpoint("/push")
public class Push {

    private static final Set<Session> SESSIONS = ConcurrentHashMap.newKeySet();

    @OnOpen
    public void onOpen(Session session) {
        SESSIONS.add(session);
    }

    @OnClose
    public void onClose(Session session) {
        SESSIONS.remove(session);
    }

    public static void sendAll(String text) {
        synchronized (SESSIONS) {
            for (Session session : SESSIONS) {
                if (session.isOpen()) {
                    session.getAsyncRemote().sendText(text);
                }
            }
        }
    }

}

The example above has an additional method sendAll() which sends the given message to all open websocket sessions (i.e. application scoped push). Note that this message can also quite good be a JSON string.

If you intend to explicitly store them in application scope (or (HTTP) session scope), then you can use the ServletAwareConfig example in this answer for that. You know, ServletContext attributes map to ExternalContext#getApplicationMap() in JSF (and HttpSession attributes map to ExternalContext#getSessionMap()).

2. Open the WebSocket in client side and listen on it

Use this piece of JavaScript to open a websocket and listen on it:

if (window.WebSocket) {
    var ws = new WebSocket("ws://example.com/contextname/push");
    ws.onmessage = function(event) {
        var text = event.data;
        console.log(text);
    };
}
else {
    // Bad luck. Browser doesn't support it. Consider falling back to long polling.
    // See http://caniuse.com/websockets for an overview of supported browsers.
    // There exist jQuery WebSocket plugins with transparent fallback.
}

As of now it merely logs the pushed text. We'd like to use this text as an instruction to update the menu component. For that, we'd need an additional <p:remoteCommand>.

<h:form>
    <p:remoteCommand name="updateMenu" update=":menu" />
</h:form>

Imagine that you're sending a JS function name as text by Push.sendAll("updateMenu"), then you could interpret and trigger it as follows:

    ws.onmessage = function(event) {
        var functionName = event.data;
        if (window[functionName]) {
            window[functionName]();
        }
    };

Again, when using a JSON string as message (which you could parse by $.parseJSON(event.data)), more dynamics is possible.

3a. Either trigger WebSocket push from DB side

Now we need to trigger the command Push.sendAll("updateMenu") from the DB side. One of simplest ways it letting the DB to fire a HTTP request on a web service. A plain vanilla servlet is more than sufficient to act like a web service:

@WebServlet("/push-update-menu")
public class PushUpdateMenu extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Push.sendAll("updateMenu");
    }

}

You've of course the opportunity to parameterize the push message based on request parameters or path info, if necessary. Don't forget to perform security checks if the caller is allowed to invoke this servlet, otherwise anyone else in the world other then the DB itself would be able to invoke it. You could check the caller's IP address, for example, which is handy if both DB server and web server run at the same machine.

In order to let the DB fire a HTTP request on that servlet, you need to create a reusable stored procedure which basically invokes the operating system specific command to execute a HTTP GET request, e.g. curl. MySQL doesn't natively support executing an OS specific command, so you'd need to install a user defined function (UDF) for that first. At mysqludf.org you can find a bunch of which SYS is of our interest. It contains the sys_exec() function which we need. Once installed it, create the following stored procedure in MySQL:

DELIMITER //
CREATE PROCEDURE menu_push()
BEGIN 
SET @result = sys_exec('curl http://example.com/contextname/push-update-menu'); 
END //
DELIMITER ;

Now you can create insert/update/delete triggers which will invoke it (assuming table name is named menu):

CREATE TRIGGER after_menu_insert
AFTER INSERT ON menu
FOR EACH ROW CALL menu_push();
CREATE TRIGGER after_menu_update
AFTER UPDATE ON menu
FOR EACH ROW CALL menu_push();
CREATE TRIGGER after_menu_delete
AFTER DELETE ON menu
FOR EACH ROW CALL menu_push();

3b. Or trigger WebSocket push from JPA side

If your requirement/situation allows to listen on JPA entity change events only, and thus external changes to the DB does not need to be covered, then you can instead of DB triggers as described in step 3a also just use a JPA entity change listener. You can register it via @EntityListeners annotation on the @Entity class:

@Entity
@EntityListeners(MenuChangeListener.class)
public class Menu {
    // ...
}

If you happen to use a single web profile project wherein everything (EJB/JPA/JSF) is thrown together in the same project, then you can just directly invoke Push.sendAll("updateMenu") in there.

public class MenuChangeListener {

    @PostPersist
    @PostUpdate
    @PostRemove
    public void onChange(Menu menu) {
        Push.sendAll("updateMenu");
    }

}

However, in "enterprise" projects, service layer code (EJB/JPA/etc) is usually separated in EJB project while web layer code (JSF/Servlets/WebSocket/etc) is kept in Web project. The EJB project should have no single dependency on web project. In that case, you'd better fire a CDI Event instead which the Web project could @Observes.

public class MenuChangeListener {

    // Outcommented because it's broken in current GF/WF versions.
    // @Inject
    // private Event<MenuChangeEvent> event;

    @Inject
    private BeanManager beanManager;

    @PostPersist
    @PostUpdate
    @PostRemove
    public void onChange(Menu menu) {
        // Outcommented because it's broken in current GF/WF versions.
        // event.fire(new MenuChangeEvent(menu));

        beanManager.fireEvent(new MenuChangeEvent(menu));
    }

}

(note the outcomments; injecting a CDI Event is broken in both GlassFish and WildFly in current versions (4.1 / 8.2); the workaround fires the event via BeanManager instead; if this still doesn't work, the CDI 1.1 alternative is CDI.current().getBeanManager().fireEvent(new MenuChangeEvent(menu)))

public class MenuChangeEvent {

    private Menu menu;

    public MenuChangeEvent(Menu menu) {
        this.menu = menu;
    }

    public Menu getMenu() {
        return menu;
    }

}

And then in the web project:

@ApplicationScoped
public class Application {

    public void onMenuChange(@Observes MenuChangeEvent event) {
        Push.sendAll("updateMenu");
    }

}

Update: at 1 april 2016 (half a year after above answer), OmniFaces introduced with version 2.3 the <o:socket> which should make this all less circuitous. The upcoming JSF 2.3 <f:websocket> is largely based on <o:socket>. See also How can server push asynchronous changes to a HTML page created by JSF?

Gwenore answered 12/10, 2014 at 8:22 Comment(18)
Can this be done by just using JPA callbacks (or something else) like @PrePersist, @PostPersist, @PreUpdate ...? (I'm not good at PL/SQL on database side {though I will go with it somehow later on, no worry. Good answer :) }).Pralltriller
I asked you in my first comment on your question if external changes to the DB needed to be accounted. You replied "either way", so I understood that you wanted to cover this as well. JPA entity listeners wouldn't be sufficent to cover this as they are not notified when you manipulate the DB from outside the application (e.g. via some DB admin tool or even another application sharing the same DB).Gwenore
I extended the answer with step 3b in case users are interested in JPA entity listeners only.Gwenore
In most cases I rely on JPA side and CDI events, nice to know that it's not restricted only on that side, UDF seems promising for some cases where might be multiple apps on one db! This shoudl be a blog post tho :)Mainspring
You've mentioned that he might not be interested in p:push and You'll leave the exact reason in the middle ? What is the reason that you might consider p:push not suitable ?Mainspring
This CDI event @Inject private Event<MenuChangeEvent> event; is not injected. This throws a java.lang.NullPointerException. I annotated its class (MenuChangeListener) with @javax.enterprise.context.ApplicationScoped. Does any possible symptom come to mind at a glance? I will go with a separate question later on otherwise removing this comment.Pralltriller
@Inject Event is unfortunately severely broken in current versions of both GlassFish and WildFly. You need @Inject BeanManager and then BeanManager#fireEvent() instead. See also a.o. issues.jboss.org/browse/WFLY-2387 I'll update the answer.Gwenore
I got the same java.lang.NullPointerException while injecting javax.enterprise.inject.spi.BeanManager and firing the event using beanManager.fireEvent(new MenuChangeEvent(menu)) on GlassFish 4.1 :)Pralltriller
Did actually not test on GF, but works on WF. What if you manually grab it from JNDI? If you're using OmniFaces, use Beans.getManager().fireEvent(...);.Gwenore
I currently avoided adding OmniFaces to the enterprise project but this BeanManager beanManager = (BeanManager) InitialContext.doLookup("java:comp/BeanManager"); and this BeanManager beanManager = (BeanManager) new InitialContext().lookup("java:comp/BeanManager"); both failed with the exact same exception alternatively. The first one is working fine somewhere else in the application though.Pralltriller
Manually grabbing an instance of BeanManager worked either way by using BeanManager beanManager = (BeanManager) InitialContext.doLookup("java:comp/BeanManager");. The problem was a class library which I have been using for remote EJBs. Changes made to the class library did not propagate until the application had fully been rebuilt/redeployed. An injection of BeanManager however, did not work for some unknown reasons. Thanks.Pralltriller
"When using a JSON string as message (which you could parse by $.parseJSON(event.data)), more dynamics is possible." Here I missed this line window[functionName]();. I found this an unusual syntax (everything works fine though). I have a JSON message - {"jsonMessage":"updateMenu"} (returned by session.getAsyncRemote().sendText(message); and parsed by JSON.parse(event.data); where message=JsonProvider.provider().createObjectBuilder().add("jsonMessage", "updateMenu").build().toString();). I cannot imagine the relationship between the JSON message and "more dynamics is possible"?Pralltriller
You could parameterize the function name and arguments. E.g. {"functionName": "updateMenu", "arguments": ["foo", "bar", "baz"]}.Gwenore
Does javax.enterprise.event.Event<T extends Object> have to be injected into CDI managed beans like an application scoped bean (where it fails according to a few previous comments) or into plain Java classes (i.e. in other words, is it necessary for that injection to take place in CDI managed beans and plain Java classes according to the documentation)? This is because it gets injected successfully into EJBs. I have just attempted to inject it into a singleton EJB in GlassFish 4.1 as a trial and it has succeeded.Pralltriller
Basically, every class with a default constructor, or a constructor annotated with @Inject is considered a "bean". Those classes are eligible for dependency injection. You may only need to add state/scope annotations on the class to put the bean instance in the desired state/scope, otherwise you can't get a hand of it elsewhere.Gwenore
Never mind. There was a mistake in library import, since the beginning. I attempted to import Java EE 7 API Library (containing the CDI API) at three different places namely a web module, EJB module and a class library but I mistakenly imported Java EE Web 7 API Library in the EJB module. The CDI event was successfully injected in GlassFish 4.1 after I had corrected the wrong import by replacing that library with Java EE 7 API Library. (After this, I even replaced Java EE 7 API Library with only a single JAR file namely cdi-api.jar).Pralltriller
Is there any specialty about "view scoped push" i.e. is there any special built-in way to accomplish "view scoped push"? In general, I just pass a random, view based token to @ServerEndpoint("EndpointURI") through either query-string or @PathParam to accomplish it.Pralltriller
Pass view-specific parameter (e.g. view ID) as request (path|query) param and associate sessions with it in a map. In PrimeFaces it's called "channel".Gwenore
C
6

Since you are using Primefaces and Java EE 7 it should be easy to implement:

use Primefaces Push ( example here http://www.primefaces.org/showcase/push/notify.xhtml )

  • Create a view which listen to a Websocket endpoint
  • Create a database listener which produces a CDI event on database change
    • The payload of the event could either be the delta of the latest data or just and update information
  • Propagate the CDI event via Websocket to all clients
  • Clients updating the data

Hope this helps If you need some more details just ask

Regards

Coley answered 23/9, 2014 at 12:23 Comment(0)
A
1

PrimeFaces has poll features to update the component automatically. In the following example, <h:outputText> will be auto updated every 3 seconds by <p:poll>.

How to notify the associated clients and update the above-mentioned CSS menus with the latest values from the database?

Create a listener method like process() to select your menu data. <p:poll> will be auto-update your menu component.

<h:form>
    <h:outputText id="count"
                  value="#{AutoCountBean.count}"/> <!-- Replace your menu component-->

    <p:poll interval="3" listener="#{AutoCountBean.process}" update="count" />
</h:form>
@ManagedBean
@ViewScoped
public class AutoCountBean implements Serializable {

    private int count;

    public int getCount() {
        return count;
    }

    public void process() {
        number++; //Replace your select data from db.
    }
}   
Athalla answered 22/9, 2014 at 7:4 Comment(1)
<p:poll> is useful for triggering periodic operations that happen at a specified point in time. In this case, it is not like long polling that happens at regular intervals. The operation here is not dependent upon a timer. It should happen, only when something else happens or never happen otherwise.Pralltriller

© 2022 - 2024 — McMap. All rights reserved.