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 configured BrowserPage, 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.

Get technical assistance

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();
        }

    }

}

Download demo projects from here