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