001package jmri.jmris.simpleserver;
002
003import java.beans.PropertyChangeEvent;
004import java.io.DataInputStream;
005import java.io.DataOutputStream;
006import java.io.IOException;
007import java.util.ArrayList;
008import java.util.List;
009import javax.management.Attribute;
010import jmri.JmriException;
011import jmri.jmris.AbstractOperationsServer;
012import jmri.jmris.JmriConnection;
013import jmri.jmrit.operations.locations.Location;
014import jmri.jmrit.operations.trains.Train;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018/**
019 * Simple interface between the JMRI operations and a network connection
020 *
021 * @author Paul Bender Copyright (C) 2010
022 * @author Dan Boudreau Copyright (C) 2012 (Documented the code, changed reply
023 * format, and some minor refactoring)
024 * @author Randall Wood Copyright (C) 2012
025 */
026public class SimpleOperationsServer extends AbstractOperationsServer {
027
028    /**
029     * All operation messages start with the key word "OPERATIONS" followed by a
030     * command like "TRAINS". The reply message also starts with the key word
031     * "OPERATIONS" followed by the original command followed by the desired
032     * results.
033     */
034    public static final String OPERATIONS = "OPERATIONS";
035
036    // The supported commands for operations
037    /**
038     * the tag identifying the train's identity
039     */
040    public static final String TRAIN = "TRAIN";
041
042    /**
043     * Returns a list of trains. One line for each train in the list.
044     */
045    public static final String TRAINS = "TRAINS";
046
047    /**
048     * Returns a list of locations that the trains visit. One line for each
049     * location in the list.
050     */
051    public static final String LOCATIONS = "LOCATIONS";
052
053    /**
054     * Requests/returns the train's length. The train's name is required. Proper
055     * message format: "OPERATIONS TRAIN=train_name , TRAINLENGTH" Returns train
056     * length if train exists, otherwise an error message.
057     * <p>
058     * Request: "Operations , TRAIN=train"
059     * <p>
060     * Reply: "OPERATIONS , TRAIN=train , TRAINLENGTH=train_length"
061     */
062    public static final String TRAINLENGTH = "TRAINLENGTH";
063
064    /**
065     * Requests/returns the train's weight. The train's name is required.
066     */
067    public static final String TRAINWEIGHT = "TRAINWEIGHT";
068
069    /**
070     * Requests/returns the number of cars in the train. The train's name is
071     * required.
072     */
073    public static final String TRAINCARS = "TRAINCARS";
074
075    /**
076     * Requests/returns the road and number of the lead loco for this train. The
077     * train's name is required.
078     */
079    public static final String TRAINLEADLOCO = "TRAINLEADLOCO";
080
081    /**
082     * Requests/returns the road and number of the caboose for this train if
083     * there's one assigned. The train's name is required.
084     */
085    public static final String TRAINCABOOSE = "TRAINCABOOSE";
086
087    /**
088     * Requests/returns the train's status. The train's name is required.
089     */
090    public static final String TRAINSTATUS = "TRAINSTATUS";
091
092    /**
093     * Terminates the train and returns the train's status. The train's name is
094     * required.
095     */
096    public static final String TERMINATE = "TERMINATE";
097
098    /**
099     * Sets/requests/returns the train's location or gets the train's current
100     * location.
101     * <p>
102     * Sets the train's location: "Operations , TRAIN=train_name ,
103     * TRAINLOCATION=location"
104     * <p>
105     * Requests the train's location: "OPERATIONS , TRAIN=train_name"
106     * <p>
107     * Returns the train's location: "OPERATIONS , TRAIN=train_name ,
108     * TRAINLOCATION=location"
109     */
110    public static final String TRAINLOCATION = "TRAINLOCATION";
111
112    private static final String REQUEST_DELIMITER = " , ";
113
114    /**
115     * the character that separates the field tag from its value.
116     */
117    public static final String FIELDSEPARATOR = "=";
118
119    private DataOutputStream output;
120    private JmriConnection connection;
121
122    public SimpleOperationsServer(JmriConnection connection) {
123        super();
124        this.connection = connection;
125    }
126
127    public SimpleOperationsServer(DataInputStream inStream, DataOutputStream outStream) {
128        super();
129        output = outStream;
130    }
131
132    /*
133     * Protocol Specific Simple Functions
134     */
135    /**
136     * send a String to the other end of the telnet connection.
137     * The String is composed of a set of attributes.
138     *
139     * @param contents is the ArrayList of Attributes to be sent. A linefeed
140     *                 ('\n") is appended to the String.
141     * @throws java.io.IOException if unable to send.
142     */
143    @Override
144    public void sendMessage(ArrayList<Attribute> contents) throws IOException {
145        this.sendMessage(constructOperationsMessage(contents) + "\n");
146    }
147
148    /**
149     * constructs an error message and sends it to the client. The error message
150     * will be
151     * <ul>
152     * <li> OPERATIONS: </li>
153     * <li> the error string </li>
154     * <li> "\n" </li>
155     * </ul>
156     *
157     * @param errorStatus is the error message. It need not include any padding
158     *                    - this method will add it. It should be plain text.
159     * @throws IOException if there is a problem sending the error message
160     */
161    @Override
162    public void sendErrorStatus(String errorStatus) throws IOException {
163        this.sendMessage(OPERATIONS + ": " + errorStatus + "\n");
164    }
165
166    /**
167     * constructs a request in a format that parseOperationsMessage can handle.
168     * An OperationsMessage has the format:
169     * <ul>
170     * <li> OPERATIONS </li>
171     * <li> " , " (delimiter) </li>
172     * <li> request/reponse </li>
173     * <li> any number of " , " , followed by additional request/response pairs
174     * </li>
175     * </ul>
176     * The meaning of request/response is context sensitive. If the
177     * SimpleOperationsServer client is sending the message, then it is a
178     * request. If the SimpleOperationsServer is sending the message, then it is
179     * a response.
180     *
181     * @param contents is an array of Attributes. An Attribute is a String (tag)
182     *                 and a value. For this use, the value will always be a
183     *                 String or null. Thus, "=" and REQUEST_DELIMITER are
184     *                 illegal in a tag and REQUEST_DELIMITER is illegal in a
185     *                 value.
186     * @return a String which is a serialized version of the attribute array,
187     *         which can be sent to an SimpleOperationsServer or received from a
188     *         SimpleOperationsServer
189     */
190    public static String constructOperationsMessage(List<Attribute> contents) {
191        StringBuilder result = new StringBuilder(OPERATIONS);
192        for (Attribute content : contents) {
193            result.append(REQUEST_DELIMITER).append(content.getName());
194            if (content.getValue() != null) {
195                result.append(FIELDSEPARATOR).append(content.getValue());
196            }
197        }
198        return new String(result);
199    }
200
201    /**
202     * parse a String presumably constructed by constructOperationsMessage. It
203     * breaks the String down into tag or tag=value pairs, using a
204     * REQUEST_DELIMITER as the separator. Each pair is further broken down into
205     * the tag and value and stuffed into an Attribute. The Attribute is then
206     * added to the resulting ArrayList.
207     * <p>
208     * The leading OPERATIONS String is NOT included. If the first String is
209     * not OPERATIONS, an empty ArrayList is returned.
210     *
211     * @param message is the String received
212     * @return an ArrayList of Attributes of the constituent pieces of the
213     *         message
214     */
215    // This should never have been a public method, Deprecating so we can
216    // make it private or eliminate it later.
217    //
218    // Note from @pabender in Issue #8877
219    // parseOprationsMessage() actually is just decoding a piece of the message after it has
220    // been sent over the network. It doesn't appear to decode the whole message. parseStatus()
221    // really is the method in the server message that actually does something use.
222    // The missing piece is that there isn't any code in the JMRIClient that handles the operations
223    // data. Until that is in place, parseOperationsMessage() shouldn't have been made private
224    // because there isn't any other way to use the message data sent by the server.
225    public static ArrayList<Attribute> parseOperationsMessage(String message) {
226        ArrayList<Attribute> contents = new ArrayList<>();
227        int start;
228        int end;
229        int equals;
230        String request;
231        if ((message != null) && message.startsWith(OPERATIONS)) {
232            for (start = message.indexOf(REQUEST_DELIMITER);
233                    start > 0;
234                    start = end) {  // step through all the requests/responses in the message
235                start += REQUEST_DELIMITER.length();
236                end = message.indexOf(REQUEST_DELIMITER, start);
237                if (end > 0) {
238                    request = message.substring(start, end);
239                } else {
240                    request = message.substring(start, message.length());
241                }
242
243                //convert a request/response to an Attribute and add it to the result
244                equals = request.indexOf(FIELDSEPARATOR);
245                if ((equals > 0) && (equals < (request.length() - 1))) {
246                    contents.add(new Attribute(request.substring(0, equals), request.substring(equals + 1, request.length())));
247                } else {
248                    contents.add(new Attribute(request, null));
249                }
250            }
251        }
252        return contents;
253    }
254
255    /**
256     * Parse operation commands.
257     * They all start with "OPERATIONS" followed by a command like "LOCATIONS".
258     * A command like "TRAINLENGTH" requires a train name.
259     * The delimiter is the tab character.
260     *
261     * @param statusString the string to parse.
262     * @throws jmri.JmriException if unable to parse status.
263     * @throws java.io.IOException if unable to send response.
264     */
265    @Override
266    public void parseStatus(String statusString) throws JmriException, IOException {
267        ArrayList<Attribute> contents = parseOperationsMessage(statusString);
268        ArrayList<Attribute> response = new ArrayList<>();
269        String trainName = null;
270        String tag;
271        String value;
272
273        for (Attribute field : contents) {
274            tag = field.getName();
275            if (TRAIN.equals(tag)) {
276                trainName = (String) field.getValue();
277                response.add(field);
278            } else if (LOCATIONS.equals(tag)) {
279                sendLocationList();
280            } else if (TRAINS.equals(tag)) {
281                sendTrainList();
282            } else if (trainName != null) {
283                if (null == tag) {
284                    throw new jmri.JmriException();
285                } else switch (tag) {
286                    case TRAINLENGTH:
287                        value = constructTrainLength(trainName);
288                        if (value != null) {
289                            response.add(new Attribute(TRAINLENGTH, value));
290                        }   break;
291                    case TRAINWEIGHT:
292                        value = constructTrainWeight(trainName);
293                        if (value != null) {
294                            response.add(new Attribute(TRAINWEIGHT, value));
295                        }   break;
296                    case TRAINCARS:
297                        value = constructTrainNumberOfCars(trainName);
298                        if (value != null) {
299                            response.add(new Attribute(TRAINCARS, value));
300                        }   break;
301                    case TRAINLEADLOCO:
302                        value = constructTrainLeadLoco(trainName);
303                        if (value != null) {
304                            response.add(new Attribute(TRAINLEADLOCO, value));
305                        }   break;
306                    case TRAINCABOOSE:
307                        value = constructTrainCaboose(trainName);
308                        if (value != null) {
309                            response.add(new Attribute(TRAINCABOOSE, value));
310                        }   break;
311                    case TRAINSTATUS:
312                        value = constructTrainStatus(trainName);
313                        if (value != null) {
314                            response.add(new Attribute(TRAINSTATUS, value));
315                        }   break;
316                    case TERMINATE:
317                        value = terminateTrain(trainName);
318                        if (value != null) {
319                            response.add(new Attribute(TERMINATE, value));
320                        }   break;
321                    case TRAINLOCATION:
322                        if (field.getValue() == null) {
323                            value = constructTrainLocation(trainName);
324                        } else {
325                            value = setTrainLocation(trainName, (String) field.getValue());
326                        }   if (value != null) {
327                            response.add(new Attribute(TRAINLOCATION, value));
328                        }   break;
329                    default:
330                        throw new jmri.JmriException();
331                }
332            } else {
333                throw new jmri.JmriException();
334            }
335        }
336        if (response.size() > 1) {  // something more than just a train ID
337            sendMessage(response);
338        }
339    }
340
341    private void sendMessage(String message) throws IOException {
342        if (this.output != null) {
343            this.output.writeBytes(message);
344        } else {
345            this.connection.sendMessage(message);
346        }
347    }
348
349    /**
350     * send a list of trains known by Operations to the client
351     */
352    @Override
353    public void sendTrainList() {
354        List<Train> trainList = tm.getTrainsByNameList();
355        ArrayList<Attribute> aTrain;
356        for (Train train : trainList) {
357            aTrain = new ArrayList<>(1);
358            aTrain.add(new Attribute(TRAINS, train.getName()));
359            try {
360                sendMessage(aTrain);
361            } catch (IOException ioe) {
362                log.debug("could not send train {}",train.getName());
363            }
364        }
365    }
366
367    /**
368     * send a list of locations known by Operations to the client
369     */
370    @Override
371    public void sendLocationList() {
372        List<Location> locationList = lm.getLocationsByNameList();
373        ArrayList<Attribute> location;
374        for (Location loc : locationList) {
375            location = new ArrayList<>(1);
376            location.add(new Attribute(LOCATIONS, loc));
377            try {
378                sendMessage(location);
379            } catch (IOException ioe) {
380                log.debug("could not send train {}",loc.getName());
381            }
382        }
383    }
384
385    /**
386     * sends the full status for a train to a client
387     *
388     * @param train The desired train.
389     * @throws IOException on failure to send an error message
390     */
391    @Override
392    public void sendFullStatus(Train train) throws IOException {
393        ArrayList<Attribute> status = new ArrayList<>();
394        if (train != null) {
395            status.add(new Attribute(TRAIN, train.getName()));
396            status.add(new Attribute(TRAINLOCATION, train.getCurrentLocationName()));
397            status.add(new Attribute(TRAINLENGTH, String.valueOf(train.getTrainLength())));
398            status.add(new Attribute(TRAINWEIGHT, String.valueOf(train.getTrainWeight())));
399            status.add(new Attribute(TRAINCARS, String.valueOf(train.getNumberCarsInTrain())));
400            status.add(new Attribute(TRAINLEADLOCO, constructTrainLeadLoco(train.getName())));
401            status.add(new Attribute(TRAINCABOOSE, constructTrainCaboose(train.getName())));
402            sendMessage(status);
403        }
404    }
405
406    @Override
407    public void propertyChange(PropertyChangeEvent e) {
408        log.debug("property change: {} old: {} new: {}", e.getPropertyName(), e.getOldValue(), e.getNewValue());
409        if (e.getPropertyName().equals(Train.BUILT_CHANGED_PROPERTY)) {
410            try {
411                sendFullStatus((Train) e.getSource());
412            } catch (IOException e1) {
413                log.error("Exception: ", e1);
414            }
415        }
416    }
417
418    private static final Logger log = LoggerFactory.getLogger(SimpleOperationsServer.class);
419
420}