bekwam courses

WildFly WebSocket Development with IntelliJ and Maven

January 4, 2019

Most of web programming involves a browser sending an HTTP request to a server, the server processing the request, and the server returning an HTTP response. This Request-Response model works well when the processing is quick and the payloads are small. However, when service is slow, the user is forced to wait. If that waiting is too long, then a timeout results. This article presents a solution called WebSockets which gives the user continuous feedback and timely feedback on processing using a firewall-friendly protocol.

Unlike plain HTTP, WebSockets are a bi-directional protocol. This means that a connection is retained during an interaction, and the server can continually push updates to the browser. WebSockets are asynchronous. At any point, either connection can send a message to the other end. Although this particular example addresses long-running processes, many other use cases can be supported such as pushing chat messages to connected clients or transmitting video game positions.

The WebSocket API was added to Java EE 7. Every JavaEE app server is required to support it. The code in this example was deployed to WildFly 15 which is a JavaEE 8 compliant app server.

Demo

The following video shows a web page "imageInfo.html". The function of imageInfo.html is to find the file sizes of a bunch of images on the Web. A plain HTTP GET retrieves the static web resource from the WildFly 15 app server. Pressing the Run Fetch button opens a WebSocket to WildFly and immediately sends a message to start the processing. WildFly hits each of the specified URLs, retrieving each file size. As each URL is hit, a message is sent back to the browser which is appended to the imageInfo.html document.

There is also a status change. Too fast to see, the status label is marked as "Started" when the button is pressed. As the updates are piped from the server back to the client, the status changes to "Running". Finally, when the last image has been queried, the server sends a terminal message and the label is marked as "Finished".

Maven and Project Setup

The code will be developed in the IntelliJ IDE as a multi-module Maven project for portability. Although there is only one module in this example, the structure will be expanded upon in future posts. The module is a WAR to be deployed in WildFly.

This article shows how to develop RESTful Web Services for the WildFly platform. The services will be built using Maven which to support Continuous Integration. The development environment is the IntelliJ IDE which allows for some optimizations to make development more rapid.

This article is on the IntelliJ Ultimate Edition which is the commercial (not free) edition. Ultimate is needed for the JBoss connector.

  1. Start IntelliJ
  2. Select New > Project > Maven
  3. Set the groupId and artifactId. This demo uses "org.bekwam" and "wf15-websocket-demo"
  4. Set the location and project name

With wf15-websocket-demo selected in the Project Browser

  1. Right-click and select New > Module > Maven
  2. Verify that there is a parent specified
  3. Set the artifactId. This demo uses "wf15-websocket-demo-web"
  4. Set the location and module name.

Next, edit the parent and module poms. The individual edits will be described below. Use the listed pom.xml files for the specific syntax. A few steps are flagged as needed only for this demo and not for WebSocket projects in general.

  1. Double-click on the parent pom to edit
  2. Add in the properties element to set the Java version to 8
  3. Double-click on the child module pom to edit
  4. Add in the dependency for the WebSocket API
  5. Add in the dependency for CDI (javax.inject)
  6. Add in the build plugin directive to not require a web.xml
  7. NOT REQUIRED FOR WEB SOCKETS : Add a Gson dependency for JSON processing
  8. NOT REQUIRED FOR WEB SOCKETS : Add extra dependencies for demo code

The Project Browser should show a Module nested in a Project. The source directory structure and Java files will be added in the next section.

Screenshot of IntelliJ
Project Structure

Parent pom.xml

The following is the pom.xml for the parent.

	
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.bekwam</groupId>
    <artifactId>wf15-websocket-demo</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>wf15-websocket-demo-web</module>
    </modules>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

</project>
	

WAR Module pom.xml

The following is the pom.xml for the child module.


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>wf15-websocket-demo</artifactId>
        <groupId>org.bekwam</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>wf15-websocket-demo-web</artifactId>

    <packaging>war</packaging>

    <dependencies>
        <dependency>
            <groupId>org.jboss.spec.javax.websocket</groupId>
            <artifactId>jboss-websocket-api_1.0_spec</artifactId>
            <version>1.0.0.Final</version>
        </dependency>

        <dependency>
            <groupId>javax.inject</groupId>
            <artifactId>javax.inject</artifactId>
            <version>1</version>
        </dependency>

        <!-- not required, but nice to have for data serialization -->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.5</version>
        </dependency>

        <!-- not required, used in the demo code -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.6</version>
        </dependency>

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>fluent-hc</artifactId>
            <version>4.5.6</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.2.2</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Code

The code presented in this article consists of a web page, a server-side component running in WildFly, and some supporting classes for the data transport.

imageInfo.html
A static webpage that initiates a connection to WildFly and pipes results back to the user
ImageInfoEndpoint
A WildFly component that listens for an incoming message and responds with a start message, several file size listings, and an end message
MessageEnvelope
A data transport object
MessageType
An enumeration of the different messages: START, UPDATE, END
MessageEnvelopeEncoder
A WebSocket API subclass used to encode MessageEnvelope into a String by the framework
MessageEnvelopeSerializer
A Gson class used by the WebSocket Encoder to form a JsonElement

The following diagram shows the internal structure of the demo presented in the earlier video.

A UML Sequence Diagram
Interaction Between imageInfo.html and ImageInfoEndpoint

The browser initiates the action. A WebSocket connection is made by imageInfo.html and an initial text message is sent to the endpoint. The parameters might seem strange, "Some text" and "dummy". The WebSocket API support three different method types: text, binary, and Pong. Pong is a special WebSocket API message to enable a heartbeat for connection status. There is no "no-parameter" method so something must be passed to initiate the fetch.

The server responds with a START message. From this point on, the server will push updates to the browser. That's just a behavior for this particular demo. The client can send messages back to the server at any time. A cancel message terminating the file loop iteration comes to mind.

Next, the server-side code iterates through a list of files. As each file is retrieved, a WebSocket API Encoder is called to translate the Java object MessageEnvelope into a String. The Encoder itself delegates to some Gson code which implements Serializer.

Finally, the server responds with an END message. When received by the browser, the WebSocket is closed.

imageInfo.html

The following is the listing for imageInfo.html. You will need to modify the WebSocket constructor argument if your artifactId is different that what I'm using. This is a file that resides in the src/main/webapp folder of the project.

This HTML page is based on an example I found at tutorialspoint.

	
<!DOCTYPE html>

<html>
<head>
    <title>Bekwam Image Info WebSocket Demo</title>
    <script>
        function fetchImages() {

            var myNode = document.getElementById("data");
            while (myNode.firstChild) {
                myNode.removeChild(myNode.firstChild);
            }

            if ("WebSocket" in window) {

                document.getElementById("status").innerText = "Status: Started";

                var ws = new WebSocket("ws://localhost:8080/wf15-websocket-demo-web-1.0-SNAPSHOT/imageInfo");
                ws.onopen = function() {
                    ws.send("Some value");
                };

                ws.onmessage = function (evt) {

                    var received_msg = evt.data;

                    var obj = JSON.parse(received_msg);

                    if( obj.type == "START" ) {
                        document.getElementById("status").innerText = "Status: Running";
                    } else if( obj.type == "UPDATE") {
                        var newElem = document.createElement("li");
                        var newText = document.createTextNode(obj.payload.url + " (" + obj.payload.fileSize + " bytes)");
                        newElem.appendChild(newText);
                        document.getElementById("data").appendChild( newElem );

                    } else if( obj.type == "END" ) {
                        document.getElementById("status").innerText = "Status: Finished";
                        ws.close();
                    } else {
                        alert("Unrecognized type in json=" + received_msg);
                    }
                };

                ws.onerror = function(evt) {
                    document.getElementById("status").innerText = "Status: Error";
                    ws.close();
                }

            } else {
                alert("WebSocket is NOT supported by your Browser!");
                document.getElementById("status").innerText = "Status: Error";
            }
        }
    </script>
</head>
<body>
<h1>Bekwam Image Info WebSocket Demo</h1>
<button type="button" onclick="fetchImages()">Run Fetch</button>
<p id="status">Not Started</p>
<ul id="data"></ul>
</body>
</html>	
	

ImageInfoEndpoint.java

The next code listing is for the server-side component, ImageInfoEndpoint.

	
package websocket;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.HttpResponse;
import org.apache.http.client.fluent.Request;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;

@ServerEndpoint(value = "/imageInfo",encoders = { MessageEnvelopeEncoder.class })
public class ImageInfoEndpoint {

    private Logger logger = Logger.getLogger("ImageInfoEndpoint");

    private String[] files = {
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_archdemo_mvc_uml.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_archdemo_mvvm_screenshot.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_archdemo_mvvm_uml.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_cancelapp_activity.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_cancelapp_screenshot_cancel.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_cancelapp_screenshot_fetched.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_cancelapp_screenshot_fetching.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_afterspacing.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_final_resize.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_final_sb.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_nostyling.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_posthbox.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_resize_2.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_resize_posthbox.png",
            "https://courses.bekwam.net/public_tutorials/images/bkcourse_dlogstyling_resized_markedup.png"
    };

    @OnOpen
    public void wsOpen() {
        logger.info("opening connection" );
    }

    @OnClose
    public void wsClose() {
        logger.info( "closing connection" );
    }

    @OnMessage
    public void fetchImages(Session session, String dummy) throws Exception {

        //
        // First step of protocol: Respond to a text message (can be send from client immediately
        // after opening)
        //
        logger.info("called fetchImages()");

        //
        // Second step of protocol: Send a START message
        //
        session.getBasicRemote().sendObject( new MessageEnvelope(MessageType.START, "") );

        //
        // Third step of protocol: Send zero or more UPDATE messages
        //
        doFetchImages()
                .map( p -> new MessageEnvelope(MessageType.UPDATE, p) )
                .forEach( m -> {
                    try {
                        session.getBasicRemote().sendObject( m );
                    } catch (Exception exc) {
                        logger.log(Level.SEVERE, "error encoding or sending json=" + m, exc );
                    }
                } );

        //
        // Fourth step of protocol: Send an END message (closes WS on client side)
        //
        session.getBasicRemote().sendObject( new MessageEnvelope(MessageType.END, "") );
    }

    private Stream<Pair<String, Long>> doFetchImages() {
        return Arrays.asList( files )
                .stream()
                .map(
                    fn -> {
                        if( logger.isLoggable(Level.INFO) ) {
                            logger.info("[RETRIEVE] retrieving fn=" + fn);
                        }
                        return new ImmutablePair<gt;(fn, getFileSize(fn));
                    }
                );
    }

    private long getFileSize(String fn) {

        try {

            HttpResponse r = Request.Get(fn)
                    .connectTimeout(1000)
                    .socketTimeout(1000)
                    .execute()
                    .returnResponse();

            if( r.getStatusLine().getStatusCode() == 200 ) {

                return r.getEntity().getContentLength();

            } else {
                logger.log(Level.SEVERE, "error retrieving fn=" + fn + "; status=" + r.getStatusLine().getStatusCode());
            }
        } catch(Exception exc) {
            logger.log(Level.SEVERE, "error retrieving fn=" + fn, exc);
        }
        return -1L;
    }
}		
	

The previous two code listings implement the bulk of the functionality. The next classes are used to streamline the data transport. These are showing a preferred coding style which lets you work with Java objects instead of raw Strings.

MessageEnvelope.java

MessageEnvelope communicates a MessageType and a payload from the client to the server.


package websocket;

import java.io.Serializable;

public class MessageEnvelope implements Serializable {

    private final MessageType messageType;
    private final Object payload;

    public MessageEnvelope(MessageType messageType, Object payload) {
        this.messageType = messageType;
        this.payload = payload;
    }

    public MessageType getMessageType() {
        return messageType;
    }

    public Object getPayload() {
        return payload;
    }
}

MessageType.java

MessageType is an enumeration that defines the type of message returned by the server to the client. Notice that the values appear in the OnMessage logic in imageInfo.html.

package websocket;

public enum MessageType {

    START,

    UPDATE,

    END
}

MessageEnvelopeEncoder.java

An Encoder is a WebSocket API class that can be associated with a ServerEndpoint. This allows me to call sendObject() from the Endpoint so that I can handle the conversion to a String in a consistent manner. Alternatively, I could call sendText() and form the encoded String by hand.


package websocket;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

public class MessageEnvelopeEncoder implements Encoder.Text<MessageEnvelope> {

    private Gson gson;

    @Override
    public void init(EndpointConfig ec) {
        GsonBuilder builder = new GsonBuilder();
        builder.registerTypeAdapter(MessageEnvelope.class, new MessageEnvelopeSerializer());
        gson = builder.create();
    }

    @Override
    public void destroy() { }

    @Override
    public String encode(MessageEnvelope messageEnvelope) throws EncodeException {
        return gson.toJson(messageEnvelope);
    }
}

MessageEnvelopeSerializer.java

This is neither required for WebSockets nor WildFly, but I'm using Gson to assist with the JSON formatting. I'm returning a Commons Lang 3 Pair object from my "business logic" (the file size retrieving code).


package websocket;

import com.google.gson.*;
import org.apache.commons.lang3.tuple.Pair;

import java.lang.reflect.Type;

public class MessageEnvelopeSerializer implements JsonSerializer<MessageEnvelope> {

    @Override
    public JsonElement serialize(MessageEnvelope messageEnvelope, Type type, JsonSerializationContext jsonSerializationContext) {

        JsonObject obj = new JsonObject();

        obj.add( "type", new JsonPrimitive(messageEnvelope.getMessageType().toString()));

        if( messageEnvelope.getMessageType().equals(MessageType.UPDATE) ) {
            Pair<String, Long> data = (Pair<String, Long>) messageEnvelope.getPayload();
            JsonObject inner_obj = new JsonObject();
            inner_obj.add( "url", new JsonPrimitive(data.getKey()) );
            inner_obj.add( "fileSize", new JsonPrimitive(data.getValue()) );
            obj.add( "payload", inner_obj );
        }

        return obj;
    }
}


Deployment and Testing

This article uses an IntelliJ setup described in this post. You'll deploy your WAR file as an exploded configuration. Additionally, I use the "Update classes and resource" setting for the Update and Frame actions since I'm working with a static web resource in this example. I don't need to redeploy the exploded WAR to affect the class loader if I make a web page-only modification.

The URL for the deployed application -- using my Maven coordinates -- is http://localhost:8080/wf15-websocket-demo-web-1.0-SNAPSHOT/imageInfo.html. If your artifactId is different, modify accordingly.

This article solves a need I frequently have with JavaEE: providing status for long-running processes. Unlike a Request-Response model, this program is able to let the browser and the server operate independently. Yet, the browser is made aware of the back-end processing. There is much more that you can do with WebSockets to produce a robust and iterating interface without inefficient workarounds like long polling.


Headshot of Carl Walker

By Carl Walker

President and Principal Consultant of Bekwam, Inc