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