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}