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