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("<", "&lt;").replace(">", "&gt;"));
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}