001package jmri.web.servlet.operations;
002
003import java.io.IOException;
004import java.text.ParseException;
005import java.util.ArrayList;
006import java.util.Comparator;
007import java.util.Iterator;
008import java.util.Locale;
009import java.util.Map.Entry;
010
011import org.apache.commons.text.StringEscapeUtils;
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015import com.fasterxml.jackson.databind.JsonNode;
016import com.fasterxml.jackson.databind.ObjectMapper;
017import com.fasterxml.jackson.databind.util.StdDateFormat;
018
019import jmri.InstanceManager;
020import jmri.jmrit.operations.rollingstock.Xml;
021import jmri.jmrit.operations.routes.RouteLocation;
022import jmri.jmrit.operations.setup.Setup;
023import jmri.jmrit.operations.trains.JsonManifest;
024import jmri.jmrit.operations.trains.Train;
025import jmri.jmrit.operations.trains.schedules.TrainScheduleManager;
026import jmri.server.json.JSON;
027import jmri.server.json.operations.JsonOperations;
028
029/**
030 *
031 * @author Randall Wood
032 */
033public class HtmlManifest extends HtmlTrainCommon {
034
035    protected ObjectMapper mapper;
036    private JsonNode jsonManifest = null;
037    private final static Logger log = LoggerFactory.getLogger(HtmlManifest.class);
038
039    public HtmlManifest(Locale locale, Train train) throws IOException {
040        super(locale, train);
041        this.mapper = new ObjectMapper();
042        this.resourcePrefix = "Manifest";
043    }
044
045    // TODO cache the results so a quick check that if the JsonManifest file is not
046    // newer than the Html manifest, the cached copy is returned instead.
047    public String getLocations() throws IOException {
048        // build manifest from JSON manifest
049        if (this.getJsonManifest() == null) {
050            return "Error manifest file not found for this train";
051        }
052        StringBuilder builder = new StringBuilder();
053        JsonNode locations = this.getJsonManifest().path(JsonOperations.LOCATIONS);
054        String previousLocationName = null;
055        boolean hasWork;
056        for (JsonNode location : locations) {
057            RouteLocation routeLocation = train.getRoute().getLocationById(location.path(JSON.NAME).textValue());
058            log.debug("Processing {} ({})", routeLocation.getName(), location.path(JSON.NAME).textValue());
059            String routeLocationName = location.path(JSON.USERNAME).textValue();
060            builder.append(String.format(locale, strings.getProperty("LocationStart"), routeLocation.getId())); // NOI18N
061            hasWork = (location.path(JsonOperations.CARS).path(JSON.ADD).size() > 0
062                    || location.path(JsonOperations.CARS).path(JSON.REMOVE).size() > 0
063                    || location.path(JSON.ENGINES).path(JSON.ADD).size() > 0 || location.path(JSON.ENGINES).path(
064                            JSON.REMOVE).size() > 0);
065            if (hasWork && !routeLocationName.equals(previousLocationName)) {
066                if (!train.isShowArrivalAndDepartureTimesEnabled()) {
067                    builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
068                } else if (routeLocation == train.getTrainDepartsRouteLocation()) {
069                    builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,
070                            train.getFormatedDepartureTime())); // NOI18N
071                } else if (!routeLocation.getDepartureTime().equals(RouteLocation.NONE)) {
072                    builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,
073                            routeLocation.getFormatedDepartureTime())); // NOI18N
074                } else if (Setup.isUseDepartureTimeEnabled()
075                        && routeLocation != train.getTrainTerminatesRouteLocation()) {
076                    builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,
077                            train.getExpectedDepartureTime(routeLocation))); // NOI18N
078                } else if (!train.getExpectedArrivalTime(routeLocation).equals(Train.ALREADY_SERVICED)) { // NOI18N
079                    builder.append(String.format(locale, strings.getProperty("WorkArrivalTime"), routeLocationName,
080                            train.getExpectedArrivalTime(routeLocation))); // NOI18N
081                } else {
082                    builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
083                }
084                // add route comment
085                if (!location.path(JSON.COMMENT).textValue().trim().isEmpty()) {
086                    builder.append(String.format(locale, strings.getProperty("RouteLocationComment"), 
087                            location.path(JSON.COMMENT).textValue()));
088                }
089
090                builder.append(getTrackComments(location.path(JsonOperations.TRACK), location.path(JsonOperations.CARS)));
091
092                // add location comment
093                if (Setup.isPrintLocationCommentsEnabled()
094                        && !location.path(JsonOperations.LOCATION).path(JSON.COMMENT).textValue().trim().isEmpty()) {
095                    builder.append(String.format(locale, strings.getProperty("LocationComment"), location.path(
096                            JsonOperations.LOCATION).path(JSON.COMMENT).textValue()));
097                }
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(JsonOperations.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(JsonOperations.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().trim().isEmpty()) {
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().equals(RouteLocation.NONE)) {
190                                if (routeLocation.getComment().trim().length() > 0) {
191                                    builder.append(String.format(locale, strings.getProperty("CommentAt"), // NOI18N
192                                            routeLocationName, StringEscapeUtils
193                                            .escapeHtml4(routeLocation.getComment())));
194                                }
195                            }
196                            if (train.isShowArrivalAndDepartureTimesEnabled()) {
197                                if (routeLocation == train.getTrainDepartsRouteLocation()) {
198                                    builder.append(String.format(locale, strings
199                                            .getProperty("CommentAtWithDepartureTime"), routeLocationName, train // NOI18N
200                                            .getFormatedDepartureTime(), StringEscapeUtils
201                                            .escapeHtml4(routeLocation.getComment())));
202                                } else if (!routeLocation.getDepartureTime().equals(RouteLocation.NONE)) {
203                                    builder.append(String.format(locale, strings
204                                            .getProperty("CommentAtWithDepartureTime"), routeLocationName, // NOI18N
205                                            routeLocation.getFormatedDepartureTime(), StringEscapeUtils
206                                            .escapeHtml4(routeLocation.getComment())));
207                                } else if (Setup.isUseDepartureTimeEnabled() &&
208                                        !routeLocation.getComment().equals(RouteLocation.NONE)) {
209                                    builder.append(String.format(locale, strings
210                                            .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName, // NOI18N
211                                            train.getExpectedDepartureTime(routeLocation)));
212                                }
213                            }                           
214                        }
215                        // add location comment
216                        if (Setup.isPrintLocationCommentsEnabled()
217                                && !routeLocation.getLocation().getComment().isEmpty()) {
218                            builder.append(String.format(locale, strings.getProperty("LocationComment"),
219                                    StringEscapeUtils.escapeHtml4(routeLocation.getLocation().getComment())));
220                        }
221                    }
222                }
223            } else {
224                builder.append(String.format(strings.getProperty("TrainTerminatesIn"), routeLocationName));
225            }
226        }
227        return builder.toString();
228    }
229
230    protected String blockCars(JsonNode cars, RouteLocation location, boolean isManifest) {
231        StringBuilder builder = new StringBuilder();
232        log.debug("Cars is {}", cars);
233
234        //copy the adds into a sortable arraylist
235        ArrayList<JsonNode> adds = new ArrayList<JsonNode>();
236        cars.path(JSON.ADD).forEach(adds::add);
237            
238        //sort if requested
239        if (adds.size() > 0 && Setup.isSortByTrackNameEnabled()) {
240            adds.sort(Comparator.comparing(o -> o.path("location").path("track").path("userName").asText()));
241        }
242        //format each car for output
243        for (JsonNode car : adds) {
244            if (!this.isLocalMove(car)) {
245                // TODO utility format not quite ready, so display each car in
246                // manifest for now.
247                // if (this.isUtilityCar(car)) {
248                // builder.append(pickupUtilityCars(cars, car, location,
249                // isManifest));
250                // }
251                // else
252                // use truncated format if there's a switch list
253                if (isManifest &&
254                        Setup.isPrintTruncateManifestEnabled() &&
255                        location.getLocation().isSwitchListEnabled()) {
256                    builder.append(pickUpCar(car, Setup.getPickupTruncatedManifestMessageFormat()));
257                } else {
258                    builder.append(pickUpCar(car, Setup.getPickupManifestMessageFormat()));
259                }
260            }
261        }
262        for (JsonNode car : cars.path(JSON.REMOVE)) {
263            boolean local = isLocalMove(car);
264            // TODO utility format not quite ready, so display each car in
265            // manifest for now.
266            // if (this.isUtilityCar(car)) {
267            // builder.append(setoutUtilityCars(cars, car, location,
268            // isManifest));
269            // } else
270            if (isManifest &&
271                    Setup.isPrintTruncateManifestEnabled() &&
272                    location.getLocation().isSwitchListEnabled() &&
273                    !train.isLocalSwitcher()) {
274                // use truncated format if there's a switch list
275                builder.append(dropCar(car, Setup.getDropTruncatedManifestMessageFormat(), local));
276            } else {
277                String[] format;
278                if (isManifest) {
279                    format = (!local) ? Setup.getDropManifestMessageFormat() : Setup
280                            .getLocalManifestMessageFormat();
281                } else {
282                    format = (!local) ? Setup.getDropSwitchListMessageFormat() : Setup
283                            .getLocalSwitchListMessageFormat();
284                }
285                builder.append(dropCar(car, format, local));
286            }
287        }
288        return String.format(locale, strings.getProperty("CarsList"), builder.toString());
289    }
290
291    protected String pickupUtilityCars(JsonNode cars, JsonNode car, RouteLocation location, boolean isManifest) {
292        // list utility cars by type, track, length, and load
293        String[] messageFormat;
294        if (isManifest) {
295            messageFormat = Setup.getPickupUtilityManifestMessageFormat();
296        } else {
297            messageFormat = Setup.getPickupUtilitySwitchListMessageFormat();
298        }
299        // TODO: reimplement following commented out code
300        // if (this.countUtilityCars(messageFormat, carList, car, location, rld, PICKUP) == 0) {
301        // return ""; // already printed out this car type
302        // }
303        return this.pickUpCar(car, messageFormat);
304    }
305
306    protected String setoutUtilityCars(JsonNode cars, JsonNode car, RouteLocation location, boolean isManifest) {
307        boolean isLocal = isLocalMove(car);
308        String[] messageFormat;
309        if (isLocal && isManifest) {
310            messageFormat = Setup.getLocalUtilityManifestMessageFormat();
311        } else if (isLocal && !isManifest) {
312            messageFormat = Setup.getLocalUtilitySwitchListMessageFormat();
313        } else if (!isLocal && !isManifest) {
314            messageFormat = Setup.getDropUtilitySwitchListMessageFormat();
315        } else {
316            messageFormat = Setup.getDropUtilityManifestMessageFormat();
317        }
318        // TODO: reimplement following commented out code
319        // if (countUtilityCars(messageFormat, carList, car, location, null, !PICKUP) == 0) {
320        // return ""; // already printed out this car type
321        // }
322        return dropCar(car, messageFormat, isLocal);
323    }
324
325    protected String pickUpCar(JsonNode car, String[] format) {
326        if (isLocalMove(car)) {
327            return ""; // print nothing for local move, see dropCar()
328        }
329        StringBuilder builder = new StringBuilder();
330        builder.append(Setup.getPickupCarPrefix()).append(" ");
331        for (String attribute : format) {
332            if (!attribute.trim().isEmpty()) {
333                attribute = attribute.toLowerCase();
334                log.trace("Adding car with attribute {}", attribute);
335                if (attribute.equals(JsonOperations.LOCATION) || attribute.equals(JsonOperations.TRACK)) {
336                    attribute = JsonOperations.LOCATION; // treat "track" as "location"
337                    builder.append(
338                            this.getFormattedAttribute(attribute, this.getPickupLocation(car.path(attribute),
339                                            ShowLocation.track))).append(" "); // NOI18N
340                } else if (attribute.equals(JsonOperations.DESTINATION)) {
341                    builder.append(
342                            this.getFormattedAttribute(attribute, this.getDropLocation(car.path(attribute),
343                                            ShowLocation.location))).append(" "); // NOI18N
344                } else if (attribute.equals(JsonOperations.DESTINATION_TRACK)) {
345                    builder.append(
346                            this.getFormattedAttribute(attribute, this.getDropLocation(car.path(JsonOperations.DESTINATION),
347                                            ShowLocation.both))).append(" "); // NOI18N
348                } else if (attribute.equals(Xml.TYPE)) {
349                    builder.append(this.getTextAttribute(JsonOperations.CAR_TYPE, car)).append(" "); // NOI18N
350                } else {
351                    builder.append(this.getTextAttribute(attribute, car)).append(" "); // NOI18N
352                }
353            }
354        }
355        log.debug("Picking up car {}", builder);
356        return String.format(locale, strings.getProperty(this.resourcePrefix + "PickUpCar"), builder.toString()); // NOI18N
357    }
358
359    protected String dropCar(JsonNode car, String[] format, boolean isLocal) {
360        StringBuilder builder = new StringBuilder();
361        if (!isLocal) {
362            builder.append(Setup.getDropCarPrefix()).append(" ");
363        } else {
364            builder.append(Setup.getLocalPrefix()).append(" ");
365        }
366        log.debug("dropCar {}", car);
367        for (String attribute : format) {
368            if (!attribute.trim().isEmpty()) {
369                attribute = attribute.toLowerCase();
370                log.trace("Removing car with attribute {}", attribute);
371                if (attribute.equals(JsonOperations.DESTINATION) || attribute.equals(JsonOperations.TRACK)) {
372                    attribute = JsonOperations.DESTINATION; // treat "track" as "destination"
373                    builder.append(
374                            this.getFormattedAttribute(attribute, this.getDropLocation(car.path(attribute),
375                                            ShowLocation.track))).append(" "); // NOI18N
376                } else if (attribute.equals(JsonOperations.LOCATION) && isLocal) {
377                    builder.append(
378                            this.getFormattedAttribute(attribute, this.getPickupLocation(car.path(attribute),
379                                            ShowLocation.track))).append(" "); // NOI18N
380                } else if (attribute.equals(JsonOperations.LOCATION)) {
381                    builder.append(
382                            this.getFormattedAttribute(attribute, this.getPickupLocation(car.path(attribute),
383                                            ShowLocation.location))).append(" "); // NOI18N
384                } else if (attribute.equals(Xml.TYPE)) {
385                    builder.append(this.getTextAttribute(JsonOperations.CAR_TYPE, car)).append(" "); // NOI18N
386                } else {
387                    builder.append(this.getTextAttribute(attribute, car)).append(" "); // NOI18N
388                }
389            }
390        }
391        log.debug("Dropping {}car {}", (isLocal) ? "local " : "", builder);
392        if (!isLocal) {
393            return String.format(locale, strings.getProperty(this.resourcePrefix + "DropCar"), builder.toString()); // NOI18N
394        } else {
395            return String.format(locale, strings.getProperty(this.resourcePrefix + "LocalCar"), builder.toString()); // NOI18N
396        }
397    }
398
399    protected String dropEngines(JsonNode engines) {
400        StringBuilder builder = new StringBuilder();
401        engines.forEach((engine) -> {
402            builder.append(this.dropEngine(engine));
403        });
404        return String.format(locale, strings.getProperty("EnginesList"), builder.toString());
405    }
406
407    protected String dropEngine(JsonNode engine) {
408        StringBuilder builder = new StringBuilder();
409        builder.append(Setup.getDropEnginePrefix()).append(" ");
410        for (String attribute : Setup.getDropEngineMessageFormat()) {
411            if (!attribute.trim().isEmpty()) {
412                attribute = attribute.toLowerCase();
413                if (attribute.equals(JsonOperations.DESTINATION) || attribute.equals(JsonOperations.TRACK)) {
414                    attribute = JsonOperations.DESTINATION; // treat "track" as "destination"
415                    builder.append(
416                            this.getFormattedAttribute(attribute, this.getDropLocation(engine.path(attribute),
417                                            ShowLocation.track))).append(" "); // NOI18N
418                } else {
419                    builder.append(this.getTextAttribute(attribute, engine)).append(" "); // NOI18N
420                }
421            }
422        }
423        log.debug("Drop engine: {}", builder);
424        return String.format(locale, strings.getProperty(this.resourcePrefix + "DropEngine"), builder.toString());
425    }
426
427    protected String pickupEngines(JsonNode engines) {
428        StringBuilder builder = new StringBuilder();
429        if (engines.size() > 0) {
430            for (JsonNode engine : engines) {
431                builder.append(this.pickupEngine(engine));
432            }
433        }
434        return String.format(locale, strings.getProperty("EnginesList"), builder.toString());
435    }
436
437    protected String pickupEngine(JsonNode engine) {
438        StringBuilder builder = new StringBuilder();
439        builder.append(Setup.getPickupEnginePrefix()).append(" ");
440        log.debug("PickupEngineMessageFormat: {}", (Object) Setup.getPickupEngineMessageFormat());
441        for (String attribute : Setup.getPickupEngineMessageFormat()) {
442            if (!attribute.trim().isEmpty()) {
443                attribute = attribute.toLowerCase();
444                if (attribute.equals(JsonOperations.LOCATION) || attribute.equals(JsonOperations.TRACK)) {
445                    attribute = JsonOperations.LOCATION; // treat "track" as "location"
446                    builder.append(
447                            this.getFormattedAttribute(attribute, this.getPickupLocation(engine.path(attribute),
448                                            ShowLocation.track))).append(" "); // NOI18N
449                } else {
450                    builder.append(this.getTextAttribute(attribute, engine)).append(" "); // NOI18N
451                }
452            }
453        }
454        log.debug("Picking up engine: {}", builder);
455        return String.format(locale, strings.getProperty(this.resourcePrefix + "PickUpEngine"), builder.toString());
456    }
457
458    protected String getDropLocation(JsonNode location, ShowLocation show) {
459        return this.getFormattedLocation(location, show, "To"); // NOI18N
460    }
461
462    protected String getPickupLocation(JsonNode location, ShowLocation show) {
463        return this.getFormattedLocation(location, show, "From"); // NOI18N
464    }
465
466    protected String getTextAttribute(String attribute, JsonNode rollingStock) {
467        if (attribute.equals(JSON.HAZARDOUS)) {
468            return this.getFormattedAttribute(attribute, (rollingStock.path(attribute).asBoolean() ? Setup
469                    .getHazardousMsg() : "")); // NOI18N
470        } else if (attribute.equals(Setup.PICKUP_COMMENT.toLowerCase())) { // NOI18N
471            return this.getFormattedAttribute(JSON.ADD_COMMENT, rollingStock.path(JSON.ADD_COMMENT).textValue());
472        } else if (attribute.equals(Setup.DROP_COMMENT.toLowerCase())) { // NOI18N
473            return this.getFormattedAttribute(JSON.REMOVE_COMMENT, rollingStock.path(JSON.REMOVE_COMMENT).textValue());
474        } else if (attribute.equals(Setup.RWE.toLowerCase())) {
475            return this.getFormattedLocation(rollingStock.path(JSON.RETURN_WHEN_EMPTY), ShowLocation.both, "RWE"); // NOI18N
476        } else if (attribute.equals(Setup.FINAL_DEST.toLowerCase())) {
477            return this.getFormattedLocation(rollingStock.path(JSON.FINAL_DESTINATION), ShowLocation.location, "FinalDestination"); // NOI18N
478        } else if (attribute.equals(Setup.FINAL_DEST_TRACK.toLowerCase())) {
479            return this.getFormattedLocation(rollingStock.path(JSON.FINAL_DESTINATION), ShowLocation.track, "FinalDestination"); // NOI18N
480        }
481        return this.getFormattedAttribute(attribute, rollingStock.path(attribute).asText());
482    }
483
484    protected String getFormattedAttribute(String attribute, String value) {
485        return String.format(locale, strings.getProperty("Attribute"), StringEscapeUtils.escapeHtml4(value), attribute);
486    }
487
488    protected String getFormattedLocation(JsonNode location, ShowLocation show, String prefix) {
489        if (location.isNull() || location.isEmpty()) {
490            // return an empty string if location is an empty or null
491            return "";
492        }
493        // TODO handle tracks without names
494        switch (show) {
495            case location:
496                return String.format(locale, strings.getProperty(prefix + "Location"),
497                        splitString(location.path(JSON.USERNAME).asText()));
498            case track:
499                return String.format(locale, strings.getProperty(prefix + "Track"),
500                        splitString(location.path(JsonOperations.TRACK).path(JSON.USERNAME).asText()));
501            case both:
502            default: // default here ensures the method always returns
503                return String.format(locale, strings.getProperty(prefix + "LocationAndTrack"),
504                        splitString(location.path(JSON.USERNAME).asText()),
505                        splitString(location.path(JsonOperations.TRACK).path(JSON.USERNAME).asText()));
506        }
507    }
508
509    private String getTrackComments(JsonNode tracks, JsonNode cars) {
510        StringBuilder builder = new StringBuilder();
511        if (tracks.size() > 0) {
512            Iterator<Entry<String, JsonNode>> iterator = tracks.fields();
513            while (iterator.hasNext()) {
514                Entry<String, JsonNode> track = iterator.next();
515                boolean pickup = false;
516                boolean setout = false;
517                if (cars.path(JSON.ADD).size() > 0) {
518                    for (JsonNode car : cars.path(JSON.ADD)) {
519                        if (track.getKey().equals(car.path(JsonOperations.TRACK).path(JSON.NAME).textValue())) {
520                            pickup = true;
521                            break; // we do not need to iterate all cars
522                        }
523                    }
524                }
525                if (cars.path(JSON.REMOVE).size() > 0) {
526                    for (JsonNode car : cars.path(JSON.REMOVE)) {
527                        if (track.getKey().equals(car.path(JsonOperations.TRACK).path(JSON.NAME).textValue())) {
528                            setout = true;
529                            break; // we do not need to iterate all cars
530                        }
531                    }
532                }
533                if (pickup && setout) {
534                    builder.append(String.format(locale, strings.getProperty("TrackComments"), track.getValue().path(
535                            JSON.ADD_AND_REMOVE).textValue()));
536                } else if (pickup) {
537                    builder.append(String.format(locale, strings.getProperty("TrackComments"), track.getValue().path(
538                            JSON.ADD).textValue()));
539                } else if (setout) {
540                    builder.append(String.format(locale, strings.getProperty("TrackComments"), track.getValue().path(
541                            JSON.REMOVE).textValue()));
542                }
543            }
544        }
545        return builder.toString();
546    }
547
548    protected boolean isLocalMove(JsonNode car) {
549        return car.path(JSON.IS_LOCAL).booleanValue();        
550    }
551
552    protected boolean isUtilityCar(JsonNode car) {
553        return car.path(JSON.UTILITY).booleanValue();
554    }
555
556    protected JsonNode getJsonManifest() throws IOException {
557        if (this.jsonManifest == null) {
558            try {
559                this.jsonManifest = this.mapper.readTree((new JsonManifest(this.train)).getFile());
560            } catch (IOException e) {
561                log.error("Json manifest file not found for train ({})", this.train.getName());
562            }
563        }
564        return this.jsonManifest;
565    }
566
567    @Override
568    public String getValidity() {
569        try {
570            if (Setup.isPrintTrainScheduleNameEnabled()) {
571                return String.format(locale, strings.getProperty(this.resourcePrefix + "ValidityWithSchedule"),
572                        getDate((new StdDateFormat()).parse(this.getJsonManifest().path(JsonOperations.DATE).textValue())),
573                        InstanceManager.getDefault(TrainScheduleManager.class).getScheduleById(train.getId()));
574            } else {
575                return String.format(locale, strings.getProperty(this.resourcePrefix + "Validity"),
576                        getDate((new StdDateFormat()).parse(this.getJsonManifest().path(JsonOperations.DATE).textValue())));
577            }
578        } catch (NullPointerException ex) {
579            log.warn("Manifest for train {} (id {}) does not have any validity.", this.train.getIconName(), this.train
580                    .getId());
581        } catch (ParseException ex) {
582            log.error("Date of JSON manifest could not be parsed as a Date.");
583        } catch (IOException ex) {
584            log.error("JSON manifest could not be read.");
585        }
586        return "";
587    }
588}