How it works...

We started with our SSE engine, the ServerEvent class, and a JAX-RS (short for JAVA API for RESTful Web Services) endpoint—these hold all of the methods that we need for this recipe.

Let's look at the first one:

    @Path("start")
@POST
public Response start(@Context Sse sse) {

final UserEvent process = new UserEvent(sse);

POOL.put(process.getId(), process);
executor.submit(process);

final URI uri = UriBuilder.fromResource(ServerSentService.class).
path("register/{id}").build(process.getId());
return Response.created(uri).build();
}

Following are the main points:

  1. This method will create and prepare an event to be sent by the server to the clients.
  2. Then, the newly-created event is put in a HashMap called POOL.
  3. Then, our event is attached to URI that represents another method in this same class (details are provided next).

Pay attention to this parameter:

@Context Sse sse

This brings the server-side events feature from the server context and lets you use it as you need and, of course, it is injected by Jakarta CDI (yes, CDI, short for Contexts and Dependency Injection, is everywhere!).

Now, we see our register() method:

    @Path("register/{id}")
@Produces(MediaType.SERVER_SENT_EVENTS)
@GET
public void register(@PathParam("id") Long id,
@Context SseEventSink sseEventSink) {
final UserEvent event = POOL.get(id);

if (event != null) {
event.getSseBroadcaster().register(sseEventSink);
} else {
throw new NotFoundException();
}
}

This is the very method that sends the events to your clients—check the @Produces annotation; it uses the new media type, SERVER_SENT_EVENTS.

The engine works, thanks to this small piece of code:

@Context SseEventSink sseEventSink

...

event.getSseBroadcaster().register(sseEventSink);

SseEventSink is a queue of events managed by the Jakarta EE server, and it is served to you by injection from the context.

Then, you get the process broadcaster and register it to this sink, which means that everything that this process broadcasts will be sent by the server from SseEventSink.

Now, we check our event setup:

    static class UserEvent implements Runnable {

...

UserEvent(Sse sse) {
this.sse = sse;
this.sseBroadcaster = sse.newBroadcaster();
id = System.currentTimeMillis();
}

...

@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(5);
sseBroadcaster.broadcast(sse.newEventBuilder().
name("register").data(String.class, "Text from event "
+ id).build());
sseBroadcaster.close();
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
}
}

Now, pay attention to the following line:

this.sseBroadcaster = sse.newBroadcaster();

You'll remember that we've just used this broadcaster in the last class. Here, we see that this broadcaster is brought by the Sse object injected by the server.

This event implements the Runnable interface so we can use it with the executor (as explained before), so once it runs, you can broadcast to your clients:

sseBroadcaster.broadcast(sse.newEventBuilder().name("register").
data(String.class, "Text from event " + id).build());

This is the exact message that is sent to the client. This could be whatever message you need.

For this recipe, we used another class to interact with Sse. Let's highlight the most important parts:

        WebTarget target = client.target(URI.create
("http://localhost:8080/ch03-sse/"));
Response response = target.path("webresources/serverSentService
/start")
.request()
.post(Entity.json(""), Response.class);

This is a simple piece of code that you can use to call any JAX-RS endpoint.

And finally, the most important part of this mock client is as follows:

        for (int i = 0; i < countClient; i++) {
final int id = i;
sources[id] = SseEventSource.target(sseTarget).build();
sources[id].register((event) -> {
final String message = event.readData(String.class);

if (message.contains("Text")) {
messageMap.put(id, message);
}
});
sources[i].open();
}

Each message that is broadcast is read here:

final String message = messageMap.get(i);

It could be any client you want—another service, a web page, a mobile client, or anything.

Then, we check our UI:

<h:inputText id="countClient" value="#{sseBean.countClient}" />
...
<h:commandButton type="submit" action="#{sseBean.sendEvent()}"
value="Send Events" />

We are using the countClient field to fill the countClient value in the client, so you can play around with as many threads as you want.