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