001package jmri.server.json.message;
002
003import com.fasterxml.jackson.databind.JsonNode;
004import com.fasterxml.jackson.databind.ObjectMapper;
005import java.io.IOException;
006import java.util.ArrayList;
007import java.util.HashMap;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Map.Entry;
011import java.util.Set;
012
013import javax.annotation.CheckForNull;
014import javax.annotation.Nonnull;
015import jmri.InstanceManagerAutoDefault;
016import jmri.server.json.JsonConnection;
017
018/**
019 * Manager for JSON streaming clients that are subscribing to messages triggered
020 * by out-of-channel events.
021 *
022 * @author Randall Wood Copyright 2017
023 */
024public class JsonMessageClientManager implements InstanceManagerAutoDefault {
025
026    final ObjectMapper mapper = new ObjectMapper();
027    HashMap<String, JsonConnection> clients = new HashMap<>();
028
029    /**
030     * Subscribe to the message service.
031     *
032     * @param client     the client identifier to use for the subscription
033     * @param connection the connection associated with client
034     * @throws IllegalArgumentException if client is already in use for a
035     *                                      different connection
036     */
037    public void subscribe(@Nonnull String client, @Nonnull JsonConnection connection) {
038        if (clients.containsKey(client) && !connection.equals(clients.get(client))) {
039            throw new IllegalArgumentException("client in use with different connection");
040        }
041        clients.putIfAbsent(client, connection);
042    }
043
044    /**
045     * Cancel the subscription for a single client.
046     *
047     * @param client the client canceling the subscription
048     */
049    public void unsubscribe(@CheckForNull String client) {
050        clients.remove(client);
051    }
052
053    /**
054     * Cancel the subscription for all clients on a given connection.
055     *
056     * @param connection the connection canceling the subscription
057     */
058    public void unsubscribe(@CheckForNull JsonConnection connection) {
059        List<String> keys = new ArrayList<>();
060        clients.entrySet().stream()
061                .filter(entry -> entry.getValue().equals(connection))
062                .forEachOrdered(entry -> keys.add(entry.getKey()));
063        keys.forEach(this::unsubscribe);
064    }
065
066    /**
067     * Send a message to a client or clients. The determination of a single
068     * client or all clients is made using {@link JsonMessage#getClient()}.
069     *
070     * @param message the message to send
071     */
072    public void send(@Nonnull JsonMessage message) {
073        JsonNode node = getJsonMessage(message);
074        if (message.getClient() == null) {
075            new HashMap<>(clients).entrySet().forEach(client -> {
076                try {
077                    client.getValue().sendMessage(node, 0);
078                } catch (IOException ex) {
079                    unsubscribe(client.getKey());
080                }
081            });
082        } else {
083            JsonConnection connection = clients.get(message.getClient());
084            if (connection != null) {
085                try {
086                    connection.sendMessage(node, 0);
087                } catch (IOException ex) {
088                    unsubscribe(message.getClient());
089                }
090            }
091        }
092    }
093
094    private JsonNode getJsonMessage(JsonMessage message) {
095        return message.toJSON(mapper);
096    }
097
098    /**
099     * Get the first client name associated with a connection.
100     *
101     * @param connection the connection to get a client for
102     * @return the client or null if the connection is not subscribed
103     */
104    @CheckForNull
105    public synchronized String getClient(@Nonnull JsonConnection connection) {
106        for (Entry<String, JsonConnection> entry : clients.entrySet()) {
107            if (entry.getValue().equals(connection)) {
108                return entry.getKey();
109            }
110        }
111        return null;
112    }
113
114    /**
115     * Get all client names associated with a connection.
116     * 
117     * @param connection the connection to get clients for
118     * @return a set of clients or an empty set if the connection is not
119     *         subscribed
120     */
121    public synchronized Set<String> getClients(@Nonnull JsonConnection connection) {
122        Set<String> set = new HashSet<>();
123        for (Entry<String, JsonConnection> entry : clients.entrySet()) {
124            if (entry.getValue().equals(connection)) {
125                set.add(entry.getKey());
126            }
127        }
128        return set;
129    }
130}