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