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}