webfirmframework for Java Experts
wffweb Configurations
Unlike other java ee frameworks, wffweb doesn't have any direct dependency over java ee classes. And, websocket implementation is different in different servers and i.e. not all servers follow JSR 356 specification. So, we have to configure wffweb if its server client communication feature needs to be used.
Configuration steps
There are three main configurations.- Configure websocket,
- set up a session listener and
- set up a
BrowserPage
. Of course, once we have configuredBrowserPage
, we have to expose it by any servlet/rest.
Configure websocket
There must be a websocket url set up with your server, that websocket url should not be used by others. It should be dedicated for wffweb. The websocket should support sending and receiving binary data because wffweb and client communication is done via a binary protocol called wff binary message. when the websocket connection is opened, it must be informed to the wffweb as follows
On Open websocket connection event
If the websocket supports partial message sending and maxBinaryMessageBufferSize is available or if the websocket conforms to JSR 356 implementation then
//webSocket session
List wffInstanceIds = session.getRequestParameterMap().get(BrowserPage.WFF_INSTANCE_ID);
String wffInstanceId = wffInstanceIds.get(0);
BrowserPage browserPage = BrowserPageContext.INSTANCE.webSocketOpened(wffInstanceId);
final int maxBinaryMessageBufferSize = session
.getMaxBinaryMessageBufferSize();
browserPage.addWebSocketPushListener(session.getId(), data -> {
ByteBufferUtil.sliceIfRequired(data, maxBinaryMessageBufferSize,
(part, last) -> {
try {
session.getBasicRemote().sendBinary(part, last);
} catch (IOException e) {
LOGGER.log(Level.SEVERE,
"IOException while session.getBasicRemote().sendBinary(part, last)",
e);
try {
session.close();
} catch (IOException e1) {
LOGGER.log(Level.SEVERE,
"IOException while session.close()",
e1);
}
throw new PushFailedException(e.getMessage(), e);
}
return !last;
});
});
other than JSR 356 websocket implementation
//webSocket session
List wffInstanceIds = session.getRequestParameterMap().get(BrowserPage.WFF_INSTANCE_ID);
String wffInstanceId = wffInstanceIds.get(0);
BrowserPage browserPage = BrowserPageContext.INSTANCE.webSocketOpened(wffInstanceId);
//or unique id with String.valueOf(session.hashcode()) if session.getId() is not available
browserPage.addWebSocketPushListener(session.getId(), new WebSocketPushListener() {
@Override
public void push(ByteBuffer data) {
try {
session.getBasicRemote()
.sendBinary(data);
} catch (Exception e) {
throw new PushFailedException(e.getMessage(), e);
}
}
});
The above code setting a listener so that wffweb can push messages to the client browser page.
When the websocket is closed i.e. on close event, it should be informed to wffweb as follows,
List wffInstanceIds = session.getRequestParameterMap().get(BrowserPage.WFF_INSTANCE_ID);
String wffInstanceId = wffInstanceIds.get(0);
//session.getId() is the id given while adding websocket listener
BrowserPageContext.INSTANCE.webSocketClosed(wffInstanceId, session.getId());
When the websocket receives a message i.e. on message event, it should be forwarded to wffweb as follows,
List wffInstanceIds = session.getRequestParameterMap().get("wffInstanceId");
String instanceId = wffInstanceIds.get(0);
BrowserPage browserPage = BrowserPageContext.INSTANCE.getBrowserPage(instanceId);
browserPage.webSocketMessaged(message);
Or if wffweb-3.0.2 or later we can also receive client data as partial bytes, Eg:
List wffInstanceIds = session.getRequestParameterMap().get(BrowserPage.WFF_INSTANCE_ID);
String instanceId = wffInstanceIds.get(0);
BrowserPage browserPage = BrowserPageContext.INSTANCE.getBrowserPage(instanceId);
//partialMessage is array of partial bytes, byte[]
//partial is true or false, if true that is the last part of the message
browserPage.getPayloadProcessor().webSocketMessaged(partialMessage, partial);
Here, wffInstanceId
is a unique id generated by BrowserPage
instance, it can be taken by browserPage.getInstanceId
method.
Each instance of a BrowserPage
will have its own unique instanceId
If you want to keep the
HttpSession
active as long as the websocket connection is alive, you have to do
the following. In
OnOpen
, get the httpSession object and set
httpSession.setMaxInactiveInterval(-1);
and in
OnClose
reset the session time out as
httpSession.setMaxInactiveInterval(60 * 30);
(the same value which is given in web.xml).
An http request to a dummy url may need to be made afterwards.
So if the websocket doesn't make a connection again within the given time, the
httpSession will be timed out. This can also avoid keeping a heart
beat request to the server to keep the httpSession alive. This is
the solution for the issue explained in the description of this ticket.
Refer from this fully configured code from sample project. Or checkout these minimal production ready projects.
Setting up session listener for wffweb
When the http session is closed, it must be informed to wffweb as follows.
@WebListener
public class SessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent sessionEvent) {
// NOP for wffweb
}
@Override
public void sessionDestroyed(HttpSessionEvent sessionEvent) {
BrowserPageContext.INSTANCE
.httpSessionClosed(sessionEvent.getSession().getId());
}
}
BrowserPage
represents UI page (window) of a browser. In a single page application, there will be only one BrowserPage
.
public class IndexPage extends BrowserPage {
@Override
public String webSocketUrl() {
//the websocket url you have configured for wffweb
return "ws://yourdomain.com/wffwebdemoproject/ws-for-index-page";
}
@Override
public AbstractHtml render() {
//keep this as a separate class so as to
//change its different portion
Html indexPageLayout = new Html(null) {{
new Head(this);
new Body(this) {{
new Div(this) {{
new H1(this) {{
new NoTag(this, "こんにちは WFFWEB");
}};
}};
}};
}};
return indexPageLayout;
}
}
Whatever changes made to
indexPageLayout
will automatically be reflected to the client browser. So, we can
keep it as a separate class for maintainability.
Once we have created a
BrowserPage
, we have to add it to
BrowserPageContext
.
BrowserPageContext
is the context which holds all
BrowserPage
instances. Adding a
BrowserPage
instance in to
BrowserPageContext
may be done inside a servlet because we need to create instance of
BrowserPage
only when there is a request from new session. i.e. we need to have
only one instance of the
IndexPage
per session. It may be added as follows
@WebServlet("/index")
public class IndexPageServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
try (OutputStream os = response.getOutputStream();) {
HttpSession session = request.getSession();
String instanceId = (String) session
.getAttribute("indexPageInstanceId");
BrowserPage browserPage = null;
if (instanceId != null) {
browserPage = BrowserPageContext.INSTANCE
.getBrowserPage(instanceId);
// if the server is restarted browserPage could be null here,
// so you could save this instance to db after addBrowserPage
// method
// and retried from db using browserPage.getInstanceId()
}
if (browserPage == null) {
browserPage = new IndexPage();
BrowserPageContext.INSTANCE.addBrowserPage(session.getId(),
browserPage);
session.setAttribute("indexPageInstanceId",
browserPage.getInstanceId());
}
browserPage.toOutputStream(os, "UTF-8");
os.flush();
}
}
}