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}