In my last article on MicroProfile, Writing a simple MicroProfile application (3): Using Java EE concurrency, I talked about using Concurrency Utilities for Java EE (for the other articles in this series, see the Related articles section). In this article I’ll cover adding WebSockets and using CDI Events to integrate a WebSocket and the CDI beans so that the server can notify clients about changes.

The sample application solves a real problem that the Liberty development team had. The globally-distributed Liberty development team has a lot of online meetings using IBM Connections Cloud Meetings. IBM Connections Cloud provides meeting rooms to individual employees, which is a problem for team meetings if the person who initially set up the meeting room can’t make it (e.g. they were called into another meeting, are on vacation, or sick). The sample application provides a single URL for a meeting, which can then be ‘started’ by one person and everyone else gets redirected.

If you have eagle eyes, you might have noticed with the application that the redirect only works when a meeting is started, not when it is already running. In order to make the join action work, the browser needs to find out when the meeting has changed. This can be done by polling the server but that can be expensive. Instead, a WebSocket can be used to allow the server to notify the client. This reduces the number of requests to the server and provides prompt updates to the client.

The meeting application is available in GitHub. Each branch in the repository maps to an article. The starting point for the app for this article is branch part3. By the end of this article, your code should match the code in branch part4.

This article assumes you are using Eclipse and WebSphere Developer Tools (WDT). To install WDT, download and start Eclipse, then drag and drop
Drag to your running Eclipse workspace to install WebSphere Developer Tools
on to the toolbar to start the WDT installer.

Getting the source into Eclipse from GitHub

The files for this application are available in GitHub. To follow this article, you want to clone the repository, check out the part3 branch of the repository, and import the project into Eclipse. You can do this from the command line or directly from Eclipse.

From the command line

If you prefer to clone the Git repository from the command line:

  1. Run the following commands:
    git clone https://github.com/WASdev/sample.microprofile.meetingapp.git
    cd sample.microprofile.meetingapp
    git checkout part3
  2. In Eclipse, import the project as an existing project, then switch to the Java EE perspective.

From Eclipse

If you prefer to clone the Git repository from Eclipse:

  1. In Eclipse, switch to the Git perspective.
  2. Click Clone a Git repository from the Git Repositories view.
  3. Enter URI https://github.com/WASdev/sample.microprofile.meetingapp.git
  4. Click Next, then click Next again accepting the defaults.
  5. From the Initial branch drop-down list, click part3.
  6. Select Import all existing Eclipse projects after clone finishes, then click Finish.
  7. Switch to the Java EE perspective.

The meetings project is automatically created in the Project Explorer view.

Installing MongoDB

If you completed the previous articles and installed MongoDB, make sure MongoDB is running. If you are starting fresh, make sure you install MongoDB. Depending on what platform you are on the installation instructions may be different. For this exercise you should get the community version of MongoDB from the mongoDB download-center.

Once installed you can run the MongoDB database daemon using:

mongod -dbpath <path to database>

The database needs to be running for the application to work. If it isn’t running there will be a lot of noise in the server logs.

Updating the application to compile against the WebSocket API

To start writing code, the Maven pom.xml needs to be updated to indicate the dependency on the WebSocket API for Java EE:

  1. Open the pom.xml in Eclipse.
  2. In the editor, select the Dependencies tab.
  3. On the Dependencies tab there are two sections, one for Dependencies and the other for Dependency Management. Just to the right of the Dependencies box there is an Add button. Click the Add button.
  4. Enter a groupdId of javax.websocket`
  5. Enter a artifactId of javax.websocket-api
  6. Enter a version of 1.1
  7. From the scope drop-down list, select provided. This will allow the application to compile but will prevent the Maven WAR packager putting the API in the WAR file. Later, the build will be configured to make it available to the server.
  8. Click OK.
  9. Save the pom.xml.

Create a CDI qualifier

A CDI qualifier is simply an annotation annotated with @Qualifier. This can then be used with other CDI annotations to influence behaviour. In the case of CDI events, it links the event producer to the event consumer.

  1. Right-click the meetings* project, then click **New > Annotation….
  2. Enter a name of MeetingEvent.
  3. Click Finish.
  4. The annotations should be added to the type name. There are three key annotations. The first is Qualifier, which indicates that the annotation is a CDI qualifier:
    @Qualifier
    public @interface MeetingEvent {
    }
    
  5. This introduces a new type Qualifier in the package javax.inject:
        import javax.inject.Qualifier;
    
  6. The second annotation, Retention, indicates that the annotation should be available at runtime. This allows the CDI runtime to process them:
    @Qualifier
    @Retention(RetentionPolicy.RUNTIME)
    
  7. This introduces two new types: Retention and RetentionPolicy. These are in the package java.lang.annotation:
        import java.lang.annotation.Retention;
        import java.lang.annotation.RetentionPolicy;
    
  8. The last annotation, Target, indicates where the annotation can be applied. For the CDI qualifier it needs to be applied to a field and a parameter:
    @Qualifier
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.FIELD, ElementType.PARAMETER})
    
  9. This introduces two new types: ‘Target’ and ‘ElementType’. These are in the package ‘java.lang.annotation`:
        import java.lang.annotation.ElementType;
        import java.lang.annotation.Target;
    
  10. Save the file. The annotations should look like this:
        @Qualifier
        @Retention(RetentionPolicy.RUNTIME)
        @Target({ElementType.FIELD, ElementType.PARAMETER})
        public @interface MeetingEvent {
    
        }
    

Creating a CDI event object

With CDI events you can pass any object you want between the producer and consumer but, for this application, we will create an object. There are two things to be passed: one is the event to identify the meeting, the other is the URL of the meeting.

To create the event object:

  1. Right-click the meetings* project, then click **New > Class….
  2. In the Package field, type net.wasdev.samples.microProfile.meetings
  3. Enter a name of MeetingStartEvent
  4. Click Finish.
  5. Add a String field to store the ID.
        private String id;
    
  6. Add a String field to store the URL.
        private String url;
    
  7. Next add an constructor that takes the values of the id and url and stores them in the fields:
        public MeetingStartEvent(String id, String url) {
            this.id = id;
            this.url = url;
        }
    
  8. Finally, create the simple getters to return the fields:
        public String getId() {
            return id;
        }
    
        public String getUrl() {
            return url;
        }
    
  9. Save the file.

Sending an event when a meeting starts

The next part is to get the MeetingManager to emit an event when a meeting is started:

  1. Open the MeetingManager class.
  2. Add a new field to inject the CDI Event class. The Event class is parameterized with the event object to be set. This field should also be annotated using the CDI qualifier, MeetingEvent, that we created earlier:
    @Resource
    private ManagedScheduledExecutorService executor;
    @Inject
    @MeetingEvent
    private Event<MeetingStartEvent> events;
    
  3. This introduces a new type Event in the package javax.enterprise.event. It also introduces Inject from the package javax.inject:
        import javax.enterprise.event.Event;
        import javax.inject.Inject;
    
  4. Find the startMeeting method. At the end of the method construct a new instance of the MeetingStartEvent passing in the meeting ID and URL:
        MeetingStartEvent eventObject = new MeetingStartEvent(id, url);
    
  5. Then call the Event fire object passing in the event object:
        events.fire(eventObject);
    
  6. Save the file.

At this stage the application could be run, the event would be emitted, but nothing would happen since there is nothing to receive the event.

Creating the WebSocket

The WebSocket will handle the connection between the browser and server, and receive the meeting start event. The browser will send the meeting ID and the WebSocket will notify it when that meeting gets started.

To create the WebSocket:

  1. Right-click the meetings* project, then click **New > Class….
  2. In the Package field, type net.wasdev.samples.microProfile.meetings
  3. Enter a name of MeetingNotifier
  4. Click Finish.
  5. According to the spec, WebSocket components are not CDI beans. To ensure that CDI can see the bean it needs to be annotated. In this case we add the Dependent annotation to the type:
    @Dependent
    public class MeetingNotifier {
    }
    
  6. This introduces a new class Dependent which is in package javax.enterprise.context:
        import javax.enterprise.context.Dependent;
    
  7. To make the class into a WebSocket it needs to be annotated with the ServerEndpoint annotation. The annotation takes a URL path that will be used to invoke it. The URL path must start with a forward slash:
    @Dependent
    @ServerEndpoint("/notifier")
    
  8. This introduces a new class ServerEndpoint which is in package javax.websocket.server. When importing take care to import the right one since there are multiple ServerEndpoint classes:
        import javax.websocket.server.ServerEndpoint;
    
  9. Save the file. The type definition should now look like this:
        @Dependent
        @ServerEndpoint("/notifier")
        public class MeetingNotifier {
    
  10. The WebSocket will need to interact with the MeetingManager so it needs to be injected into a field:
    public class MeetingNotifier {
    @Inject
    private MeetingManager manager;
    
  11. This introduces a new type Inject from the javax.inject package:
        import javax.inject.Inject;
    
  12. The WebSocket container manages an instance of the class for each WebSocket connection. When the CDI event system distributes events, however, it creates a new instance so the WebSocket Session objects need to be stored for later. A Map is used to store the Session objects associated with a meeting. Because there will be multiple Session objects, a Collection of Session objects is appropriate. Of course, because this will need to cope with multiple threads, we use concurrent versions of the sessions (added on the next line of the MeetingNotifier class):
            private static ConcurrentMap<String, Queue<Session>> listeners = new ConcurrentHashMap<>();
    
  13. This introduces four new classes. The ConcurrentMap, and ConcurrentHashMap classes are in the java.util.concurrent package. The Queue class is in the java.util package and Session is in the javax.websocket package:
        import java.util.Queue;
        import java.util.concurrent.ConcurrentHashMap;
        import java.util.concurrent.ConcurrentMap;
        import javax.websocket.Session;
    
  14. There are multiple methods on a ServerEndpoint but for this the key one is the onMessage method:
    1. An onMessage method is indicated using the OnMessage annotation. There are multiple method signatures that can be used but in this case the method will take a String that will contain the ID of the meeting and the WebSocket session:
              @OnMessage
              public void onMessage(String id, Session s) throws IOException {
                  // code will go in here
              }
      
    2. This introduces a new type the OnMessage annotation in the javax.websocket package, as well as IOException from java.io:
          import javax.websocket.OnMessage;
          import java.io.IOException;
      
    3. The first thing to do is to check that the ID really is for a meeting. If there is no meeting with the ID’s name then the method should exit:
              JsonObject m = manager.get(id);
              if (m == null) {
                  s.close();
                  return;
              }
      
    4. This introduces a new class JsonObject which is in the package javax.json:
          import javax.json.JsonObject;
      
    5. The next thing to do is to get the meeting URL for the meeting:
              JsonString url = m.getJsonString("meetingURL");
      
    6. This introduces a new class JsonString which is in the package javax.json:
          import javax.json.JsonString;
      
    7. If the meeting URL is there, the information should be sent to the WebSocket client directly then the method should exit. To send information to the client the session is used: get a remote object, then send some text. The JsonString toString method wraps the URL in quotes so the getString method must be used:
              if (url != null) {
                  s.getBasicRemote().sendText(url.getString());
                  s.close();
                  return;
              }
      
    8. Now the session needs to be stored away so that when the meeting is started the client is notified. This is stored in the map, so the first thing to do is to get the collection of sessions:
              Queue<Session> sessions = listeners.get(id);
              if (sessions == null) {
                  // code will go here
              }
      
    9. Inside the null check we need to create a new collection. This should be a concurrent collection, so use an ArrayBlockingQueue:
                  sessions = new ArrayBlockingQueue<>(1000);
      
    10. This introduces a new class, the ArrayBlockingQueue in the package java.util.concurrent.
          import java.util.concurrent.ArrayBlockingQueue;
      
    11. Now it needs to be put in the map. Of course there could be two clients coming through the method so, rather than doing a put which will overwrite, use the putIfAbsent method:
                  Queue<Session> actual = listeners.putIfAbsent(id, sessions);
      
    12. If the put succeeded, the actual will be null. If another thread won and put their copy of sessions in the map it’ll have the collection that should be used, so a swap is needed:
                  if (actual != null) {
                      sessions = actual;
                  }
      
    13. The last thing to do in the method (and outside the if block with the null check for sessions) is to add the Session to the Collection of Session objects:
              sessions.add(s);
      
    14. The code added as a result of steps h-m should look like this:
      		Queue<Session> sessions = listeners.get(id);
      		if (sessions == null) {
      			sessions = new ArrayBlockingQueue<>(1000);
      			Queue<Session> actual = listeners.putIfAbsent(id, sessions);
      			if (actual != null) {
      				sessions = actual;
      			}
      		}
      		sessions.add(s);
      
  15. Now the sessions are stored, the event method needs to be defined:
    1. The name of the method isn’t important but it has to take the event. The parameter that takes the event needs to be annotated with the Observes annotation (which indicates that this is an event notification method) and the MeetingEvent annotation so it knows which kind of event to call with it:
              public void startMeeting(@Observes @MeetingEvent MeetingStartEvent event) {
                  // add the notification code here
              }
      
    2. This introduces the new type Observes in the package javax.enterprise.event:
          import javax.enterprise.event.Observes;
      
    3. If this method is called then the meeting has started. The sessions no longer need to be cached away because the meeting has started and so they can be removed from the map:
                  Queue<Session> sessions = listeners.remove(event.getId());
      
    4. Of course it is possible there are no sessions stored, at which point it’ll be null so the next part should only happen if the sessions are non-null:
                  if (sessions != null) {
                      // add the next bit of code  here
                  }
      
    5. The logic should be done for each session, so a simple enhanced for loop will do:
                      for (Session s : sessions) {
                          // add the next bit of code here
                      }
      
    6. The session needs to be open to send data to the client, so check that first:
                          if (s.isOpen()) {
                              // add the next bit of code here
                          }
      
    7. Finally the URL should be sent to the client. This could cause an IOException which can’t be thrown by this method, so needs to be caught:
                              try {
                                  s.getBasicRemote().sendText(event.getUrl());
                                  s.close();
                              } catch (IOException e) {
                                  e.printStackTrace();
                              }
      
  16. Save the file.

You’ve now coded the application. Test the app by opening two browser windows, one to join the meeting and the other to start the meeting. Watch as both browser windows redirect at once.

Configuring Liberty to run WebSockets

  1. Open the server.xml from src > main > liberty > config > server.xml.
  2. Find the <feature manager> element. It should look like this:
        <featureManager>
            <feature>mongodb-2.0</feature>
            <feature>concurrent-1.0</feature>
        </featureManager>
    
  3. Before the closing </featureManager> element add a feature element with the feature websocket-1.1 as the body.
            <feature>websocket-1.1</feature>
    
  4. Save the file.

Running the application

There are two ways to get the application running from within WDT:

  • The first is to use Maven to build and run the project:
    1. Run the Maven install goal to build and test the project: Right-click pom.xml in the meetings project, click Run As… > Maven Build…, then in the Goals field type install and click Run. The first time you run this goal, it might take a few minutes to download the Liberty dependencies.
    2. Run a Maven build for the liberty:start-server goal: Right-click pom.xml, click Run As… > Maven Build…, then in the Goals field, type liberty:start-server and click Run. This starts the server in the background.
    3. Open the application, which is available at http://localhost:9080/meetings/.
    4. To stop the server again, run the liberty:stop-server build goal.
  • The second way is to right-click the meetings project and select Run As… > Run on Server but there are a few things to note if you do this. WDT doesn’t automatically add the MicroProfile features as you would expect so you need to manually add those. Also, any changes to the configuration in src/main/liberty/config won’t be picked up unless you add an include.

Find out more about MicroProfile and WebSphere Liberty.

Join The Discussion

Your email address will not be published. Required fields are marked *