Skip to content

IBM/microprofile-meeting-websockets

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Part 4: MicroProfile Meeting Application - Using WebSockets and CDI events

Overview

In this lab we will cover adding WebSockets and using CDI Events to integrate a WebSocket and the CDI beans so that the server can notify clients about changes

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.

Adapted from the blog post: Writing a simple MicroProfile application: Using WebSockets and CDI events

Prerequisites

  • Completed Part 3: MicroProfile Meeting Application - Using Java EE Concurrency
  • Eclipse IDE for Web Developers: Run the installer and select Eclipse IDE for Java EE developers. Note: these steps were tested on the 2018-09 version of Eclipse running on Linux and Liberty Developer Tools 18.0.0.3. Note: If you encounter an error message like Could not initialize class org.codehaus.plexus.archiver.jar.JarArchiver please see the Troubleshooting section.
  • IBM Liberty Developer Tools (WDT)
    1. Start Eclipse
    2. Launch the Eclipse Marketplace: Help -> Eclipse Marketplace
    3. Search for IBM Liberty Developer Tools, and click Install with the defaults configuration selected
  • Git
  • Install the IBM Cloud CLI

Steps

Step 1. Check out the source code

From the command line

Run the following commands:

$    git clone https://github.com/IBM/microprofile-meeting-websockets.git

In Eclipse, import the project as an existing project.

  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/IBM/microprofile-meeting-websockets.git
  4. Click Next, then click Next again accepting the defaults.
  5. From the Initial branch drop-down list, click master.
  6. Select Import all existing Eclipse projects after clone finishes, then click Finish.
  7. Switch to the Java EE perspective.
  8. The meetings project is automatically created in the Project Explorer view.

Step 2. Installing MongoDB

If you completed the previous labs 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.

  1. 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 is not running there will be a lot of noise in the server logs.

Step 3. 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.

Step 4. 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 {
}
  1. This introduces a new type Qualifier in the package javax.inject:
import javax.inject.Qualifier;
  1. 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)
  1. 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;
  1. 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})
  1. 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;
  1. Save the file. The annotations should look like this:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface MeetingEvent {
 
}

Step 5. 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;
  1. Add a String field to store the URL.
private String url;
  1. 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;
}
  1. Finally, create the simple getters to return the fields:
public String getId() {
    return id;
}
 
public String getUrl() {
    return url;
}
  1. Save the file.

Step 6. 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;
  1. 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;
  1. 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);
  1. Then call the Event fire object passing in the event object:
events.fire(eventObject);
  1. 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.

Step 7. 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 {
}
  1. This introduces a new class Dependent which is in package javax.enterprise.context:
import javax.enterprise.context.Dependent;
  1. 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")
  1. 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;
  1. Save the file. The type definition should now look like this:
@Dependent
@ServerEndpoint("/notifier")
public class MeetingNotifier {
  1. 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;
  1. This introduces a new type Inject from the javax.inject package:
import javax.inject.Inject;
  1. 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<>();
  1. 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;
  1. There are multiple methods on a ServerEndpoint but for this the key one is the onMessage method:
  • 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
}
  • 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;
  • 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;
}
  • This introduces a new class JsonObject which is in the package javax.json:
import javax.json.JsonObject;
  • The next thing to do is to get the meeting URL for the meeting:
JsonString url = m.getJsonString("meetingURL");
  • This introduces a new class JsonString which is in the package javax.json:
import javax.json.JsonString;
  • 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;
}
  • 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
}
  • 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);
  • This introduces a new class, the ArrayBlockingQueue in the package java.util.concurrent.
import java.util.concurrent.ArrayBlockingQueue;
  • 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);
  • 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;
}
  • 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);
  • 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);
  1. Now the sessions are stored, the event method needs to be defined:
  • 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
}
  • This introduces the new type Observes in the package javax.enterprise.event:
import javax.enterprise.event.Observes;
  • 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());
  • 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
}
  • 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
}
  • 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
}
  • 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();
}
  1. 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.

Step 8. 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>
  1. Before the closing </featureManager> element add a feature element with the feature websocket-1.1 as the body.
<feature>websocket-1.1</feature>
  1. 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.

GitHub

Check out the final code for this project at: https://github.com/WASdev/sample.microprofile.meetingapp

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published