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