001package jmri.web.servlet.operations;
002
003import com.fasterxml.jackson.databind.JsonNode;
004import com.fasterxml.jackson.databind.ObjectMapper;
005
006import java.io.IOException;
007import java.util.*;
008import java.util.Map.Entry;
009
010import jmri.InstanceManager;
011import jmri.jmrit.operations.rollingstock.cars.Car;
012import jmri.jmrit.operations.rollingstock.cars.CarManager;
013import jmri.jmrit.operations.routes.RouteLocation;
014import jmri.jmrit.operations.setup.Setup;
015import jmri.jmrit.operations.trains.*;
016import jmri.jmrit.operations.trains.schedules.TrainScheduleManager;
017import jmri.server.json.JSON;
018import jmri.server.json.operations.JsonOperations;
019
020import org.apache.commons.text.StringEscapeUtils;
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024/**
025 *
026 * @author Randall Wood
027 */
028public class HtmlManifest extends HtmlTrainCommon {
029
030    protected ObjectMapper mapper;
031    private JsonNode jsonManifest = null;
032    private static final Logger log = LoggerFactory.getLogger(HtmlManifest.class);
033
034    public HtmlManifest(Locale locale, Train train) throws IOException {
035        super(locale, train);
036        this.mapper = new ObjectMapper();
037        this.resourcePrefix = "Manifest";
038    }
039
040    // TODO cache the results so a quick check that if the JsonManifest file is not
041    // newer than the Html manifest, the cached copy is returned instead.
042    public String getLocations() throws IOException {
043        // build manifest from JSON manifest
044        if (this.getJsonManifest() == null) {
045            return "Error manifest file not found for this train";
046        }
047        StringBuilder builder = new StringBuilder();
048        JsonNode locations = this.getJsonManifest().path(JSON.LOCATIONS);
049        String previousLocationName = null;
050        boolean hasWork;
051        for (JsonNode location : locations) {
052            RouteLocation routeLocation = train.getRoute().getRouteLocationById(location.path(JSON.NAME).textValue());
053            log.debug("Processing {} ({})", routeLocation.getName(), location.path(JSON.NAME).textValue());
054            String routeLocationName = location.path(JSON.USERNAME).textValue();
055            builder.append(String.format(locale, strings.getProperty("LocationStart"), routeLocation.getId())); // NOI18N
056            hasWork = (location.path(JsonOperations.CARS).path(JSON.ADD).size() > 0
057                    || location.path(JsonOperations.CARS).path(JSON.REMOVE).size() > 0
058                    || location.path(JSON.ENGINES).path(JSON.ADD).size() > 0 || location.path(JSON.ENGINES).path(
059                            JSON.REMOVE).size() > 0);
060            if (hasWork && !routeLocationName.equals(previousLocationName)) {
061                if (!train.isShowArrivalAndDepartureTimesEnabled()) {
062                    builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
063                } else if (routeLocation == train.getTrainDepartsRouteLocation()) {
064                    builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,
065                            train.getFormatedDepartureTime())); // NOI18N
066                } else if (!routeLocation.getDepartureTimeHourMinutes().equals(RouteLocation.NONE)) {
067                    builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,
068                            routeLocation.getFormatedDepartureTime())); // NOI18N
069                } else if (Setup.isUseDepartureTimeEnabled()
070                        && routeLocation != train.getTrainTerminatesRouteLocation()) {
071                    builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,
072                            train.getExpectedDepartureTime(routeLocation))); // NOI18N
073                } else if (!train.getExpectedArrivalTime(routeLocation).equals(Train.ALREADY_SERVICED)) { // NOI18N
074                    builder.append(String.format(locale, strings.getProperty("WorkArrivalTime"), routeLocationName,
075                            train.getExpectedArrivalTime(routeLocation))); // NOI18N
076                } else {
077                    builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
078                }
079                // add route comment
080                if (!location.path(JSON.COMMENT).textValue().isBlank()) {
081                    builder.append(String.format(locale, strings.getProperty("RouteLocationComment"),
082                            location.path(JSON.COMMENT).textValue()));
083                }
084
085                // add location comment
086                if (Setup.isPrintLocationCommentsEnabled()
087                        && !location.path(JSON.LOCATION).path(JSON.COMMENT).textValue().isBlank()) {
088                    builder.append(String.format(locale, strings.getProperty("LocationComment"), location.path(
089                            JSON.LOCATION).path(JSON.COMMENT).textValue()));
090                }
091
092                // add track comments
093                builder.append(
094                        getTrackComments(location.path(JSON.TRACK), location.path(JsonOperations.CARS)));
095            }
096
097            previousLocationName = routeLocationName;
098
099            // engine change or helper service?
100            if (location.path(JSON.OPTIONS).size() > 0) {
101                boolean changeEngines = false;
102                boolean changeCaboose = false;
103                for (JsonNode option : location.path(JSON.OPTIONS)) {
104                    switch (option.asText()) {
105                        case JSON.CHANGE_ENGINES:
106                            changeEngines = true;
107                            break;
108                        case JSON.CHANGE_CABOOSE:
109                            changeCaboose = true;
110                            break;
111                        case JSON.ADD_HELPERS:
112                            builder.append(String.format(strings.getProperty("AddHelpersAt"), routeLocationName));
113                            break;
114                        case JSON.REMOVE_HELPERS:
115                            builder.append(String.format(strings.getProperty("RemoveHelpersAt"), routeLocationName));
116                            break;
117                        default:
118                            break;
119                    }
120                }
121                if (changeEngines && changeCaboose) {
122                    builder.append(String.format(strings.getProperty("LocoAndCabooseChangeAt"), routeLocationName)); // NOI18N
123                } else if (changeEngines) {
124                    builder.append(String.format(strings.getProperty("LocoChangeAt"), routeLocationName)); // NOI18N
125                } else if (changeCaboose) {
126                    builder.append(String.format(strings.getProperty("CabooseChangeAt"), routeLocationName)); // NOI18N
127                }
128            }
129
130            builder.append(pickupEngines(location.path(JSON.ENGINES).path(JSON.ADD)));
131            builder.append(blockCars(location.path(JsonOperations.CARS), routeLocation, true));
132            builder.append(dropEngines(location.path(JSON.ENGINES).path(JSON.REMOVE)));
133
134            if (routeLocation != train.getTrainTerminatesRouteLocation()) {
135                // Is the next location the same as the current?
136                RouteLocation rlNext = train.getRoute().getNextRouteLocation(routeLocation);
137                if (!routeLocationName.equals(rlNext.getSplitName())) {
138                    if (hasWork) {
139                        if (!Setup.isPrintLoadsAndEmptiesEnabled()) {
140                            // Message format: Train departs Boston Westbound with 12 cars, 450 feet, 3000 tons
141                            builder.append(String.format(strings.getProperty("TrainDepartsCars"), routeLocationName,
142                                    strings.getProperty("Heading"
143                                            + Setup.getDirectionString(location.path(JSON.TRAIN_DIRECTION).intValue())),
144                                    location.path(JSON.LENGTH).path(JSON.LENGTH).intValue(), location.path(JSON.LENGTH)
145                                    .path(JSON.UNIT).asText().toLowerCase(), location.path(JSON.WEIGHT)
146                                    .intValue(), location.path(JsonOperations.CARS).path(JSON.TOTAL).intValue()));
147                        } else {
148                            // Message format: Train departs Boston Westbound with 4 loads, 8 empties, 450 feet, 3000
149                            // tons
150                            builder.append(String.format(strings.getProperty("TrainDepartsLoads"), routeLocationName,
151                                    strings.getProperty("Heading"
152                                            + Setup.getDirectionString(location.path(JSON.TRAIN_DIRECTION).intValue())),
153                                    location.path(JSON.LENGTH).path(JSON.LENGTH).intValue(), location.path(JSON.LENGTH)
154                                    .path(JSON.UNIT).asText().toLowerCase(), location.path(JSON.WEIGHT)
155                                    .intValue(), location.path(JsonOperations.CARS).path(JSON.LOADS).intValue(), location
156                                    .path(JsonOperations.CARS).path(JSON.EMPTIES).intValue()));
157                        }
158                    } else {
159                        log.debug("No work ({})", routeLocation.getComment());
160                        if (routeLocation.getComment().isBlank()) {
161                            // no route comment, no work at this location
162                            if (train.isShowArrivalAndDepartureTimesEnabled()) {
163                                if (routeLocation == train.getTrainDepartsRouteLocation()) {
164                                    builder.append(String.format(locale, strings
165                                            .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName,
166                                            train.getFormatedDepartureTime()));
167                                } else if (!routeLocation.getDepartureTimeHourMinutes().isEmpty()) {
168                                    builder.append(String.format(locale, strings
169                                            .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName,
170                                            routeLocation.getFormatedDepartureTime()));
171                                } else if (Setup.isUseDepartureTimeEnabled()) {
172                                    builder.append(String.format(locale, strings
173                                            .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName,
174                                            location.path(JSON.EXPECTED_DEPARTURE)));
175                                } else { // fall back to generic no scheduled work message
176                                    builder.append(String.format(locale, strings.getProperty("NoScheduledWorkAt"),
177                                            routeLocationName));
178                                }
179                            } else {
180                                builder.append(String.format(locale, strings.getProperty("NoScheduledWorkAt"),
181                                        routeLocationName));
182                            }
183                        } else {
184                            // if a route comment, then only use location name and route comment, useful for passenger
185                            // trains
186                            if (!routeLocation.getComment().isBlank()) {
187                                builder.append(String.format(locale, strings.getProperty("CommentAt"), // NOI18N
188                                        routeLocationName, StringEscapeUtils
189                                                .escapeHtml4(routeLocation.getCommentWithColor())));
190                            }
191                            if (train.isShowArrivalAndDepartureTimesEnabled()) {
192                                if (routeLocation == train.getTrainDepartsRouteLocation()) {
193                                    builder.append(String.format(locale, strings
194                                            .getProperty("CommentAtWithDepartureTime"), routeLocationName, train // NOI18N
195                                            .getFormatedDepartureTime(), StringEscapeUtils
196                                            .escapeHtml4(routeLocation.getComment())));
197                                } else if (!routeLocation.getDepartureTimeHourMinutes().equals(RouteLocation.NONE)) {
198                                    builder.append(String.format(locale, strings
199                                            .getProperty("CommentAtWithDepartureTime"), routeLocationName, // NOI18N
200                                            routeLocation.getFormatedDepartureTime(), StringEscapeUtils
201                                            .escapeHtml4(routeLocation.getComment())));
202                                } else if (Setup.isUseDepartureTimeEnabled() &&
203                                        !routeLocation.getComment().equals(RouteLocation.NONE)) {
204                                    builder.append(String.format(locale, strings
205                                            .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName, // NOI18N
206                                            train.getExpectedDepartureTime(routeLocation)));
207                                }
208                            }
209                        }
210                        // add location comment
211                        if (Setup.isPrintLocationCommentsEnabled()
212                                && !routeLocation.getLocation().getComment().isEmpty()) {
213                            builder.append(String.format(locale, strings.getProperty("LocationComment"),
214                                    StringEscapeUtils.escapeHtml4(routeLocation.getLocation().getCommentWithColor())));
215                        }
216                    }
217                }
218            } else {
219                builder.append(String.format(strings.getProperty("TrainTerminatesIn"), routeLocationName));
220            }
221        }
222        return builder.toString();
223    }
224
225    protected String blockCars(JsonNode cars, RouteLocation location, boolean isManifest) {
226        StringBuilder builder = new StringBuilder();
227        log.debug("Cars is {}", cars);
228
229        //copy the adds into a sortable arraylist
230        ArrayList<JsonNode> adds = new ArrayList<JsonNode>();
231        cars.path(JSON.ADD).forEach(adds::add);
232
233        //sort if requested
234        if (adds.size() > 0 && Setup.isSortByTrackNameEnabled()) {
235            adds.sort(Comparator.comparing(o -> o.path("location").path("track").path("userName").asText()));
236        }
237        //format each car for output
238        // use truncated format if there's a switch list
239        for (JsonNode car : adds) {
240            if (!this.isLocalMove(car)) {
241                if (this.isUtilityCar(car)) {
242                    builder.append(pickupUtilityCars(adds, car, location, isManifest));
243                } else {
244                    String[] messageFormat;
245                    if (isManifest &&
246                            Setup.isPrintTruncateManifestEnabled() &&
247                            location.getLocation().isSwitchListEnabled()) {
248                        messageFormat = Setup.getPickupTruncatedManifestMessageFormat();
249                    } else {
250                        messageFormat = Setup.getPickupManifestMessageFormat();
251                    }
252                    Setup.stringToTagConversion(messageFormat);
253                    builder.append(pickUpCar(car, messageFormat));
254                }
255            }
256        }
257
258        //copy the drops into a sortable arraylist
259        ArrayList<JsonNode> drops = new ArrayList<JsonNode>();
260        cars.path(JSON.REMOVE).forEach(drops::add);
261
262        for (JsonNode car : drops) {
263            boolean local = isLocalMove(car);
264            if (this.isUtilityCar(car)) {
265                builder.append(setoutUtilityCars(drops, car, location, isManifest));
266            } else if (isManifest &&
267                    Setup.isPrintTruncateManifestEnabled() &&
268                    location.getLocation().isSwitchListEnabled() &&
269                    !train.isLocalSwitcher()) {
270                builder.append(dropCar(car, Setup.getDropTruncatedManifestMessageFormat(), local));
271            } else {
272                String[] format;
273                if (isManifest) {
274                    format = (!local) ? Setup.getDropManifestMessageFormat() : Setup
275                            .getLocalManifestMessageFormat();
276                } else {
277                    format = (!local) ? Setup.getDropSwitchListMessageFormat() : Setup
278                            .getLocalSwitchListMessageFormat();
279                }
280                Setup.stringToTagConversion(format);
281                builder.append(dropCar(car, format, local));
282            }
283        }
284        return String.format(locale, strings.getProperty("CarsList"), builder.toString());
285    }
286
287    protected String pickupUtilityCars(ArrayList<JsonNode> jnCars, JsonNode jnCar, RouteLocation location,
288            boolean isManifest) {
289        List<Car> cars = getCarList(jnCars);
290        Car car = getCar(jnCar);
291        return pickupUtilityCars(cars, car, isManifest);
292    }
293
294    protected String setoutUtilityCars(ArrayList<JsonNode> jnCars, JsonNode jnCar, RouteLocation location,
295            boolean isManifest) {
296        List<Car> cars = getCarList(jnCars);
297        Car car = getCar(jnCar);
298        return setoutUtilityCars(cars, car, isManifest);
299    }
300
301    protected List<Car> getCarList(ArrayList<JsonNode> jnCars) {
302        List<Car> cars = new ArrayList<>();
303        for (JsonNode kar : jnCars) {
304            cars.add(getCar(kar));
305        }
306        return cars;
307    }
308
309    protected Car getCar(JsonNode jnCar) {
310        String id = jnCar.path(JSON.NAME).asText();
311        Car car = InstanceManager.getDefault(CarManager.class).getById(id);
312        return car;
313    }
314
315    protected String pickUpCar(JsonNode car, String[] format) {
316        if (isLocalMove(car)) {
317            return ""; // print nothing for local move, see dropCar()
318        }
319        StringBuilder builder = new StringBuilder();
320        builder.append("<span style=\"color: " + Setup.getPickupTextColor() + ";\">");
321        builder.append(Setup.getPickupCarPrefix()).append(" ");
322        for (String attribute : format) {
323            if (!attribute.trim().isEmpty()) {
324                attribute = attribute.toLowerCase();
325                log.trace("Adding car with attribute {}", attribute);
326                if (attribute.equals(JsonOperations.LOCATION) || attribute.equals(JsonOperations.TRACK)) {
327                    attribute = JsonOperations.LOCATION; // treat "track" as "location"
328                    builder.append(
329                            this.getFormattedAttribute(attribute, this.getPickupLocation(car.path(attribute),
330                                            ShowLocation.track))).append(" "); // NOI18N
331                } else if (attribute.equals(JsonOperations.DESTINATION)) {
332                    builder.append(
333                            this.getFormattedAttribute(attribute, this.getDropLocation(car.path(attribute),
334                                            ShowLocation.location))).append(" "); // NOI18N
335                } else if (attribute.equals(JsonOperations.DESTINATION_TRACK)) {
336                    builder.append(
337                            this.getFormattedAttribute(attribute, this.getDropLocation(car.path(JsonOperations.DESTINATION),
338                                            ShowLocation.both))).append(" "); // NOI18N
339                } else {
340                    builder.append(this.getTextAttribute(attribute, car)).append(" "); // NOI18N
341                }
342            }
343        }
344        log.debug("Picking up car {}", builder);
345        return String.format(locale, strings.getProperty(this.resourcePrefix + "PickUpCar"), builder.toString()); // NOI18N
346    }
347
348    protected String dropCar(JsonNode car, String[] format, boolean isLocal) {
349        StringBuilder builder = new StringBuilder();
350        if (!isLocal) {
351            builder.append("<span style=\"color: " + Setup.getDropTextColor() + ";\">");
352            builder.append(Setup.getDropCarPrefix()).append(" ");
353        } else {
354            builder.append("<span style=\"color: " + Setup.getLocalTextColor() + ";\">");
355            builder.append(Setup.getLocalPrefix()).append(" ");
356        }
357        log.debug("dropCar {}", car);
358        for (String attribute : format) {
359            if (!attribute.trim().isEmpty()) {
360                attribute = attribute.toLowerCase();
361                log.trace("Removing car with attribute {}", attribute);
362                if (attribute.equals(JsonOperations.DESTINATION) || attribute.equals(JsonOperations.TRACK)) {
363                    attribute = JsonOperations.DESTINATION; // treat "track" as "destination"
364                    builder.append(
365                            this.getFormattedAttribute(attribute, this.getDropLocation(car.path(attribute),
366                                            ShowLocation.track))).append(" "); // NOI18N
367                } else if (attribute.equals(JsonOperations.DESTINATION_TRACK)) {
368                    builder.append(
369                            this.getFormattedAttribute(attribute, this.getDropLocation(car.path(JsonOperations.DESTINATION),
370                                            ShowLocation.both))).append(" "); // NOI18N
371                } else if (attribute.equals(JsonOperations.LOCATION) && isLocal) {
372                    builder.append(
373                            this.getFormattedAttribute(attribute, this.getPickupLocation(car.path(attribute),
374                                            ShowLocation.track))).append(" "); // NOI18N
375                } else if (attribute.equals(JsonOperations.LOCATION)) {
376                    builder.append(
377                            this.getFormattedAttribute(attribute, this.getPickupLocation(car.path(attribute),
378                                            ShowLocation.location))).append(" "); // NOI18N
379                } else {
380                    builder.append(this.getTextAttribute(attribute, car)).append(" "); // NOI18N
381                }
382            }
383        }
384        log.debug("Dropping {}car {}", (isLocal) ? "local " : "", builder);
385        if (!isLocal) {
386            return String.format(locale, strings.getProperty(this.resourcePrefix + "DropCar"), builder.toString()); // NOI18N
387        } else {
388            return String.format(locale, strings.getProperty(this.resourcePrefix + "LocalCar"), builder.toString()); // NOI18N
389        }
390    }
391
392    protected String dropEngines(JsonNode engines) {
393        StringBuilder builder = new StringBuilder();
394        engines.forEach((engine) -> {
395            builder.append(this.dropEngine(engine));
396        });
397        return String.format(locale, strings.getProperty("EnginesList"), builder.toString());
398    }
399
400    protected String dropEngine(JsonNode engine) {
401        StringBuilder builder = new StringBuilder();
402        builder.append("<span style=\"color: " + Setup.getDropEngineTextColor() + ";\">");
403        builder.append(Setup.getDropEnginePrefix()).append(" ");
404        String[] formatMessage = Setup.getDropEngineMessageFormat();
405        Setup.stringToTagConversion(formatMessage);
406        for (String attribute : formatMessage) {
407            if (!attribute.trim().isEmpty()) {
408                attribute = attribute.toLowerCase();
409                if (attribute.equals(JsonOperations.DESTINATION) || attribute.equals(JsonOperations.TRACK)) {
410                    attribute = JsonOperations.DESTINATION; // treat "track" as "destination"
411                    builder.append(
412                            this.getFormattedAttribute(attribute, this.getDropLocation(engine.path(attribute),
413                                    ShowLocation.track)))
414                            .append(" "); // NOI18N
415                } else if (attribute.equals(JsonOperations.LOCATION)) {
416                    builder.append(
417                            this.getFormattedAttribute(attribute, this.getPickupLocation(engine.path(attribute),
418                                    ShowLocation.location)))
419                            .append(" "); // NOI18N
420                } else {
421                    builder.append(this.getTextAttribute(attribute, engine)).append(" "); // NOI18N
422                }
423            }
424        }
425        log.debug("Drop engine: {}", builder);
426        return String.format(locale, strings.getProperty(this.resourcePrefix + "DropEngine"), builder.toString());
427    }
428
429    protected String pickupEngines(JsonNode engines) {
430        StringBuilder builder = new StringBuilder();
431        if (engines.size() > 0) {
432            for (JsonNode engine : engines) {
433                builder.append(this.pickupEngine(engine));
434            }
435        }
436        return String.format(locale, strings.getProperty("EnginesList"), builder.toString());
437    }
438
439    protected String pickupEngine(JsonNode engine) {
440        StringBuilder builder = new StringBuilder();
441        builder.append("<span style=\"color: " + Setup.getPickupEngineTextColor() + ";\">");
442        builder.append(Setup.getPickupEnginePrefix()).append(" ");
443        String[] messageFormat =  Setup.getPickupEngineMessageFormat();
444        Setup.stringToTagConversion(messageFormat);
445        log.debug("PickupEngineMessageFormat: {}", (Object) messageFormat);
446        for (String attribute : messageFormat) {
447            if (!attribute.trim().isEmpty()) {
448                attribute = attribute.toLowerCase();
449                if (attribute.equals(JsonOperations.LOCATION) || attribute.equals(JsonOperations.TRACK)) {
450                    attribute = JsonOperations.LOCATION; // treat "track" as "location"
451                    builder.append(
452                            this.getFormattedAttribute(attribute, this.getPickupLocation(engine.path(attribute),
453                                    ShowLocation.track)))
454                            .append(" "); // NOI18N
455                } else if (attribute.equals(JsonOperations.DESTINATION)) {
456                    builder.append(
457                            this.getFormattedAttribute(attribute, this.getDropLocation(engine.path(attribute),
458                                    ShowLocation.location)))
459                            .append(" "); // NOI18N
460                } else {
461                    builder.append(this.getTextAttribute(attribute, engine)).append(" "); // NOI18N
462                }
463            }
464        }
465        log.debug("Picking up engine: {}", builder);
466        return String.format(locale, strings.getProperty(this.resourcePrefix + "PickUpEngine"), builder.toString());
467    }
468
469    protected String getDropLocation(JsonNode location, ShowLocation show) {
470        return this.getFormattedLocation(location, show, "To"); // NOI18N
471    }
472
473    protected String getPickupLocation(JsonNode location, ShowLocation show) {
474        return this.getFormattedLocation(location, show, "From"); // NOI18N
475    }
476
477    protected String getTextAttribute(String attribute, JsonNode rollingStock) {
478        if (attribute.equals(JsonOperations.HAZARDOUS)) {
479            return this.getFormattedAttribute(attribute, (rollingStock.path(attribute).asBoolean() ? Setup
480                    .getHazardousMsg() : "")); // NOI18N
481        } else if (attribute.equals(JsonOperations.PICKUP_COMMENT)) {
482            return this.getFormattedAttribute(JsonOperations.PICKUP_COMMENT, rollingStock.path(JsonOperations.PICKUP_COMMENT).textValue());
483        } else if (attribute.equals(JsonOperations.SETOUT_COMMENT)) {
484            return this.getFormattedAttribute(JsonOperations.SETOUT_COMMENT, rollingStock.path(JsonOperations.SETOUT_COMMENT).textValue());
485        } else if (attribute.equals(JsonOperations.RETURN_WHEN_EMPTY)) {
486            return this.getFormattedLocation(rollingStock.path(JsonOperations.RETURN_WHEN_EMPTY), ShowLocation.both, "RWE"); // NOI18N
487        } else if (attribute.equals(JsonOperations.FINAL_DESTINATION)) {
488            return this.getFormattedLocation(rollingStock.path(JsonOperations.FINAL_DESTINATION), ShowLocation.location, "FinalDestination"); // NOI18N
489        } else if (attribute.equals(JsonOperations.FINAL_DEST_TRACK)) {
490            return this.getFormattedLocation(rollingStock.path(JsonOperations.FINAL_DESTINATION), ShowLocation.both, "FinalDestination"); // NOI18N
491        } else if (attribute.equals(JsonOperations.LAST_TRAIN)) {
492            return TrainManifestHeaderText.getStringHeader_Last_Train() +
493                    SPACE +
494                    getFormattedAttribute(attribute, rollingStock.path(attribute).asText());
495        } else if (attribute.equals(JsonOperations.LAST_LOCATION)) {
496            return TrainManifestHeaderText.getStringHeader_Last_Location() +
497                    SPACE +
498                    getFormattedAttribute(attribute, rollingStock.path(attribute).asText());
499        }
500
501        return this.getFormattedAttribute(attribute, rollingStock.path(attribute).asText());
502    }
503
504    protected String getFormattedAttribute(String attribute, String value) {
505        return String.format(locale, strings.getProperty("Attribute"), StringEscapeUtils.escapeHtml4(value), attribute);
506    }
507
508    protected String getFormattedLocation(JsonNode location, ShowLocation show, String prefix) {
509        if (location.isNull() || location.isEmpty()) {
510            // return an empty string if location is an empty or null
511            return "";
512        }
513        // TODO handle tracks without names
514        switch (show) {
515            case location:
516                return String.format(locale, strings.getProperty(prefix + "Location"),
517                        splitString(location.path(JSON.USERNAME).asText()));
518            case track:
519                return String.format(locale, strings.getProperty(prefix + "Track"),
520                        splitString(location.path(JSON.TRACK).path(JSON.USERNAME).asText()));
521            case both:
522            default: // default here ensures the method always returns
523                return String.format(locale, strings.getProperty(prefix + "LocationAndTrack"),
524                        splitString(location.path(JSON.USERNAME).asText()),
525                        splitString(location.path(JSON.TRACK).path(JSON.USERNAME).asText()));
526        }
527    }
528
529    private String getTrackComments(JsonNode tracks, JsonNode cars) {
530        StringBuilder builder = new StringBuilder();
531        if (tracks.size() > 0) {
532            Set<Entry<String, JsonNode>> properties = tracks.properties();
533            for (var track : properties) {
534                boolean pickup = false;
535                boolean setout = false;
536                if (cars.path(JSON.ADD).size() > 0) {
537                    for (JsonNode car : cars.path(JSON.ADD)) {
538                        if (track.getKey().equals(car.path(JSON.LOCATION).path(JSON.TRACK)
539                                .path(JSON.NAME).asText())) {
540                            pickup = true;
541                            break; // we do not need to iterate all cars
542                        }
543                    }
544                }
545                if (cars.path(JSON.REMOVE).size() > 0) {
546                    for (JsonNode car : cars.path(JSON.REMOVE)) {
547                        if (track.getKey().equals(car.path(JsonOperations.DESTINATION).path(JSON.TRACK)
548                                .path(JSON.NAME).textValue())) {
549                            setout = true;
550                            break; // we do not need to iterate all cars
551                        }
552                    }
553                }
554                if (pickup && setout) {
555                    builder.append(String.format(locale, strings.getProperty("TrackComments"), track.getValue().path(
556                            JSON.ADD_AND_REMOVE).textValue()));
557                } else if (pickup) {
558                    builder.append(String.format(locale, strings.getProperty("TrackComments"), track.getValue().path(
559                            JSON.ADD).textValue()));
560                } else if (setout) {
561                    builder.append(String.format(locale, strings.getProperty("TrackComments"), track.getValue().path(
562                            JSON.REMOVE).textValue()));
563                }
564            }
565        }
566        return builder.toString();
567    }
568
569    protected boolean isLocalMove(JsonNode car) {
570        return car.path(JsonOperations.IS_LOCAL).booleanValue();
571    }
572
573    protected boolean isUtilityCar(JsonNode car) {
574        return car.path(JsonOperations.UTILITY).booleanValue();
575    }
576
577    protected JsonNode getJsonManifest() throws IOException {
578        if (this.jsonManifest == null) {
579            try {
580                this.jsonManifest = this.mapper.readTree((new JsonManifest(this.train)).getFile());
581            } catch (IOException e) {
582                log.error("Json manifest file not found for train ({})", this.train.getName());
583            }
584        }
585        return this.jsonManifest;
586    }
587
588    @Override
589    public String getValidity() {
590        try {
591            if (Setup.isPrintTrainScheduleNameEnabled()) {
592                return String.format(locale, strings.getProperty(this.resourcePrefix + "ValidityWithSchedule"),
593                        this.getJsonManifest().path(JsonOperations.DATE).textValue(),
594                        InstanceManager.getDefault(TrainScheduleManager.class).getActiveSchedule().getName());
595            } else {
596                return String.format(locale, strings.getProperty(this.resourcePrefix + "Validity"),
597                        this.getJsonManifest().path(JsonOperations.DATE).textValue());
598            }
599        } catch (NullPointerException ex) {
600            log.warn("Manifest for train {} (id {}) does not have any validity.", this.train.getIconName(), this.train
601                    .getId());
602//        } catch (DateTimeParseException ex) {
603//            log.error("Date of JSON manifest could not be parsed as a LocalDateTime.");
604        } catch (IOException ex) {
605            log.error("JSON manifest could not be read.");
606        }
607        return "";
608    }
609}