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}