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