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?