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}