All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jaxrs.examples.sse.ItemStoreResource Maven / Gradle / Ivy

/*
 * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0, which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package jaxrs.examples.sse;

import java.io.IOException;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;

import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.ServiceUnavailableException;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.sse.OutboundSseEvent;
import jakarta.ws.rs.sse.Sse;
import jakarta.ws.rs.sse.SseBroadcaster;
import jakarta.ws.rs.sse.SseEventSink;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

/**
 * A resource for storing named items.
 *
 * @author Marek Potociar (marek.potociar at oracle.com)
 */
@SuppressWarnings("VoidMethodAnnotatedWithGET")
@Path("items")
@Singleton
public class ItemStoreResource {

    private static final Logger LOGGER = Logger.getLogger(ItemStoreResource.class.getName());

    private final ReentrantReadWriteLock storeLock = new ReentrantReadWriteLock();
    private final LinkedList itemStore = new LinkedList<>();

    private final Sse sse;
    private final SseBroadcaster broadcaster;

    @Inject
    public ItemStoreResource(Sse sse) {
        this.sse = sse;
        this.broadcaster = sse.newBroadcaster();

        broadcaster.onError((subscriber, e) -> LOGGER.log(Level.WARNING, "An exception has been thrown while broadcasting to an event output.", e));

        broadcaster.onClose(subscriber -> LOGGER.log(Level.INFO, "SSE event output has been closed."));
    }

    private static volatile long reconnectDelay = 0;

    /**
     * List all stored items.
     *
     * @return list of all stored items.
     */
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String listItems() {
        try {
            storeLock.readLock().lock();
            return itemStore.toString();
        } finally {
            storeLock.readLock().unlock();
        }
    }

    /**
     * Receive & process commands sent by the test client that control the internal resource state.
     * 

* Following is the list of recognized commands: *

    *
  • disconnect - disconnect all registered event streams.
  • *
  • reconnect now - enable client reconnecting.
  • *
  • reconnect <seconds> - disable client reconnecting. Reconnecting clients will receive a HTTP 503 * response with {@value jakarta.ws.rs.core.HttpHeaders#RETRY_AFTER} set to the amount of milliseconds specified.
  • *
* * @param command command to be processed. * @return message about processing result. * @throws BadRequestException in case the command is not recognized or not specified. */ @POST @Path("commands") public String processCommand(String command) { if (command == null || command.isEmpty()) { throw new BadRequestException("No command specified."); } if ("disconnect".equals(command)) { broadcaster.close(); return "Disconnected."; } else if (command.length() > "reconnect ".length() && command.startsWith("reconnect ")) { final String when = command.substring("reconnect ".length()); try { reconnectDelay = "now".equals(when) ? 0 : Long.parseLong(when); return "Reconnect strategy updated: " + when; } catch (NumberFormatException ignore) { // ignored } } throw new BadRequestException("Command not recognized: '" + command + "'"); } /** * Connect or re-connect to SSE event stream. * * @param lastEventId Value of custom SSE HTTP {@value jakarta.ws.rs.core.HttpHeaders#LAST_EVENT_ID_HEADER} * header. Defaults to {@code -1} if not set. * @param serverSink new SSE server sink stream representing the (re-)established SSE client connection. * @throws InternalServerErrorException in case replaying missed events to the reconnected output stream fails. * @throws ServiceUnavailableException in case the reconnect delay is set to a positive value. */ @GET @Path("events") @Produces(MediaType.SERVER_SENT_EVENTS) public void itemEvents( @HeaderParam(HttpHeaders.LAST_EVENT_ID_HEADER) @DefaultValue("-1") int lastEventId, @Context SseEventSink serverSink) { if (lastEventId >= 0) { LOGGER.info("Received last event id :" + lastEventId); // decide the reconnect handling strategy based on current reconnect delay value. final long delay = reconnectDelay; if (delay > 0) { LOGGER.info("Non-zero reconnect delay [" + delay + "] - responding with HTTP 503."); throw new ServiceUnavailableException(delay); } else { LOGGER.info("Zero reconnect delay - reconnecting."); try { replayMissedEvents(lastEventId, serverSink); } catch (IOException e) { // handle I/O failure. } } } broadcaster.register(serverSink); } private void replayMissedEvents(final int lastEventId, final SseEventSink eventOutput) throws IOException { try { storeLock.readLock().lock(); final int firstUnreceived = lastEventId + 1; final int missingCount = itemStore.size() - firstUnreceived; if (missingCount > 0) { LOGGER.info("Replaying events - starting with id " + firstUnreceived); final ListIterator it = itemStore.subList(firstUnreceived, itemStore.size()).listIterator(); while (it.hasNext()) { eventOutput.send(createItemEvent(it.nextIndex() + firstUnreceived, it.next())); } } else { LOGGER.info("No events to replay."); } } finally { storeLock.readLock().unlock(); } } /** * Add new item to the item store. *

* Invoking this method will fire 2 new SSE events - 1st about newly added item and 2nd about the new item store size. * * @param name item name. */ @POST public void addItem(@FormParam("name") String name) { // Ignore if the request was sent without name parameter. if (name == null) { return; } final int eventId; try { storeLock.writeLock().lock(); eventId = itemStore.size(); itemStore.add(name); // Broadcasting an un-named event with the name of the newly added item in data broadcaster.broadcast(createItemEvent(eventId, name)); // Broadcasting a named "size" event with the current size of the items collection in data broadcaster.broadcast(sse.newEventBuilder().name("size").data(Integer.class, eventId + 1).build()); } finally { storeLock.writeLock().unlock(); } } private OutboundSseEvent createItemEvent(final int eventId, final String name) { Logger.getLogger(ItemStoreResource.class.getName()) .info("Creating event id [" + eventId + "] name [" + name + "]"); return sse.newEventBuilder().id("" + eventId).data(String.class, name).build(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy