001package jmri.server.json.operations; 002 003import static jmri.server.json.reporter.JsonReporter.REPORTER; 004 005import java.util.Locale; 006 007import javax.annotation.Nonnull; 008import javax.servlet.http.HttpServletResponse; 009 010import org.slf4j.Logger; 011import org.slf4j.LoggerFactory; 012 013import com.fasterxml.jackson.databind.ObjectMapper; 014import com.fasterxml.jackson.databind.node.ArrayNode; 015import com.fasterxml.jackson.databind.node.ObjectNode; 016 017import jmri.InstanceManager; 018import jmri.Reporter; 019import jmri.jmrit.operations.locations.*; 020import jmri.jmrit.operations.rollingstock.RollingStock; 021import jmri.jmrit.operations.rollingstock.cars.Car; 022import jmri.jmrit.operations.rollingstock.cars.CarManager; 023import jmri.jmrit.operations.rollingstock.engines.Engine; 024import jmri.jmrit.operations.rollingstock.engines.EngineManager; 025import jmri.jmrit.operations.routes.RouteLocation; 026import jmri.jmrit.operations.trains.Train; 027import jmri.jmrit.operations.trains.TrainManager; 028import jmri.jmrit.operations.trains.trainbuilder.TrainCommon; 029import jmri.server.json.JSON; 030import jmri.server.json.JsonException; 031import jmri.server.json.consist.JsonConsist; 032 033/** 034 * Utilities used by JSON services for Operations 035 * 036 * @author Randall Wood Copyright 2019 037 */ 038public class JsonUtil { 039 040 private final ObjectMapper mapper; 041 private static final Logger log = LoggerFactory.getLogger(JsonUtil.class); 042 043 /** 044 * Create utilities. 045 * 046 * @param mapper the mapper used to create JSON nodes 047 */ 048 public JsonUtil(ObjectMapper mapper) { 049 this.mapper = mapper; 050 } 051 052 /** 053 * Get the JSON representation of a Car. 054 * 055 * @param name the ID of the Car 056 * @param locale the client's locale 057 * @param id the message id set by the client 058 * @return the JSON representation of the Car 059 * @throws JsonException if no car by name exists 060 */ 061 public ObjectNode getCar(String name, Locale locale, int id) throws JsonException { 062 Car car = carManager().getById(name); 063 if (car == null) { 064 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, 065 Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, JsonOperations.CAR, name), id); 066 } 067 return this.getCar(car, locale); 068 } 069 070 /** 071 * Get the JSON representation of an Engine. 072 * 073 * @param engine the Engine 074 * @param locale the client's locale 075 * @return the JSON representation of engine 076 */ 077 public ObjectNode getEngine(Engine engine, Locale locale) { 078 return getEngine(engine, getRollingStock(engine, locale), locale); 079 } 080 081 /** 082 * Get the JSON representation of an Engine. 083 * 084 * @param engine the Engine 085 * @param data the JSON data from 086 * {@link #getRollingStock(RollingStock, Locale)} 087 * @param locale the client's locale 088 * @return the JSON representation of engine 089 */ 090 public ObjectNode getEngine(Engine engine, ObjectNode data, Locale locale) { 091 data.put(JsonOperations.MODEL, engine.getModel()); 092 data.put(JsonOperations.HP, engine.getHp()); 093 data.put(JsonConsist.CONSIST, engine.getConsistName()); 094 return data; 095 } 096 097 /** 098 * Get the JSON representation of an Engine. 099 * 100 * @param name the ID of the Engine 101 * @param locale the client's locale 102 * @param id the message id set by the client 103 * @return the JSON representation of engine 104 * @throws JsonException if no engine exists by name 105 */ 106 public ObjectNode getEngine(String name, Locale locale, int id) throws JsonException { 107 Engine engine = engineManager().getById(name); 108 if (engine == null) { 109 throw new JsonException(HttpServletResponse.SC_NOT_FOUND, 110 Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, JsonOperations.ENGINE, name), id); 111 } 112 return this.getEngine(engine, locale); 113 } 114 115 /** 116 * Get a JSON representation of a Car. 117 * 118 * @param car the Car 119 * @param locale the client's locale 120 * @return the JSON representation of car 121 */ 122 public ObjectNode getCar(@Nonnull Car car, Locale locale) { 123 return getCar(car, getRollingStock(car, locale), locale); 124 } 125 126 /** 127 * Get a JSON representation of a Car. 128 * 129 * @param car the Car 130 * @param data the JSON data from 131 * {@link #getRollingStock(RollingStock, Locale)} 132 * @param locale the client's locale 133 * @return the JSON representation of car 134 */ 135 public ObjectNode getCar(@Nonnull Car car, @Nonnull ObjectNode data, Locale locale) { 136 data.put(JsonOperations.LOAD, car.getLoadName().split(TrainCommon.HYPHEN)[0]); // NOI18N 137 data.put(JsonOperations.HAZARDOUS, car.isHazardous()); 138 data.put(JsonOperations.CABOOSE, car.isCaboose()); 139 data.put(JsonOperations.PASSENGER, car.isPassenger()); 140 data.put(JsonOperations.FRED, car.hasFred()); 141 data.put(JsonOperations.SETOUT_COMMENT, car.getDropComment()); 142 data.put(JsonOperations.PICKUP_COMMENT, car.getPickupComment()); 143 data.put(JsonOperations.KERNEL, car.getKernelName()); 144 data.put(JsonOperations.UTILITY, car.isUtility()); 145 data.put(JsonOperations.IS_LOCAL, car.isLocalMove()); 146 data.put(JsonOperations.LAST_TRAIN, car.getLastTrainName()); 147 if (car.getFinalDestinationTrack() != null) { 148 data.set(JsonOperations.FINAL_DESTINATION, this.getRSLocationAndTrack(car.getFinalDestinationTrack(), null, locale)); 149 } else if (car.getFinalDestination() != null) { 150 data.set(JsonOperations.FINAL_DESTINATION, 151 this.getRSLocation(car.getFinalDestination(), (RouteLocation) null, locale)); 152 } else { 153 data.set(JsonOperations.FINAL_DESTINATION, null); 154 } 155 if (car.getReturnWhenEmptyDestTrack() != null) { 156 data.set(JsonOperations.RETURN_WHEN_EMPTY, 157 this.getRSLocationAndTrack(car.getReturnWhenEmptyDestTrack(), null, locale)); 158 } else if (car.getReturnWhenEmptyDestination() != null) { 159 data.set(JsonOperations.RETURN_WHEN_EMPTY, 160 this.getRSLocation(car.getReturnWhenEmptyDestination(), (RouteLocation) null, locale)); 161 } else { 162 data.set(JsonOperations.RETURN_WHEN_EMPTY, null); 163 } 164 if (car.getReturnWhenLoadedDestTrack() != null) { 165 data.set(JsonOperations.RETURN_WHEN_LOADED, 166 this.getRSLocationAndTrack(car.getReturnWhenLoadedDestTrack(), null, locale)); 167 } else if (car.getReturnWhenLoadedDestination() != null) { 168 data.set(JsonOperations.RETURN_WHEN_LOADED, 169 this.getRSLocation(car.getReturnWhenLoadedDestination(), (RouteLocation) null, locale)); 170 } else { 171 data.set(JsonOperations.RETURN_WHEN_LOADED, null); 172 } 173 data.put(JsonOperations.DIVISION, car.getDivisionName()); 174 data.put(JsonOperations.BLOCKING_ORDER, car.isPassenger() ? Integer.toString(car.getBlocking()) : ""); 175 data.put(JSON.STATUS, car.getStatus().replace("<", "<").replace(">", ">")); 176 return data; 177 } 178 179 /** 180 * Get the JSON representation of a Location. 181 * <p> 182 * <strong>Note:</strong>use {@link #getRSLocation(Location, Locale)} if 183 * including in rolling stock or train. 184 * 185 * @param location the location 186 * @param locale the client's locale 187 * @return the JSON representation of location 188 */ 189 public ObjectNode getLocation(@Nonnull Location location, Locale locale) { 190 ObjectNode data = mapper.createObjectNode(); 191 data.put(JSON.USERNAME, location.getName()); 192 data.put(JSON.NAME, location.getId()); 193 data.put(JSON.LENGTH, location.getLength()); 194 data.put(JSON.COMMENT, location.getCommentWithColor()); 195 Reporter reporter = location.getReporter(); 196 data.put(REPORTER, reporter != null ? reporter.getSystemName() : ""); 197 // note type defaults to all in-use rolling stock types 198 ArrayNode types = data.putArray(JsonOperations.CAR_TYPE); 199 for (String type : location.getTypeNames()) { 200 types.add(type); 201 } 202 ArrayNode tracks = data.putArray(JSON.TRACK); 203 for (Track track : location.getTracksList()) { 204 tracks.add(getTrack(track, locale)); 205 } 206 return data; 207 } 208 209 /** 210 * Get the JSON representation of a Location. 211 * 212 * @param name the ID of the location 213 * @param locale the client's locale 214 * @param id the message id set by the client 215 * @return the JSON representation of the location 216 * @throws JsonException if id does not match a known location 217 */ 218 public ObjectNode getLocation(String name, Locale locale, int id) throws JsonException { 219 if (locationManager().getLocationById(name) == null) { 220 log.error("Unable to get location id [{}].", name); 221 throw new JsonException(404, 222 Bundle.getMessage(locale, JsonException.ERROR_OBJECT, JSON.LOCATION, name), id); 223 } 224 return getLocation(locationManager().getLocationById(name), locale); 225 } 226 227 /** 228 * Get a Track in JSON. 229 * <p> 230 * <strong>Note:</strong>use {@link #getRSTrack(Track, Locale)} if including 231 * in rolling stock or train. 232 * 233 * @param track the track to get 234 * @param locale the client's locale 235 * @return a JSON representation of the track 236 */ 237 public ObjectNode getTrack(Track track, Locale locale) { 238 ObjectNode node = mapper.createObjectNode(); 239 node.put(JSON.USERNAME, track.getName()); 240 node.put(JSON.NAME, track.getId()); 241 node.put(JSON.COMMENT, track.getComment()); 242 node.put(JSON.LENGTH, track.getLength()); 243 // only includes location ID to avoid recursion 244 node.put(JSON.LOCATION, track.getLocation().getId()); 245 Reporter reporter = track.getReporter(); 246 node.put(REPORTER, reporter != null ? reporter.getSystemName() : ""); 247 node.put(JSON.TYPE, track.getTrackType()); 248 // note type defaults to all in-use rolling stock types 249 ArrayNode types = node.putArray(JsonOperations.CAR_TYPE); 250 for (String type : track.getTypeNames()) { 251 types.add(type); 252 } 253 return node; 254 } 255 256 /** 257 * Get the JSON representation of a Location for use in rolling stock or 258 * train. 259 * <p> 260 * <strong>Note:</strong>use {@link #getLocation(Location, Locale)} if not 261 * including in rolling stock or train. 262 * 263 * @param location the location 264 * @param locale the client's locale 265 * @return the JSON representation of location 266 */ 267 public ObjectNode getRSLocation(@Nonnull Location location, Locale locale) { 268 ObjectNode data = mapper.createObjectNode(); 269 data.put(JSON.USERNAME, location.getName()); 270 data.put(JSON.NAME, location.getId()); 271 return data; 272 } 273 274 private ObjectNode getRSLocation(Location location, RouteLocation routeLocation, Locale locale) { 275 ObjectNode node = getRSLocation(location, locale); 276 if (routeLocation != null) { 277 node.put(JSON.ROUTE, routeLocation.getId()); 278 } else { 279 node.put(JSON.ROUTE, (String) null); 280 } 281 return node; 282 } 283 284 private ObjectNode getRSLocationAndTrack(Track track, RouteLocation routeLocation, Locale locale) { 285 ObjectNode node = this.getRSLocation(track.getLocation(), routeLocation, locale); 286 node.set(JSON.TRACK, this.getRSTrack(track, locale)); 287 return node; 288 } 289 290 /** 291 * Get a Track in JSON for use in rolling stock or train. 292 * <p> 293 * <strong>Note:</strong>use {@link #getTrack(Track, Locale)} if not 294 * including in rolling stock or train. 295 * 296 * @param track the track to get 297 * @param locale the client's locale 298 * @return a JSON representation of the track 299 */ 300 public ObjectNode getRSTrack(Track track, Locale locale) { 301 ObjectNode node = mapper.createObjectNode(); 302 node.put(JSON.USERNAME, track.getName()); 303 node.put(JSON.NAME, track.getId()); 304 return node; 305 } 306 307 public ObjectNode getRollingStock(@Nonnull RollingStock rs, Locale locale) { 308 ObjectNode node = mapper.createObjectNode(); 309 node.put(JSON.NAME, rs.getId()); 310 node.put(JsonOperations.NUMBER, TrainCommon.splitString(rs.getNumber())); 311 node.put(JsonOperations.ROAD, rs.getRoadName().split(TrainCommon.HYPHEN)[0]); 312 node.put(JSON.RFID, rs.getRfid()); 313 if (!rs.getWhereLastSeenName().equals(Car.NONE)) { 314 node.put(JSON.WHERELASTSEEN, rs.getWhereLastSeenName() + 315 (rs.getTrackLastSeenName().equals(Car.NONE) ? "" : " (" + rs.getTrackLastSeenName() + ")")); 316 } else { 317 node.set(JSON.WHERELASTSEEN, null); 318 } 319 if (!rs.getWhenLastSeenDate().equals(Car.NONE)) { 320 node.put(JSON.WHENLASTSEEN, rs.getWhenLastSeenDate()); 321 } else { 322 node.set(JSON.WHENLASTSEEN, null); 323 } 324 // second half of string can be anything 325 String[] type = rs.getTypeName().split(TrainCommon.HYPHEN, 2); 326 node.put(JsonOperations.TYPE, type[0]); 327 node.put(JsonOperations.CAR_SUB_TYPE, type.length == 2 ? type[1] : ""); 328 node.put(JsonOperations.LENGTH, rs.getLengthInteger()); 329 node.put(JsonOperations.WEIGHT, rs.getAdjustedWeightTons()); 330 node.put(JsonOperations.WEIGHT_TONS, rs.getWeightTons()); 331 node.put(JsonOperations.COLOR, rs.getColor()); 332 node.put(JsonOperations.OWNER, rs.getOwnerName()); 333 node.put(JsonOperations.BUILT, rs.getBuilt()); 334 node.put(JsonOperations.COMMENT, rs.getComment()); 335 node.put(JsonOperations.OUT_OF_SERVICE, rs.isOutOfService()); 336 node.put(JsonOperations.LOCATION_UNKNOWN, rs.isLocationUnknown()); 337 if (rs.getTrack() != null) { 338 node.set(JsonOperations.LOCATION, this.getRSLocationAndTrack(rs.getTrack(), rs.getRouteLocation(), locale)); 339 } else if (rs.getLocation() != null) { 340 node.set(JsonOperations.LOCATION, this.getRSLocation(rs.getLocation(), rs.getRouteLocation(), locale)); 341 } else { 342 node.set(JsonOperations.LOCATION, null); 343 } 344 if (rs.getTrain() != null) { 345 node.put(JsonOperations.TRAIN_ID, rs.getTrain().getId()); 346 node.put(JsonOperations.TRAIN_NAME, rs.getTrain().getName()); 347 node.put(JsonOperations.TRAIN_ICON_NAME, rs.getTrain().getIconName()); 348 } else { 349 node.set(JsonOperations.TRAIN_ID, null); 350 node.set(JsonOperations.TRAIN_NAME, null); 351 node.set(JsonOperations.TRAIN_ICON_NAME, null); 352 } 353 if (rs.getDestinationTrack() != null) { 354 node.set(JsonOperations.DESTINATION, 355 this.getRSLocationAndTrack(rs.getDestinationTrack(), rs.getRouteDestination(), locale)); 356 } else if (rs.getDestination() != null) { 357 node.set(JsonOperations.DESTINATION, this.getRSLocation(rs.getDestination(), rs.getRouteDestination(), locale)); 358 } else { 359 node.set(JsonOperations.DESTINATION, null); 360 } 361 return node; 362 } 363 364 /** 365 * Get the JSON representation of a Train. 366 * 367 * @param train the train 368 * @param locale the client's locale 369 * @return the JSON representation of train 370 */ 371 public ObjectNode getTrain(Train train, Locale locale) { 372 ObjectNode data = this.mapper.createObjectNode(); 373 data.put(JSON.USERNAME, train.getName()); 374 data.put(JSON.ICON_NAME, train.getIconName()); 375 data.put(JSON.NAME, train.getId()); 376 data.put(JSON.DEPARTURE_TIME, train.getFormatedDepartureTime()); 377 data.put(JSON.DESCRIPTION, train.getDescription()); 378 data.put(JSON.COMMENT, train.getComment()); 379 if (train.getRoute() != null) { 380 data.put(JSON.ROUTE, train.getRoute().getName()); 381 data.put(JSON.ROUTE_ID, train.getRoute().getId()); 382 data.set(JSON.LOCATIONS, this.getRouteLocationsForTrain(train, locale)); 383 } 384 data.set(JSON.ENGINES, this.getEnginesForTrain(train, locale)); 385 data.set(JsonOperations.CARS, this.getCarsForTrain(train, locale)); 386 if (train.getTrainDepartsName() != null) { 387 data.put(JSON.DEPARTURE_LOCATION, train.getTrainDepartsName()); 388 } 389 if (train.getTrainTerminatesName() != null) { 390 data.put(JSON.TERMINATES_LOCATION, train.getTrainTerminatesName()); 391 } 392 data.put(JSON.LOCATION, train.getCurrentLocationName()); 393 if (train.getCurrentRouteLocation() != null) { 394 data.put(JsonOperations.LOCATION_ID, train.getCurrentRouteLocation().getId()); 395 } 396 data.put(JSON.STATUS, train.getStatus(locale)); 397 data.put(JSON.STATUS_CODE, train.getStatusCode()); 398 data.put(JSON.LENGTH, train.getTrainLength()); 399 data.put(JSON.WEIGHT, train.getTrainWeight()); 400 if (train.getLeadEngine() != null) { 401 data.put(JsonOperations.LEAD_ENGINE, train.getLeadEngine().toString()); 402 } 403 data.put(JsonOperations.CABOOSE, train.getCabooseRoadAndNumber()); 404 return data; 405 } 406 407 /** 408 * Get the JSON representation of a Train. 409 * 410 * @param name the id of the train 411 * @param locale the client's locale 412 * @param id the message id set by the client 413 * @return the JSON representation of the train with id 414 * @throws JsonException if id does not represent a known train 415 */ 416 public ObjectNode getTrain(String name, Locale locale, int id) throws JsonException { 417 if (trainManager().getTrainById(name) == null) { 418 log.error("Unable to get train id [{}].", name); 419 throw new JsonException(404, 420 Bundle.getMessage(locale, JsonException.ERROR_OBJECT, JsonOperations.TRAIN, name), id); 421 } 422 return getTrain(trainManager().getTrainById(name), locale); 423 } 424 425 /** 426 * Get all trains. 427 * 428 * @param locale the client's locale 429 * @return an array of all trains 430 */ 431 public ArrayNode getTrains(Locale locale) { 432 ArrayNode array = this.mapper.createArrayNode(); 433 trainManager().getTrainsByNameList() 434 .forEach(train -> array.add(getTrain(train, locale))); 435 return array; 436 } 437 438 private ArrayNode getCarsForTrain(Train train, Locale locale) { 439 ArrayNode array = mapper.createArrayNode(); 440 carManager().getByTrainDestinationList(train) 441 .forEach(car -> array.add(getCar(car, locale))); 442 return array; 443 } 444 445 private ArrayNode getEnginesForTrain(Train train, Locale locale) { 446 ArrayNode array = mapper.createArrayNode(); 447 engineManager().getByTrainBlockingList(train) 448 .forEach(engine -> array.add(getEngine(engine, locale))); 449 return array; 450 } 451 452 private ArrayNode getRouteLocationsForTrain(Train train, Locale locale) { 453 ArrayNode array = mapper.createArrayNode(); 454 train.getRoute().getLocationsBySequenceList().forEach(route -> { 455 ObjectNode root = mapper.createObjectNode(); 456 RouteLocation rl = route; 457 root.put(JSON.NAME, rl.getId()); 458 root.put(JSON.USERNAME, rl.getName()); 459 root.put(JSON.TRAIN_DIRECTION, rl.getTrainDirectionString()); 460 root.put(JSON.COMMENT, rl.getCommentWithColor()); 461 root.put(JSON.SEQUENCE, rl.getSequenceNumber()); 462 root.put(JSON.EXPECTED_ARRIVAL, train.getExpectedArrivalTime(rl)); 463 root.put(JSON.EXPECTED_DEPARTURE, train.getExpectedDepartureTime(rl)); 464 root.set(JSON.LOCATION, getRSLocation(rl.getLocation(), locale)); 465 array.add(root); 466 }); 467 return array; 468 } 469 470 private CarManager carManager() { 471 return InstanceManager.getDefault(CarManager.class); 472 } 473 474 private EngineManager engineManager() { 475 return InstanceManager.getDefault(EngineManager.class); 476 } 477 478 private LocationManager locationManager() { 479 return InstanceManager.getDefault(LocationManager.class); 480 } 481 482 private TrainManager trainManager() { 483 return InstanceManager.getDefault(TrainManager.class); 484 } 485}