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 // Holds the one most recent command received before acquisition 077 // completes, so it can be replayed once the throttle is actually ready 078 // instead of being dropped -- only the FIRST acquisition gets this 079 // treatment (see everAcquired), which is separate from the "ignore 080 // after release" behavior. 081 private JsonNode pendingData; 082 private JsonThrottleSocketService pendingServer; 083 private boolean everAcquired = false; 084 085 protected JsonThrottle(DccLocoAddress address, JsonThrottleSocketService server) { 086 this.address = address; 087 } 088 089 protected JsonThrottle(DccLocoAddress address, JsonThrottleSocketService server, @CheckForNull String connectionPrefix) { 090 this.address = address; 091 this.connectionPrefix = connectionPrefix; 092 } 093 094 /** 095 * Creates a new JsonThrottle or returns an existing one if the request is 096 * for an existing throttle. 097 * <p> 098 * data can contain either a string {@link jmri.server.json.JSON#ID} node 099 * containing the ID of a {@link jmri.jmrit.roster.RosterEntry} or an 100 * integer {@link jmri.server.json.JSON#ADDRESS} node. If data contains an 101 * ADDRESS, the ID node is ignored. The ADDRESS may be accompanied by a 102 * boolean {@link jmri.server.json.JSON#IS_LONG_ADDRESS} node specifying the 103 * type of address, if IS_LONG_ADDRESS is not specified, the inverse of 104 * {@link jmri.ThrottleManager#canBeShortAddress(int)} is used as the "best 105 * guess" of the address length. 106 * 107 * @param throttleId The client's identity token for this throttle 108 * @param data JSON object containing either an ADDRESS or an ID 109 * @param server The server requesting this throttle on behalf of a 110 * client 111 * @param id message id set by client 112 * @return The throttle 113 * @throws jmri.server.json.JsonException if unable to get the requested 114 * {@link jmri.Throttle} 115 */ 116 public static JsonThrottle getThrottle(String throttleId, JsonNode data, JsonThrottleSocketService server, int id) 117 throws JsonException { 118 JsonThrottle throttle = null; 119 DccLocoAddress address = null; 120 BasicRosterEntry entry = null; 121 Locale locale = server.getConnection().getLocale(); 122 JsonThrottleManager manager = InstanceManager.getDefault(JsonThrottleManager.class); 123 124 // Resolve the ThrottleManager: use the connection-specific one when a 125 // prefix is supplied, otherwise fall back to the default. 126 String prefix = data.path(PREFIX).asText(); 127 ThrottleManager throttleManager; 128 if (!prefix.isEmpty()) { 129 SystemConnectionMemo memo = SystemConnectionMemoManager.getDefault() 130 .getSystemConnectionMemoForSystemPrefix(prefix); 131 if (memo != null && memo.provides(ThrottleManager.class)) { 132 throttleManager = memo.get(ThrottleManager.class); 133 } else { 134 throw new JsonException(HttpServletResponse.SC_BAD_REQUEST, 135 Bundle.getMessage(locale, "ErrorUnknownPrefix", prefix), id); 136 } 137 } else { 138 throttleManager = InstanceManager.getDefault(ThrottleManager.class); 139 prefix = null; 140 } 141 142 if (!data.path(ADDRESS).isMissingNode()) { 143 if (throttleManager.canBeLongAddress(data.path(ADDRESS).asInt()) || 144 throttleManager.canBeShortAddress(data.path(ADDRESS).asInt())) { 145 address = new DccLocoAddress(data.path(ADDRESS).asInt(), 146 data.path(IS_LONG_ADDRESS).asBoolean(!throttleManager.canBeShortAddress(data.path(ADDRESS).asInt()))); 147 } else { 148 log.warn("Address \"{}\" is not a valid address.", data.path(ADDRESS).asInt()); 149 throw new JsonException(HttpServletResponse.SC_BAD_REQUEST, 150 Bundle.getMessage(locale, "ErrorThrottleInvalidAddress", data.path(ADDRESS).asInt()), id); // NOI18N 151 } 152 } else if (!data.path(ROSTER_ENTRY).isMissingNode()) { 153 entry = Roster.getDefault().getEntryForId(data.path(ROSTER_ENTRY).asText()); 154 if (entry != null) { 155 address = entry.getDccLocoAddress(); 156 } else { 157 log.warn("Roster entry \"{}\" does not exist.", data.path(ROSTER_ENTRY).asText()); 158 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, 159 Bundle.getMessage(locale, "ErrorThrottleRosterEntry", data.path(ROSTER_ENTRY).asText()), id); // NOI18N 160 } 161 } else { 162 log.warn("No address specified"); 163 throw new JsonException(HttpServletResponse.SC_BAD_REQUEST, 164 Bundle.getMessage(locale, "ErrorThrottleNoAddress"), id); // NOI18N 165 } 166 // NOTE: JsonThrottleManager keys by DccLocoAddress only. If the same address is 167 // requested on two different connections (different prefix), the existing JsonThrottle 168 // (and its underlying connection) is reused and the new prefix is ignored. Fixing this 169 // would require keying on (address, prefix) — a separate, larger change. 170 if (manager.containsKey(address)) { 171 throttle = manager.get(address); 172 manager.put(throttle, server); 173 throttle.sendMessage(server.getConnection().getObjectMapper().createObjectNode().put(CLIENTS, 174 manager.getServers(throttle).size())); 175 } else { 176 throttle = new JsonThrottle(address, server, prefix); 177 if (entry != null) { 178 if (!throttleManager.requestThrottle(entry, throttle, false)) { 179 log.error("Unable to get rostered throttle for \"{}\".", entry.getId()); 180 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, Bundle 181 .getMessage(server.getConnection().getLocale(), "ErrorThrottleUnableToGetThrottle", entry.getId()), 182 id); 183 } 184 } else { 185 if (!throttleManager.requestThrottle(address, throttle, false)) { 186 log.error("Unable to get throttle for \"{}\".", address); 187 throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, Bundle 188 .getMessage(server.getConnection().getLocale(), "ErrorThrottleUnableToGetThrottle", address), 189 id); 190 } 191 } 192 manager.put(address, throttle); 193 manager.put(throttle, server); 194 } 195 return throttle; 196 } 197 198 public void close(JsonThrottleSocketService server, boolean notifyClient) { 199 if (this.throttle != null) { 200 List<JsonThrottleSocketService> servers = 201 InstanceManager.getDefault(JsonThrottleManager.class).getServers(this); 202 if (servers.size() == 1 && servers.get(0).equals(server)) { 203 this.throttle.setSpeedSetting(0); 204 } 205 this.release(server, notifyClient); 206 } 207 } 208 209 public void release(JsonThrottleSocketService server, boolean notifyClient) { 210 JsonThrottleManager manager = InstanceManager.getDefault(JsonThrottleManager.class); 211 ObjectMapper mapper = server.getConnection().getObjectMapper(); 212 if (this.throttle != null) { 213 if (manager.getServers(this).size() == 1) { 214 this.throttle.release(this); 215 this.throttle.removePropertyChangeListener(this); 216 this.throttle = null; 217 } 218 if (notifyClient) { 219 this.sendMessage(mapper.createObjectNode().putNull(RELEASE), server); 220 } 221 } 222 manager.remove(this, server); 223 if (manager.getServers(this).isEmpty()) { 224 // Release address-based reference to this throttle if there are no 225 // servers using it 226 // so that when the server releases its reference, this throttle can 227 // be garbage collected 228 manager.remove(this.address); 229 } else { 230 this.sendMessage(mapper.createObjectNode().put(CLIENTS, manager.getServers(this).size())); 231 } 232 } 233 234 public void onMessage(Locale locale, JsonNode data, JsonThrottleSocketService server) { 235 // this.throttle is null from construction until the async 236 // ThrottleListener callback (notifyThrottleFound(), below) sets it, 237 // and null again after release(). A command received during either 238 // window is ignored here rather than crashing (every field handler 239 // below dereferences this.throttle unconditionally). If this is 240 // before the FIRST acquisition, queue it for replay in 241 // notifyThrottleFound() instead of dropping it -- see pendingData 242 // above. 243 if (this.throttle == null) { 244 if (!this.everAcquired) { 245 log.warn("onMessage(): received a command for {} before its throttle was available -- queuing for replay once acquired", 246 this.address); 247 this.pendingData = data; 248 this.pendingServer = server; 249 } else { 250 log.warn("onMessage(): received a command for {} after its throttle was released -- ignoring", 251 this.address); 252 } 253 return; 254 } 255 applyFields(data, server); 256 } 257 258 private void applyFields(JsonNode data, JsonThrottleSocketService server) { 259 for (var entry : data.properties()) { 260 String k = entry.getKey(); 261 JsonNode v = entry.getValue(); 262 switch (k) { 263 case ESTOP: 264 this.throttle.setSpeedSetting(-1); 265 return; // stop processing any commands that may conflict 266 // with ESTOP 267 case IDLE: 268 this.throttle.setSpeedSetting(0); 269 break; 270 case JSON.SPEED: 271 this.throttle.setSpeedSetting((float) v.asDouble()); 272 break; 273 case FORWARD: 274 this.throttle.setIsForward(v.asBoolean()); 275 break; 276 case RELEASE: 277 // server.release(this) nulls out this.throttle. Returning 278 // immediately (matching ESTOP above) stops the loop from 279 // processing any remaining fields in this message against 280 // a now-null throttle -- JSON object field order isn't 281 // guaranteed, so a field after "release" could otherwise 282 // still be reached. 283 server.release(this); 284 return; 285 case STATUS: 286 this.sendStatus(server); 287 break; 288 case ADDRESS: 289 case NAME: 290 case PREFIX: 291 case THROTTLE: 292 case ROSTER_ENTRY: 293 case IS_LONG_ADDRESS: 294 // no action for address, name, prefix, throttle, or 295 // isLongAddress property -- isLongAddress previously 296 // fell through to default (see the field report on 297 // RELEASE above for how that combination crashed). 298 break; 299 default: 300 for ( int i = 0; i< this.throttle.getFunctions().length; i++ ) { 301 if (k.equals(jmri.Throttle.getFunctionString(i))) { 302 this.throttle.setFunction(i,v.asBoolean()); 303 break; 304 } 305 } 306 log.debug("Unknown field \"{}\": \"{}\"", k, v); 307 // do not error on unknown or unexpected items, since a 308 // following item may be an ESTOP and we always want to 309 // catch those 310 break; 311 } 312 } 313 } 314 315 public void sendMessage(ObjectNode data) { 316 new ArrayList<>(InstanceManager.getDefault(JsonThrottleManager.class).getServers(this)).stream() 317 .forEach(server -> this.sendMessage(data, server)); 318 } 319 320 public void sendMessage(ObjectNode data, JsonThrottleSocketService server) { 321 try { 322 // .deepCopy() ensures each server gets a unique (albeit identical) 323 // message 324 // to allow each server to modify the message as needed by its 325 // client 326 server.sendMessage(this, data.deepCopy()); 327 } catch (IOException ex) { 328 this.close(server, false); 329 log.warn("Unable to send message, closing connection: {}", ex.getMessage()); 330 try { 331 server.getConnection().close(); 332 } catch (IOException e1) { 333 log.warn("Unable to close connection.", e1); 334 } 335 } 336 } 337 338 @Override 339 public void propertyChange(PropertyChangeEvent evt) { 340 ObjectNode data = InstanceManager.getDefault(JsonThrottleManager.class).getObjectMapper().createObjectNode(); 341 String property = evt.getPropertyName(); 342 if (property.equals(Throttle.SPEEDSETTING)) { // NOI18N 343 data.put(JSON.SPEED, ((Number) evt.getNewValue()).floatValue()); 344 } else if (property.equals(Throttle.ISFORWARD)) { // NOI18N 345 data.put(FORWARD, ((Boolean) evt.getNewValue())); 346 } else if (property.startsWith(F) && !property.contains("Momentary")) { // NOI18N 347 data.put(property, ((Boolean) evt.getNewValue())); 348 } 349 if (data.size() > 0) { 350 this.sendMessage(data); 351 } 352 } 353 354 @Override 355 public void notifyThrottleFound(DccThrottle throttle) { 356 log.debug("Found throttle {}", throttle.getLocoAddress()); 357 this.throttle = throttle; 358 this.everAcquired = true; 359 throttle.addPropertyChangeListener(this); 360 this.speedSteps = throttle.getSpeedStepMode().numSteps; 361 this.sendStatus(); 362 // Replay whatever command arrived before this.throttle was ready -- 363 // see pendingData above. Applied after sendStatus() so a client 364 // watching for state changes sees the acquisition first, then the 365 // requested command. 366 if (this.pendingData != null) { 367 JsonNode replay = this.pendingData; 368 JsonThrottleSocketService replayServer = this.pendingServer; 369 this.pendingData = null; 370 this.pendingServer = null; 371 applyFields(replay, replayServer); 372 } 373 } 374 375 @Override 376 public void notifyFailedThrottleRequest(LocoAddress address, String reason) { 377 JsonThrottleManager manager = InstanceManager.getDefault(JsonThrottleManager.class); 378 for (JsonThrottleSocketService server : manager.getServers(this) 379 .toArray(new JsonThrottleSocketService[manager.getServers(this).size()])) { 380 // TODO: use message id correctly 381 this.sendErrorMessage(new JsonException(512, Bundle.getMessage(server.getConnection().getLocale(), 382 "ErrorThrottleRequestFailed", address, reason), 0), server); 383 server.release(this); 384 } 385 } 386 387 /** 388 * No steal or share decisions made locally 389 * <p> 390 * {@inheritDoc} 391 */ 392 @Override 393 public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) { 394 // no steal or share decisions made locally 395 } 396 397 private void sendErrorMessage(JsonException message, JsonThrottleSocketService server) { 398 try { 399 server.getConnection().sendMessage(message.getJsonMessage(), message.getId()); 400 } catch (IOException e) { 401 log.warn("Unable to send message, closing connection. ", e); 402 try { 403 server.getConnection().close(); 404 } catch (IOException e1) { 405 log.warn("Unable to close connection.", e1); 406 } 407 } 408 } 409 410 private void sendStatus() { 411 if (this.throttle != null) { 412 this.sendMessage(this.getStatus()); 413 } 414 } 415 416 protected void sendStatus(JsonThrottleSocketService server) { 417 if (this.throttle != null) { 418 this.sendMessage(this.getStatus(), server); 419 } 420 } 421 422 private ObjectNode getStatus() { 423 ObjectNode data = InstanceManager.getDefault(JsonThrottleManager.class).getObjectMapper().createObjectNode(); 424 data.put(ADDRESS, this.throttle.getLocoAddress().getNumber()); 425 data.put(JSON.SPEED, this.throttle.getSpeedSetting()); 426 data.put(FORWARD, this.throttle.getIsForward()); 427 for ( int i = 0; i< this.throttle.getFunctions().length; i++ ) { 428 data.put(Throttle.getFunctionString(i), this.throttle.getFunction(i)); 429 } 430 data.put(SPEED_STEPS, this.speedSteps); 431 data.put(CLIENTS, InstanceManager.getDefault(JsonThrottleManager.class).getServers(this).size()); 432 if (this.throttle.getRosterEntry() != null) { 433 data.put(ROSTER_ENTRY, this.throttle.getRosterEntry().getId()); 434 } 435 if (this.connectionPrefix != null && !this.connectionPrefix.isEmpty()) { 436 data.put(PREFIX, this.connectionPrefix); 437 } 438 return data; 439 } 440 441 /** 442 * Get the Throttle this JsonThrottle is a proxy for. 443 * 444 * @return the throttle or null if no throttle is set 445 */ 446 // package private 447 Throttle getThrottle() { 448 return this.throttle; 449 } 450}