001package jmri.server.json.throttle; 002 003import static jmri.server.json.JSON.ADDRESS; 004import static jmri.server.json.JSON.F; 005import static jmri.server.json.JSON.FORWARD; 006import static jmri.server.json.JSON.IS_LONG_ADDRESS; 007import static jmri.server.json.JSON.NAME; 008import static jmri.server.json.JSON.PREFIX; 009import static jmri.server.json.JSON.STATUS; 010import static jmri.server.json.roster.JsonRoster.ROSTER_ENTRY; 011 012import com.fasterxml.jackson.databind.JsonNode; 013import com.fasterxml.jackson.databind.ObjectMapper; 014import com.fasterxml.jackson.databind.node.ObjectNode; 015import java.beans.PropertyChangeEvent; 016import java.beans.PropertyChangeListener; 017import java.io.IOException; 018import java.util.ArrayList; 019import java.util.List; 020import java.util.Locale; 021import javax.annotation.CheckForNull; 022import javax.servlet.http.HttpServletResponse; 023 024import jmri.BasicRosterEntry; 025import jmri.DccLocoAddress; 026import jmri.DccThrottle; 027import jmri.InstanceManager; 028import jmri.LocoAddress; 029import jmri.Throttle; 030import jmri.ThrottleListener; 031import jmri.ThrottleManager; 032import jmri.jmrit.roster.Roster; 033import jmri.SystemConnectionMemo; 034import jmri.jmrix.SystemConnectionMemoManager; 035import jmri.server.json.JSON; 036import jmri.server.json.JsonException; 037import org.slf4j.Logger; 038import org.slf4j.LoggerFactory; 039 040public class JsonThrottle implements ThrottleListener, PropertyChangeListener { 041 042 /** 043 * Token for type for throttle status messages. 044 * <p> 045 * {@value #THROTTLE} 046 */ 047 public static final String THROTTLE = "throttle"; // NOI18N 048 /** 049 * {@value #RELEASE} 050 */ 051 public static final String RELEASE = "release"; // NOI18N 052 /** 053 * {@value #ESTOP} 054 */ 055 public static final String ESTOP = "eStop"; // NOI18N 056 /** 057 * {@value #IDLE} 058 */ 059 public static final String IDLE = "idle"; // NOI18N 060 /** 061 * {@value #SPEED_STEPS} 062 */ 063 public static final String SPEED_STEPS = "speedSteps"; // NOI18N 064 /** 065 * Used to notify clients of the number of clients controlling the same 066 * throttle. 067 * <p> 068 * {@value #CLIENTS} 069 */ 070 public static final String CLIENTS = "clients"; // NOI18N 071 private Throttle throttle; 072 private int speedSteps = 1; // Number of speed steps. 073 private DccLocoAddress address = null; 074 private String connectionPrefix = null; 075 private static final Logger log = LoggerFactory.getLogger(JsonThrottle.class); 076 077 protected JsonThrottle(DccLocoAddress address, JsonThrottleSocketService server) { 078 this.address = address; 079 } 080 081 protected JsonThrottle(DccLocoAddress address, JsonThrottleSocketService server, @CheckForNull String connectionPrefix) { 082 this.address = address; 083 this.connectionPrefix = connectionPrefix; 084 } 085 086 /** 087 * Creates a new JsonThrottle or returns an existing one if the request is 088 * for an existing throttle. 089 * <p> 090 * data can contain either a string {@link jmri.server.json.JSON#ID} node 091 * containing the ID of a {@link jmri.jmrit.roster.RosterEntry} or an 092 * integer {@link jmri.server.json.JSON#ADDRESS} node. If data contains an 093 * ADDRESS, the ID node is ignored. The ADDRESS may be accompanied by a 094 * boolean {@link jmri.server.json.JSON#IS_LONG_ADDRESS} node specifying the 095 * type of address, if IS_LONG_ADDRESS is not specified, the inverse of 096 * {@link jmri.ThrottleManager#canBeShortAddress(int)} is used as the "best 097 * guess" of the address length. 098 * 099 * @param throttleId The client's identity token for this throttle 100 * @param data JSON object containing either an ADDRESS or an ID 101 * @param server The server requesting this throttle on behalf of a 102 * client 103 * @param id message id set by client 104 * @return The throttle 105 * @throws jmri.server.json.JsonException if unable to get the requested 106 * {@link jmri.Throttle} 107 */ 108 public static JsonThrottle getThrottle(String throttleId, JsonNode data, JsonThrottleSocketService server, int id) 109 throws JsonException { 110 JsonThrottle throttle = null; 111 DccLocoAddress address = null; 112 BasicRosterEntry entry = null; 113 Locale locale = server.getConnection().getLocale(); 114 JsonThrottleManager manager = InstanceManager.getDefault(JsonThrottleManager.class); 115 116 // Resolve the ThrottleManager: use the connection-specific one when a 117 // prefix is supplied, otherwise fall back to the default. 118 String prefix = data.path(PREFIX).asText(); 119 ThrottleManager throttleManager; 120 if (!prefix.isEmpty()) { 121 SystemConnectionMemo memo = SystemConnectionMemoManager.getDefault() 122 .getSystemConnectionMemoForSystemPrefix(prefix); 123 if (memo != null && memo.provides(ThrottleManager.class)) { 124 throttleManager = memo.get(ThrottleManager.class); 125 } else { 126 throw new JsonException(HttpServletResponse.SC_BAD_REQUEST, 127 Bundle.getMessage(locale, "ErrorUnknownPrefix", prefix), id); 128 } 129 } else { 130 throttleManager = InstanceManager.getDefault(ThrottleManager.class); 131 prefix = null; 132 } 133 134 if (!data.path(ADDRESS).isMissingNode()) { 135 if (throttleManager.canBeLongAddress(data.path(ADDRESS).asInt()) || 136 throttleManager.canBeShortAddress(data.path(ADDRESS).asInt())) { 137 address = new DccLocoAddress(data.path(ADDRESS).asInt(), 138 data.path(IS_LONG_ADDRESS).asBoolean(!throttleManager.canBeShortAddress(data.path(ADDRESS).asInt()))); 139 } else { 140 log.warn("Address \"{}\" is not a valid address.", data.path(ADDRESS).asInt()); 141 throw new JsonException(HttpServletResponse.SC_BAD_REQUEST, 142 Bundle.getMessage(locale, "ErrorThrottleInvalidAddress", data.path(ADDRESS).asInt()), id); // NOI18N 143 } 144 } else if (!data.path(ROSTER_ENTRY).isMissingNode()) { 145 entry = Roster.getDefault().getEntryForId(data.path(ROSTER_ENTRY).asText()); 146 if (entry != null) { 147 address = entry.getDccLocoAddress(); 148 } else { 149 log.warn("Roster entry \"{}\" does not exist.", data.path(ROSTER_ENTRY).asText()); 150 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, 151 Bundle.getMessage(locale, "ErrorThrottleRosterEntry", data.path(ROSTER_ENTRY).asText()), id); // NOI18N 152 } 153 } else { 154 log.warn("No address specified"); 155 throw new JsonException(HttpServletResponse.SC_BAD_REQUEST, 156 Bundle.getMessage(locale, "ErrorThrottleNoAddress"), id); // NOI18N 157 } 158 // NOTE: JsonThrottleManager keys by DccLocoAddress only. If the same address is 159 // requested on two different connections (different prefix), the existing JsonThrottle 160 // (and its underlying connection) is reused and the new prefix is ignored. Fixing this 161 // would require keying on (address, prefix) — a separate, larger change. 162 if (manager.containsKey(address)) { 163 throttle = manager.get(address); 164 manager.put(throttle, server); 165 throttle.sendMessage(server.getConnection().getObjectMapper().createObjectNode().put(CLIENTS, 166 manager.getServers(throttle).size())); 167 } else { 168 throttle = new JsonThrottle(address, server, prefix); 169 if (entry != null) { 170 if (!throttleManager.requestThrottle(entry, throttle, false)) { 171 log.error("Unable to get rostered throttle for \"{}\".", entry.getId()); 172 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, Bundle 173 .getMessage(server.getConnection().getLocale(), "ErrorThrottleUnableToGetThrottle", entry.getId()), 174 id); 175 } 176 } else { 177 if (!throttleManager.requestThrottle(address, throttle, false)) { 178 log.error("Unable to get throttle for \"{}\".", address); 179 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, Bundle 180 .getMessage(server.getConnection().getLocale(), "ErrorThrottleUnableToGetThrottle", address), 181 id); 182 } 183 } 184 manager.put(address, throttle); 185 manager.put(throttle, server); 186 } 187 return throttle; 188 } 189 190 public void close(JsonThrottleSocketService server, boolean notifyClient) { 191 if (this.throttle != null) { 192 List<JsonThrottleSocketService> servers = 193 InstanceManager.getDefault(JsonThrottleManager.class).getServers(this); 194 if (servers.size() == 1 && servers.get(0).equals(server)) { 195 this.throttle.setSpeedSetting(0); 196 } 197 this.release(server, notifyClient); 198 } 199 } 200 201 public void release(JsonThrottleSocketService server, boolean notifyClient) { 202 JsonThrottleManager manager = InstanceManager.getDefault(JsonThrottleManager.class); 203 ObjectMapper mapper = server.getConnection().getObjectMapper(); 204 if (this.throttle != null) { 205 if (manager.getServers(this).size() == 1) { 206 this.throttle.release(this); 207 this.throttle.removePropertyChangeListener(this); 208 this.throttle = null; 209 } 210 if (notifyClient) { 211 this.sendMessage(mapper.createObjectNode().putNull(RELEASE), server); 212 } 213 } 214 manager.remove(this, server); 215 if (manager.getServers(this).isEmpty()) { 216 // Release address-based reference to this throttle if there are no 217 // servers using it 218 // so that when the server releases its reference, this throttle can 219 // be garbage collected 220 manager.remove(this.address); 221 } else { 222 this.sendMessage(mapper.createObjectNode().put(CLIENTS, manager.getServers(this).size())); 223 } 224 } 225 226 public void onMessage(Locale locale, JsonNode data, JsonThrottleSocketService server) { 227 data.fields().forEachRemaining((entry) -> { 228 String k = entry.getKey(); 229 JsonNode v = entry.getValue(); 230 switch (k) { 231 case ESTOP: 232 this.throttle.setSpeedSetting(-1); 233 return; // stop processing any commands that may conflict 234 // with ESTOP 235 case IDLE: 236 this.throttle.setSpeedSetting(0); 237 break; 238 case JSON.SPEED: 239 this.throttle.setSpeedSetting((float) v.asDouble()); 240 break; 241 case FORWARD: 242 this.throttle.setIsForward(v.asBoolean()); 243 break; 244 case RELEASE: 245 server.release(this); 246 break; 247 case STATUS: 248 this.sendStatus(server); 249 break; 250 case ADDRESS: 251 case NAME: 252 case PREFIX: 253 case THROTTLE: 254 case ROSTER_ENTRY: 255 // no action for address, name, prefix, or throttle property 256 break; 257 default: 258 for ( int i = 0; i< this.throttle.getFunctions().length; i++ ) { 259 if (k.equals(jmri.Throttle.getFunctionString(i))) { 260 this.throttle.setFunction(i,v.asBoolean()); 261 break; 262 } 263 } 264 log.debug("Unknown field \"{}\": \"{}\"", k, v); 265 // do not error on unknown or unexpected items, since a 266 // following item may be an ESTOP and we always want to 267 // catch those 268 break; 269 } 270 }); 271 } 272 273 public void sendMessage(ObjectNode data) { 274 new ArrayList<>(InstanceManager.getDefault(JsonThrottleManager.class).getServers(this)).stream() 275 .forEach(server -> this.sendMessage(data, server)); 276 } 277 278 public void sendMessage(ObjectNode data, JsonThrottleSocketService server) { 279 try { 280 // .deepCopy() ensures each server gets a unique (albeit identical) 281 // message 282 // to allow each server to modify the message as needed by its 283 // client 284 server.sendMessage(this, data.deepCopy()); 285 } catch (IOException ex) { 286 this.close(server, false); 287 log.warn("Unable to send message, closing connection: {}", ex.getMessage()); 288 try { 289 server.getConnection().close(); 290 } catch (IOException e1) { 291 log.warn("Unable to close connection.", e1); 292 } 293 } 294 } 295 296 @Override 297 public void propertyChange(PropertyChangeEvent evt) { 298 ObjectNode data = InstanceManager.getDefault(JsonThrottleManager.class).getObjectMapper().createObjectNode(); 299 String property = evt.getPropertyName(); 300 if (property.equals(Throttle.SPEEDSETTING)) { // NOI18N 301 data.put(JSON.SPEED, ((Number) evt.getNewValue()).floatValue()); 302 } else if (property.equals(Throttle.ISFORWARD)) { // NOI18N 303 data.put(FORWARD, ((Boolean) evt.getNewValue())); 304 } else if (property.startsWith(F) && !property.contains("Momentary")) { // NOI18N 305 data.put(property, ((Boolean) evt.getNewValue())); 306 } 307 if (data.size() > 0) { 308 this.sendMessage(data); 309 } 310 } 311 312 @Override 313 public void notifyThrottleFound(DccThrottle throttle) { 314 log.debug("Found throttle {}", throttle.getLocoAddress()); 315 this.throttle = throttle; 316 throttle.addPropertyChangeListener(this); 317 this.speedSteps = throttle.getSpeedStepMode().numSteps; 318 this.sendStatus(); 319 } 320 321 @Override 322 public void notifyFailedThrottleRequest(LocoAddress address, String reason) { 323 JsonThrottleManager manager = InstanceManager.getDefault(JsonThrottleManager.class); 324 for (JsonThrottleSocketService server : manager.getServers(this) 325 .toArray(new JsonThrottleSocketService[manager.getServers(this).size()])) { 326 // TODO: use message id correctly 327 this.sendErrorMessage(new JsonException(512, Bundle.getMessage(server.getConnection().getLocale(), 328 "ErrorThrottleRequestFailed", address, reason), 0), server); 329 server.release(this); 330 } 331 } 332 333 /** 334 * No steal or share decisions made locally 335 * <p> 336 * {@inheritDoc} 337 */ 338 @Override 339 public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) { 340 // no steal or share decisions made locally 341 } 342 343 private void sendErrorMessage(JsonException message, JsonThrottleSocketService server) { 344 try { 345 server.getConnection().sendMessage(message.getJsonMessage(), message.getId()); 346 } catch (IOException e) { 347 log.warn("Unable to send message, closing connection. ", e); 348 try { 349 server.getConnection().close(); 350 } catch (IOException e1) { 351 log.warn("Unable to close connection.", e1); 352 } 353 } 354 } 355 356 private void sendStatus() { 357 if (this.throttle != null) { 358 this.sendMessage(this.getStatus()); 359 } 360 } 361 362 protected void sendStatus(JsonThrottleSocketService server) { 363 if (this.throttle != null) { 364 this.sendMessage(this.getStatus(), server); 365 } 366 } 367 368 private ObjectNode getStatus() { 369 ObjectNode data = InstanceManager.getDefault(JsonThrottleManager.class).getObjectMapper().createObjectNode(); 370 data.put(ADDRESS, this.throttle.getLocoAddress().getNumber()); 371 data.put(JSON.SPEED, this.throttle.getSpeedSetting()); 372 data.put(FORWARD, this.throttle.getIsForward()); 373 for ( int i = 0; i< this.throttle.getFunctions().length; i++ ) { 374 data.put(Throttle.getFunctionString(i), this.throttle.getFunction(i)); 375 } 376 data.put(SPEED_STEPS, this.speedSteps); 377 data.put(CLIENTS, InstanceManager.getDefault(JsonThrottleManager.class).getServers(this).size()); 378 if (this.throttle.getRosterEntry() != null) { 379 data.put(ROSTER_ENTRY, this.throttle.getRosterEntry().getId()); 380 } 381 if (this.connectionPrefix != null && !this.connectionPrefix.isEmpty()) { 382 data.put(PREFIX, this.connectionPrefix); 383 } 384 return data; 385 } 386 387 /** 388 * Get the Throttle this JsonThrottle is a proxy for. 389 * 390 * @return the throttle or null if no throttle is set 391 */ 392 // package private 393 Throttle getThrottle() { 394 return this.throttle; 395 } 396}