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}