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 if (car.getFinalDestinationTrack() != null) { 147 data.set(JsonOperations.FINAL_DESTINATION, this.getRSLocationAndTrack(car.getFinalDestinationTrack(), null, locale)); 148 } else if (car.getFinalDestination() != null) { 149 data.set(JsonOperations.FINAL_DESTINATION, 150 this.getRSLocation(car.getFinalDestination(), (RouteLocation) null, locale)); 151 } else { 152 data.set(JsonOperations.FINAL_DESTINATION, null); 153 } 154 if (car.getReturnWhenEmptyDestTrack() != null) { 155 data.set(JsonOperations.RETURN_WHEN_EMPTY, 156 this.getRSLocationAndTrack(car.getReturnWhenEmptyDestTrack(), null, locale)); 157 } else if (car.getReturnWhenEmptyDestination() != null) { 158 data.set(JsonOperations.RETURN_WHEN_EMPTY, 159 this.getRSLocation(car.getReturnWhenEmptyDestination(), (RouteLocation) null, locale)); 160 } else { 161 data.set(JsonOperations.RETURN_WHEN_EMPTY, null); 162 } 163 if (car.getReturnWhenLoadedDestTrack() != null) { 164 data.set(JsonOperations.RETURN_WHEN_LOADED, 165 this.getRSLocationAndTrack(car.getReturnWhenLoadedDestTrack(), null, locale)); 166 } else if (car.getReturnWhenLoadedDestination() != null) { 167 data.set(JsonOperations.RETURN_WHEN_LOADED, 168 this.getRSLocation(car.getReturnWhenLoadedDestination(), (RouteLocation) null, locale)); 169 } else { 170 data.set(JsonOperations.RETURN_WHEN_LOADED, null); 171 } 172 data.put(JsonOperations.DIVISION, car.getDivisionName()); 173 data.put(JsonOperations.BLOCKING_ORDER, car.isPassenger() ? Integer.toString(car.getBlocking()) : ""); 174 data.put(JSON.STATUS, car.getStatus().replace("<", "<").replace(">", ">")); 175 return data; 176 } 177 178 /** 179 * Get the JSON representation of a Location. 180 * <p> 181 * <strong>Note:</strong>use {@link #getRSLocation(Location, Locale)} if 182 * including in rolling stock or train. 183 * 184 * @param location the location 185 * @param locale the client's locale 186 * @return the JSON representation of location 187 */ 188 public ObjectNode getLocation(@Nonnull Location location, Locale locale) { 189 ObjectNode data = mapper.createObjectNode(); 190 data.put(JSON.USERNAME, location.getName()); 191 data.put(JSON.NAME, location.getId()); 192 data.put(JSON.LENGTH, location.getLength()); 193 data.put(JSON.COMMENT, location.getCommentWithColor()); 194 Reporter reporter = location.getReporter(); 195 data.put(REPORTER, reporter != null ? reporter.getSystemName() : ""); 196 // note type defaults to all in-use rolling stock types 197 ArrayNode types = data.putArray(JsonOperations.CAR_TYPE); 198 for (String type : location.getTypeNames()) { 199 types.add(type); 200 } 201 ArrayNode tracks = data.putArray(JSON.TRACK); 202 for (Track track : location.getTracksList()) { 203 tracks.add(getTrack(track, locale)); 204 } 205 return data; 206 } 207 208 /** 209 * Get the JSON representation of a Location. 210 * 211 * @param name the ID of the location 212 * @param locale the client's locale 213 * @param id the message id set by the client 214 * @return the JSON representation of the location 215 * @throws JsonException if id does not match a known location 216 */ 217 public ObjectNode getLocation(String name, Locale locale, int id) throws JsonException { 218 if (locationManager().getLocationById(name) == null) { 219 log.error("Unable to get location id [{}].", name); 220 throw new JsonException(404, 221 Bundle.getMessage(locale, JsonException.ERROR_OBJECT, JSON.LOCATION, name), id); 222 } 223 return getLocation(locationManager().getLocationById(name), locale); 224 } 225 226 /** 227 * Get a Track in JSON. 228 * <p> 229 * <strong>Note:</strong>use {@link #getRSTrack(Track, Locale)} if including 230 * in rolling stock or train. 231 * 232 * @param track the track to get 233 * @param locale the client's locale 234 * @return a JSON representation of the track 235 */ 236 public ObjectNode getTrack(Track track, Locale locale) { 237 ObjectNode node = mapper.createObjectNode(); 238 node.put(JSON.USERNAME, track.getName()); 239 node.put(JSON.NAME, track.getId()); 240 node.put(JSON.COMMENT, track.getComment()); 241 node.put(JSON.LENGTH, track.getLength()); 242 // only includes location ID to avoid recursion 243 node.put(JSON.LOCATION, track.getLocation().getId()); 244 Reporter reporter = track.getReporter(); 245 node.put(REPORTER, reporter != null ? reporter.getSystemName() : ""); 246 node.put(JSON.TYPE, track.getTrackType()); 247 // note type defaults to all in-use rolling stock types 248 ArrayNode types = node.putArray(JsonOperations.CAR_TYPE); 249 for (String type : track.getTypeNames()) { 250 types.add(type); 251 } 252 return node; 253 } 254 255 /** 256 * Get the JSON representation of a Location for use in rolling stock or 257 * train. 258 * <p> 259 * <strong>Note:</strong>use {@link #getLocation(Location, Locale)} if not 260 * including in rolling stock or train. 261 * 262 * @param location the location 263 * @param locale the client's locale 264 * @return the JSON representation of location 265 */ 266 public ObjectNode getRSLocation(@Nonnull Location location, Locale locale) { 267 ObjectNode data = mapper.createObjectNode(); 268 data.put(JSON.USERNAME, location.getName()); 269 data.put(JSON.NAME, location.getId()); 270 return data; 271 } 272 273 private ObjectNode getRSLocation(Location location, RouteLocation routeLocation, Locale locale) { 274 ObjectNode node = getRSLocation(location, locale); 275 if (routeLocation != null) { 276 node.put(JSON.ROUTE, routeLocation.getId()); 277 } else { 278 node.put(JSON.ROUTE, (String) null); 279 } 280 return node; 281 } 282 283 private ObjectNode getRSLocationAndTrack(Track track, RouteLocation routeLocation, Locale locale) { 284 ObjectNode node = this.getRSLocation(track.getLocation(), routeLocation, locale); 285 node.set(JSON.TRACK, this.getRSTrack(track, locale)); 286 return node; 287 } 288 289 /** 290 * Get a Track in JSON for use in rolling stock or train. 291 * <p> 292 * <strong>Note:</strong>use {@link #getTrack(Track, Locale)} if not 293 * including in rolling stock or train. 294 * 295 * @param track the track to get 296 * @param locale the client's locale 297 * @return a JSON representation of the track 298 */ 299 public ObjectNode getRSTrack(Track track, Locale locale) { 300 ObjectNode node = mapper.createObjectNode(); 301 node.put(JSON.USERNAME, track.getName()); 302 node.put(JSON.NAME, track.getId()); 303 return node; 304 } 305 306 public ObjectNode getRollingStock(@Nonnull RollingStock rs, Locale locale) { 307 ObjectNode node = mapper.createObjectNode(); 308 node.put(JSON.NAME, rs.getId()); 309 node.put(JsonOperations.NUMBER, TrainCommon.splitString(rs.getNumber())); 310 node.put(JsonOperations.ROAD, rs.getRoadName().split(TrainCommon.HYPHEN)[0]); 311 node.put(JSON.RFID, rs.getRfid()); 312 if (!rs.getWhereLastSeenName().equals(Car.NONE)) { 313 node.put(JSON.WHERELASTSEEN, rs.getWhereLastSeenName() + 314 (rs.getTrackLastSeenName().equals(Car.NONE) ? "" : " (" + rs.getTrackLastSeenName() + ")")); 315 } else { 316 node.set(JSON.WHERELASTSEEN, null); 317 } 318 if (!rs.getWhenLastSeenDate().equals(Car.NONE)) { 319 node.put(JSON.WHENLASTSEEN, rs.getWhenLastSeenDate()); 320 } else { 321 node.set(JSON.WHENLASTSEEN, null); 322 } 323 // second half of string can be anything 324 String[] type = rs.getTypeName().split(TrainCommon.HYPHEN, 2); 325 node.put(JsonOperations.TYPE, type[0]); 326 node.put(JsonOperations.CAR_SUB_TYPE, type.length == 2 ? type[1] : ""); 327 node.put(JsonOperations.LENGTH, rs.getLengthInteger()); 328 node.put(JsonOperations.WEIGHT, rs.getAdjustedWeightTons()); 329 node.put(JsonOperations.WEIGHT_TONS, rs.getWeightTons()); 330 node.put(JsonOperations.COLOR, rs.getColor()); 331 node.put(JsonOperations.OWNER, rs.getOwnerName()); 332 node.put(JsonOperations.BUILT, rs.getBuilt()); 333 node.put(JsonOperations.COMMENT, rs.getComment()); 334 node.put(JsonOperations.OUT_OF_SERVICE, rs.isOutOfService()); 335 node.put(JsonOperations.LOCATION_UNKNOWN, rs.isLocationUnknown()); 336 if (rs.getTrack() != null) { 337 node.set(JsonOperations.LOCATION, this.getRSLocationAndTrack(rs.getTrack(), rs.getRouteLocation(), locale)); 338 } else if (rs.getLocation() != null) { 339 node.set(JsonOperations.LOCATION, this.getRSLocation(rs.getLocation(), rs.getRouteLocation(), locale)); 340 } else { 341 node.set(JsonOperations.LOCATION, null); 342 } 343 if (rs.getTrain() != null) { 344 node.put(JsonOperations.TRAIN_ID, rs.getTrain().getId()); 345 node.put(JsonOperations.TRAIN_NAME, rs.getTrain().getName()); 346 node.put(JsonOperations.TRAIN_ICON_NAME, rs.getTrain().getIconName()); 347 } else { 348 node.set(JsonOperations.TRAIN_ID, null); 349 node.set(JsonOperations.TRAIN_NAME, null); 350 node.set(JsonOperations.TRAIN_ICON_NAME, null); 351 } 352 if (rs.getDestinationTrack() != null) { 353 node.set(JsonOperations.DESTINATION, 354 this.getRSLocationAndTrack(rs.getDestinationTrack(), rs.getRouteDestination(), locale)); 355 } else if (rs.getDestination() != null) { 356 node.set(JsonOperations.DESTINATION, this.getRSLocation(rs.getDestination(), rs.getRouteDestination(), locale)); 357 } else { 358 node.set(JsonOperations.DESTINATION, null); 359 } 360 return node; 361 } 362 363 /** 364 * Get the JSON representation of a Train. 365 * 366 * @param train the train 367 * @param locale the client's locale 368 * @return the JSON representation of train 369 */ 370 public ObjectNode getTrain(Train train, Locale locale) { 371 ObjectNode data = this.mapper.createObjectNode(); 372 data.put(JSON.USERNAME, train.getName()); 373 data.put(JSON.ICON_NAME, train.getIconName()); 374 data.put(JSON.NAME, train.getId()); 375 data.put(JSON.DEPARTURE_TIME, train.getFormatedDepartureTime()); 376 data.put(JSON.DESCRIPTION, train.getDescription()); 377 data.put(JSON.COMMENT, train.getComment()); 378 if (train.getRoute() != null) { 379 data.put(JSON.ROUTE, train.getRoute().getName()); 380 data.put(JSON.ROUTE_ID, train.getRoute().getId()); 381 data.set(JSON.LOCATIONS, this.getRouteLocationsForTrain(train, locale)); 382 } 383 data.set(JSON.ENGINES, this.getEnginesForTrain(train, locale)); 384 data.set(JsonOperations.CARS, this.getCarsForTrain(train, locale)); 385 if (train.getTrainDepartsName() != null) { 386 data.put(JSON.DEPARTURE_LOCATION, train.getTrainDepartsName()); 387 } 388 if (train.getTrainTerminatesName() != null) { 389 data.put(JSON.TERMINATES_LOCATION, train.getTrainTerminatesName()); 390 } 391 data.put(JSON.LOCATION, train.getCurrentLocationName()); 392 if (train.getCurrentRouteLocation() != null) { 393 data.put(JsonOperations.LOCATION_ID, train.getCurrentRouteLocation().getId()); 394 } 395 data.put(JSON.STATUS, train.getStatus(locale)); 396 data.put(JSON.STATUS_CODE, train.getStatusCode()); 397 data.put(JSON.LENGTH, train.getTrainLength()); 398 data.put(JSON.WEIGHT, train.getTrainWeight()); 399 if (train.getLeadEngine() != null) { 400 data.put(JsonOperations.LEAD_ENGINE, train.getLeadEngine().toString()); 401 } 402 data.put(JsonOperations.CABOOSE, train.getCabooseRoadAndNumber()); 403 return data; 404 } 405 406 /** 407 * Get the JSON representation of a Train. 408 * 409 * @param name the id of the train 410 * @param locale the client's locale 411 * @param id the message id set by the client 412 * @return the JSON representation of the train with id 413 * @throws JsonException if id does not represent a known train 414 */ 415 public ObjectNode getTrain(String name, Locale locale, int id) throws JsonException { 416 if (trainManager().getTrainById(name) == null) { 417 log.error("Unable to get train id [{}].", name); 418 throw new JsonException(404, 419 Bundle.getMessage(locale, JsonException.ERROR_OBJECT, JsonOperations.TRAIN, name), id); 420 } 421 return getTrain(trainManager().getTrainById(name), locale); 422 } 423 424 /** 425 * Get all trains. 426 * 427 * @param locale the client's locale 428 * @return an array of all trains 429 */ 430 public ArrayNode getTrains(Locale locale) { 431 ArrayNode array = this.mapper.createArrayNode(); 432 trainManager().getTrainsByNameList() 433 .forEach(train -> array.add(getTrain(train, locale))); 434 return array; 435 } 436 437 private ArrayNode getCarsForTrain(Train train, Locale locale) { 438 ArrayNode array = mapper.createArrayNode(); 439 carManager().getByTrainDestinationList(train) 440 .forEach(car -> array.add(getCar(car, locale))); 441 return array; 442 } 443 444 private ArrayNode getEnginesForTrain(Train train, Locale locale) { 445 ArrayNode array = mapper.createArrayNode(); 446 engineManager().getByTrainBlockingList(train) 447 .forEach(engine -> array.add(getEngine(engine, locale))); 448 return array; 449 } 450 451 private ArrayNode getRouteLocationsForTrain(Train train, Locale locale) { 452 ArrayNode array = mapper.createArrayNode(); 453 train.getRoute().getLocationsBySequenceList().forEach(route -> { 454 ObjectNode root = mapper.createObjectNode(); 455 RouteLocation rl = route; 456 root.put(JSON.NAME, rl.getId()); 457 root.put(JSON.USERNAME, rl.getName()); 458 root.put(JSON.TRAIN_DIRECTION, rl.getTrainDirectionString()); 459 root.put(JSON.COMMENT, rl.getCommentWithColor()); 460 root.put(JSON.SEQUENCE, rl.getSequenceNumber()); 461 root.put(JSON.EXPECTED_ARRIVAL, train.getExpectedArrivalTime(rl)); 462 root.put(JSON.EXPECTED_DEPARTURE, train.getExpectedDepartureTime(rl)); 463 root.set(JSON.LOCATION, getRSLocation(rl.getLocation(), locale)); 464 array.add(root); 465 }); 466 return array; 467 } 468 469 private CarManager carManager() { 470 return InstanceManager.getDefault(CarManager.class); 471 } 472 473 private EngineManager engineManager() { 474 return InstanceManager.getDefault(EngineManager.class); 475 } 476 477 private LocationManager locationManager() { 478 return InstanceManager.getDefault(LocationManager.class); 479 } 480 481 private TrainManager trainManager() { 482 return InstanceManager.getDefault(TrainManager.class); 483 } 484}