001package jmri.server.json.operations;
002
003import static jmri.server.json.JSON.*;
004import static jmri.server.json.JSON.COMMENT;
005import static jmri.server.json.JSON.DESTINATION;
006import static jmri.server.json.JSON.ENGINES;
007import static jmri.server.json.JSON.KERNEL;
008import static jmri.server.json.JSON.LENGTH;
009import static jmri.server.json.JSON.LOCATION;
010import static jmri.server.json.JSON.MODEL;
011import static jmri.server.json.JSON.NUMBER;
012import static jmri.server.json.JSON.ROAD;
013import static jmri.server.json.JSON.TRACK;
014import static jmri.server.json.JSON.TYPE;
015import static jmri.server.json.JSON.WEIGHT;
016import static jmri.server.json.operations.JsonOperations.*;
017import static jmri.server.json.operations.JsonOperations.OUT_OF_SERVICE;
018import static jmri.server.json.reporter.JsonReporter.REPORTER;
019
020import java.util.*;
021
022import javax.annotation.Nonnull;
023import javax.servlet.http.HttpServletResponse;
024
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028import com.fasterxml.jackson.databind.JsonNode;
029import com.fasterxml.jackson.databind.ObjectMapper;
030import com.fasterxml.jackson.databind.node.ArrayNode;
031import com.fasterxml.jackson.databind.node.ObjectNode;
032
033import jmri.*;
034import jmri.jmrit.operations.locations.*;
035import jmri.jmrit.operations.rollingstock.RollingStock;
036import jmri.jmrit.operations.rollingstock.cars.*;
037import jmri.jmrit.operations.rollingstock.engines.Engine;
038import jmri.jmrit.operations.rollingstock.engines.EngineManager;
039import jmri.jmrit.operations.trains.Train;
040import jmri.jmrit.operations.trains.TrainManager;
041import jmri.server.json.*;
042
043/**
044 * @author Randall Wood (C) 2016, 2018, 2019, 2020
045 */
046public class JsonOperationsHttpService extends JsonHttpService {
047
048    private final JsonUtil utilities;
049
050    private static final Logger log = LoggerFactory.getLogger(JsonOperationsHttpService.class);    
051
052    public JsonOperationsHttpService(ObjectMapper mapper) {
053        super(mapper);
054        utilities = new JsonUtil(mapper);
055    }
056
057    @Override
058    public JsonNode doGet(String type, String name, JsonNode data, JsonRequest request) throws JsonException {
059        log.debug("doGet(type='{}', name='{}', data='{}')", type, name, data);
060        Locale locale = request.locale;
061        int id = request.id;
062        ObjectNode result;
063        switch (type) {
064            case CAR:
065                result = utilities.getCar(name, locale, id);
066                break;
067            case CAR_TYPE:
068                result = getCarType(name, locale, id);
069                break;
070            case ENGINE:
071                result = utilities.getEngine(name, locale, id);
072                break;
073            case KERNEL:
074                Kernel kernel = InstanceManager.getDefault(KernelManager.class).getKernelByName(name);
075                if (kernel == null) {
076                    throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
077                            Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, type, name), id);
078                }
079                result = getKernel(kernel, locale, id);
080                break;
081            case LOCATION:
082                result = utilities.getLocation(name, locale, id);
083                break;
084            case ROLLING_STOCK:
085                throw new JsonException(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
086                        Bundle.getMessage(locale, "GetNotAllowed", type), id);
087            case TRAIN:
088            case TRAINS:
089                type = TRAIN;
090                result = utilities.getTrain(name, locale, id);
091                break;
092            case TRACK:
093                result = utilities.getTrack(getTrackByName(name, data, locale, id), locale);
094                break;
095            default:
096                throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
097                        Bundle.getMessage(locale, "ErrorInternal", type), id);
098        }
099        return message(type, result, id);
100    }
101
102    @Override
103    public JsonNode doPost(String type, String name, JsonNode data, JsonRequest request) throws JsonException {
104        log.debug("doPost(type='{}', name='{}', data='{}')", type, name, data);
105        Locale locale = request.locale;
106        int id = request.id;
107        String newName = name;
108        switch (type) {
109            case CAR:
110                return message(type, postCar(name, data, locale, id), id);
111            case CAR_TYPE:
112                if (data.path(RENAME).isTextual()) {
113                    newName = data.path(RENAME).asText();
114                    InstanceManager.getDefault(CarTypes.class).replaceName(name, newName);
115                }
116                return message(type, getCarType(newName, locale, id).put(RENAME, name), id);
117            case ENGINE:
118                return message(type, postEngine(name, data, locale, id), id);
119            case KERNEL:
120                if (data.path(RENAME).isTextual()) {
121                    newName = data.path(RENAME).asText();
122                    InstanceManager.getDefault(KernelManager.class).replaceKernelName(name, newName);
123                    InstanceManager.getDefault(KernelManager.class).deleteKernel(name);
124                }
125                return message(type, getKernel(InstanceManager.getDefault(KernelManager.class).getKernelByName(newName), locale, id).put(RENAME, name), id);
126            case LOCATION:
127                return message(type, postLocation(name, data, locale, id), id);
128            case TRAIN:
129                setTrain(name, data, locale, id);
130                break;
131            case TRACK:
132                return message(type, postTrack(name, data, locale, id), id);
133            case TRAINS:
134                // do nothing
135                break;
136            default:
137                throw new JsonException(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
138                        Bundle.getMessage(locale, "PostNotAllowed", type), id); // NOI18N
139        }
140        return doGet(type, name, data, request);
141    }
142
143    @Override
144    public JsonNode doPut(String type, String name, JsonNode data, JsonRequest request)
145            throws JsonException {
146        log.debug("doPut(type='{}', name='{}', data='{}')", type, name, data);
147        Locale locale = request.locale;
148        int id = request.id;
149        switch (type) {
150            case CAR:
151                if (data.path(ROAD).isMissingNode()) {
152                    throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
153                            Bundle.getMessage(locale, JsonException.ERROR_MISSING_PROPERTY_PUT, ROAD, type), id); // NOI18N
154                }
155                if (data.path(NUMBER).isMissingNode()) {
156                    throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
157                            Bundle.getMessage(locale, JsonException.ERROR_MISSING_PROPERTY_PUT, NUMBER, type), id); // NOI18N
158                }
159                String road = data.path(ROAD).asText();
160                String number = data.path(NUMBER).asText();
161                if (carManager().getById(name) != null || carManager().getByRoadAndNumber(road, number) != null) {
162                    throw new JsonException(HttpServletResponse.SC_CONFLICT,
163                            Bundle.getMessage(locale, "ErrorPutRollingStockConflict", type, road, number), id); // NOI18N
164                }
165                return message(type, postCar(carManager().newRS(road, number), data, locale, id), id);
166            case CAR_TYPE:
167                if (name.isEmpty()) {
168                    throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
169                            Bundle.getMessage(locale, JsonException.ERROR_MISSING_PROPERTY_PUT, NAME, type), id); // NOI18N
170                }
171                InstanceManager.getDefault(CarTypes.class).addName(name);
172                return message(type, getCarType(name, locale, id), id);
173            case ENGINE:
174                if (data.path(ROAD).isMissingNode()) {
175                    throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
176                            Bundle.getMessage(locale, JsonException.ERROR_MISSING_PROPERTY_PUT, ROAD, type), id); // NOI18N
177                }
178                if (data.path(NUMBER).isMissingNode()) {
179                    throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
180                            Bundle.getMessage(locale, JsonException.ERROR_MISSING_PROPERTY_PUT, NUMBER, type), id); // NOI18N
181                }
182                road = data.path(ROAD).asText();
183                number = data.path(NUMBER).asText();
184                if (engineManager().getById(name) != null || engineManager().getByRoadAndNumber(road, number) != null) {
185                    throw new JsonException(HttpServletResponse.SC_CONFLICT,
186                            Bundle.getMessage(locale, "ErrorPutRollingStockConflict", type, road, number), id); // NOI18N
187                }
188                return message(type, postEngine(engineManager().newRS(road, number), data, locale, id), id);
189            case KERNEL:
190                if (name.isEmpty()) {
191                    throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
192                            Bundle.getMessage(locale, JsonException.ERROR_MISSING_PROPERTY_PUT, NAME, type), id); // NOI18N
193                }
194                return message(type, getKernel(InstanceManager.getDefault(KernelManager.class).newKernel(name), locale, id), id);
195            case LOCATION:
196                if (data.path(USERNAME).isMissingNode()) {
197                    throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
198                            Bundle.getMessage(locale, JsonException.ERROR_MISSING_PROPERTY_PUT, USERNAME, type), id); // NOI18N
199                }
200                String userName = data.path(USERNAME).asText();
201                if (locationManager().getLocationById(name) != null) {
202                    throw new JsonException(HttpServletResponse.SC_CONFLICT,
203                            Bundle.getMessage(locale, "ErrorPutNameConflict", type, name), id); // NOI18N
204                }
205                if (locationManager().getLocationByName(userName) != null) {
206                    throw new JsonException(HttpServletResponse.SC_CONFLICT,
207                            Bundle.getMessage(locale, "ErrorPutUserNameConflict", type, userName), id); // NOI18N
208                }
209                return message(type, postLocation(locationManager().newLocation(userName), data, locale, id), id);
210            case TRACK:
211                if (data.path(USERNAME).isMissingNode()) {
212                    throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
213                            Bundle.getMessage(locale, JsonException.ERROR_MISSING_PROPERTY_PUT, USERNAME, type), id); // NOI18N
214                }
215                userName = data.path(USERNAME).asText();
216                if (data.path(TYPE).isMissingNode()) {
217                    throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
218                            Bundle.getMessage(locale, JsonException.ERROR_MISSING_PROPERTY_PUT, TYPE, type), id); // NOI18N
219                }
220                String trackType = data.path(TYPE).asText();
221                if (data.path(LOCATION).isMissingNode()) {
222                    throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
223                            Bundle.getMessage(locale, JsonException.ERROR_MISSING_PROPERTY_PUT, LOCATION, type), id); // NOI18N
224                }
225                String locationName = data.path(LOCATION).asText();
226                Location location = locationManager().getLocationById(locationName);
227                if (location == null) {
228                    throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
229                            Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, LOCATION, locationName), id); // NOI18N
230                }
231                if (location.getTrackById(name) != null) {
232                    throw new JsonException(HttpServletResponse.SC_CONFLICT,
233                            Bundle.getMessage(locale, "ErrorPutNameConflict", type, name), id); // NOI18N
234                }
235                if (location.getTrackByName(userName, trackType) != null) {
236                    throw new JsonException(HttpServletResponse.SC_CONFLICT,
237                            Bundle.getMessage(locale, "ErrorPutUserNameConflict", type, userName), id); // NOI18N
238                }
239                return message(type, postTrack(location.addTrack(userName, trackType), data, locale, id), id);
240            default:
241                return super.doPut(type, name, data, request);
242        }
243    }
244
245    @Override
246    public JsonNode doGetList(String type, JsonNode data, JsonRequest request) throws JsonException {
247        log.debug("doGetList(type='{}', data='{}')", type, data);
248        Locale locale = request.locale;
249        int id = request.id;
250        switch (type) {
251            case CAR:
252            case CARS:
253                return message(getCars(locale, id), id);
254            case CAR_TYPE:
255                return getCarTypes(locale, id);
256            case ENGINE:
257            case ENGINES:
258                return message(getEngines(locale, id), id);
259            case KERNEL:
260                return getKernels(locale, id);
261            case LOCATION:
262            case LOCATIONS:
263                return getLocations(locale, id);
264            case ROLLING_STOCK:
265                return message(getCars(locale, id).addAll(getEngines(locale, id)), id);
266            case TRAIN:
267            case TRAINS:
268                return getTrains(locale, id);
269            default:
270                throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
271                        Bundle.getMessage(locale, "ErrorInternal", type), id); // NOI18N
272        }
273    }
274
275    @Override
276    public void doDelete(String type, String name, JsonNode data, JsonRequest request) throws JsonException {
277        log.debug("doDelete(type='{}', name='{}', data='{}')", type, name, data);
278        Locale locale = request.locale;
279        int id = request.id;
280        String token = data.path(FORCE_DELETE).asText();
281        switch (type) {
282            case CAR:
283                // TODO: do not remove an in use car
284                deleteCar(name, locale, id);
285                break;
286            case CAR_TYPE:
287                List<Car> cars = carManager().getByTypeList(name);
288                List<Location> locations = new ArrayList<>();
289                locationManager().getList().stream().filter(l -> l.acceptsTypeName(name)).forEach(locations::add);
290                if ((!cars.isEmpty() || !locations.isEmpty()) && !acceptForceDeleteToken(type, name, token)) {
291                    ArrayNode conflicts = mapper.createArrayNode();
292                    cars.forEach(car -> conflicts.add(message(CAR, utilities.getCar(car, locale), 0)));
293                    locations.forEach(
294                            location -> conflicts.add(message(LOCATION, utilities.getLocation(location, locale), 0)));
295                    throwDeleteConflictException(type, name, conflicts, request);
296                }
297                InstanceManager.getDefault(CarTypes.class).deleteName(name);
298                break;
299            case ENGINE:
300                // TODO: do not remove an in use engine
301                deleteEngine(name, locale, id);
302                break;
303            case KERNEL:
304                Kernel kernel = InstanceManager.getDefault(KernelManager.class).getKernelByName(name);
305                if (kernel == null) {
306                    throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
307                            Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, type, name), id);
308                }
309                if (kernel.getSize() != 0 && !acceptForceDeleteToken(type, name, token)) {
310                    throwDeleteConflictException(type, name, getKernelCars(kernel, true, locale), request);
311                }
312                InstanceManager.getDefault(KernelManager.class).deleteKernel(name);
313                break;
314            case LOCATION:
315                // TODO: do not remove an in use location
316                deleteLocation(name, locale, id);
317                break;
318            case TRACK:
319                // TODO: do not remove an in use track
320                deleteTrack(name, data, locale, id);
321                break;
322            default:
323                super.doDelete(type, name, data, request);
324        }
325    }
326
327    private ObjectNode getCarType(String name, Locale locale, int id) throws JsonException {
328        CarTypes manager = InstanceManager.getDefault(CarTypes.class);
329        if (!manager.containsName(name)) {
330            throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
331                    Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, CAR_TYPE, name), id);
332        }
333        ObjectNode data = mapper.createObjectNode();
334        data.put(NAME, name);
335        ArrayNode cars = data.putArray(CARS);
336        carManager().getByTypeList(name).forEach(car -> cars.add(utilities.getCar(car, locale)));
337        ArrayNode locations = data.putArray(LOCATIONS);
338        locationManager().getList().stream()
339                .filter(location -> location.acceptsTypeName(name))
340                .forEach(location -> locations.add(utilities.getLocation(location, locale)));
341        return data;
342    }
343
344    private JsonNode getCarTypes(Locale locale, int id) throws JsonException {
345        ArrayNode array = mapper.createArrayNode();
346        for (String name : InstanceManager.getDefault(CarTypes.class).getNames()) {
347            array.add(message(CAR_TYPE, getCarType(name, locale, id), id));
348        }
349        return message(array, id);
350    }
351
352    private ObjectNode getKernel(Kernel kernel, Locale locale, int id) {
353        ObjectNode data = mapper.createObjectNode();
354        data.put(NAME, kernel.getName());
355        data.put(WEIGHT, kernel.getAdjustedWeightTons());
356        data.put(LENGTH, kernel.getTotalLength());
357        Car lead = kernel.getLead();
358        if (lead != null) {
359            data.set(LEAD, utilities.getCar(kernel.getLead(), locale));
360        } else {
361            data.putNull(LEAD);
362        }
363        data.set(CARS, getKernelCars(kernel, false, locale));
364        return data;
365    }
366
367    private ArrayNode getKernelCars(Kernel kernel, boolean asMessage, Locale locale) {
368        ArrayNode array = mapper.createArrayNode();
369        kernel.getCars().forEach(car -> {
370            if (asMessage) {
371                array.add(message(CAR, utilities.getCar(car, locale), 0));
372            } else {
373                array.add(utilities.getCar(car, locale));
374            }
375        });
376        return array;
377    }
378
379    private JsonNode getKernels(Locale locale, int id) {
380        ArrayNode array = mapper.createArrayNode();
381        InstanceManager.getDefault(KernelManager.class).getNameList()
382                // individual kernels should not have id in array, but same
383                // method is used to get single kernels as requested, so pass
384                // additive inverse of id to allow errors
385                .forEach(kernel -> array.add(message(KERNEL, getKernel(InstanceManager.getDefault(KernelManager.class).getKernelByName(kernel), locale, id * -1), id * -1)));
386        return message(array, id);
387    }
388
389    public ArrayNode getCars(Locale locale, int id) {
390        ArrayNode array = mapper.createArrayNode();
391        carManager().getByIdList()
392                .forEach(car -> array.add(message(CAR, utilities.getCar(car, locale), id)));
393        return array;
394    }
395
396    public ArrayNode getEngines(Locale locale, int id) {
397        ArrayNode array = mapper.createArrayNode();
398        engineManager().getByIdList()
399                .forEach(engine -> array.add(message(ENGINE, utilities.getEngine(engine, locale), id)));
400        return array;
401    }
402
403    public JsonNode getLocations(Locale locale, int id) {
404        ArrayNode array = mapper.createArrayNode();
405        locationManager().getLocationsByIdList()
406                .forEach(location -> array.add(message(LOCATION, utilities.getLocation(location, locale), id)));
407        return message(array, id);
408    }
409
410    public JsonNode getTrains(Locale locale, int id) {
411        ArrayNode array = mapper.createArrayNode();
412        trainManager().getTrainsByIdList()
413                .forEach(train -> array.add(message(TRAIN, utilities.getTrain(train, locale), id)));
414        return message(array, id);
415    }
416
417    /**
418     * Set the properties in the data parameter for the train with the given id.
419     * <p>
420     * Currently only moves the train to the location given with the key
421     * LOCATION. If the move cannot be completed, throws error code 428.
422     *
423     * @param name   id of the train
424     * @param data   train data to change
425     * @param locale locale to throw exceptions in
426     * @param id     message id set by client
427     * @throws jmri.server.json.JsonException if the train cannot move to the
428     *                                        location in data.
429     */
430    public void setTrain(String name, JsonNode data, Locale locale, int id) throws JsonException {
431        Train train = InstanceManager.getDefault(TrainManager.class).getTrainById(name);
432        JsonNode location = data.path(LOCATION);
433        if (!location.isMissingNode()) {
434            if (location.isNull()) {
435                train.terminate();
436            } else if (!train.move(location.asText())) {
437                throw new JsonException(428, Bundle.getMessage(locale, "ErrorTrainMovement", name, location.asText()),
438                        id);
439            }
440        }
441    }
442
443    public ObjectNode postLocation(String name, JsonNode data, Locale locale, int id) throws JsonException {
444        return postLocation(getLocationByName(name, locale, id), data, locale, id);
445    }
446
447    public ObjectNode postLocation(Location location, JsonNode data, Locale locale, int id) throws JsonException {
448        // set things that throw exceptions first
449        if (!data.path(REPORTER).isMissingNode()) {
450            String name = data.path(REPORTER).asText();
451            Reporter reporter = InstanceManager.getDefault(ReporterManager.class).getBySystemName(name);
452            if (reporter != null) {
453                location.setReporter(reporter);
454            } else {
455                throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
456                        Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, REPORTER, name), id);
457            }
458        }
459        location.setName(data.path(USERNAME).asText(location.getName()));
460        location.setComment(data.path(COMMENT).asText(location.getCommentWithColor()));
461        return utilities.getLocation(location, locale);
462    }
463
464    public ObjectNode postTrack(String name, JsonNode data, Locale locale, int id) throws JsonException {
465        return postTrack(getTrackByName(name, data, locale, id), data, locale, id);
466    }
467
468    public ObjectNode postTrack(Track track, JsonNode data, Locale locale, int id) throws JsonException {
469        // set things that throw exceptions first
470        if (!data.path(REPORTER).isMissingNode()) {
471            String name = data.path(REPORTER).asText();
472            Reporter reporter = InstanceManager.getDefault(ReporterManager.class).getBySystemName(name);
473            if (reporter != null) {
474                track.setReporter(reporter);
475            } else {
476                throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
477                        Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, REPORTER, name), id);
478            }
479        }
480        track.setName(data.path(USERNAME).asText(track.getName()));
481        track.setLength(data.path(LENGTH).asInt(track.getLength()));
482        track.setComment(data.path(COMMENT).asText(track.getComment()));
483        return utilities.getTrack(track, locale);
484    }
485
486    /**
487     * Set the properties in the data parameter for the given car.
488     * <p>
489     * <strong>Note</strong> returns the modified car because changing the road
490     * or number of a car changes its name in the JSON representation.
491     *
492     * @param name   the operations id of the car to change
493     * @param data   car data to change
494     * @param locale locale to throw exceptions in
495     * @param id     message id set by client
496     * @return the JSON representation of the car
497     * @throws JsonException if a car by name cannot be found
498     */
499    public ObjectNode postCar(String name, JsonNode data, Locale locale, int id) throws JsonException {
500        return postCar(getCarByName(name, locale, id), data, locale, id);
501    }
502
503    /**
504     * Set the properties in the data parameter for the given car.
505     * <p>
506     * <strong>Note</strong> returns the modified car because changing the road
507     * or number of a car changes its name in the JSON representation.
508     *
509     * @param car    the car to change
510     * @param data   car data to change
511     * @param locale locale to throw exceptions in
512     * @param id     message id set by client
513     * @return the JSON representation of the car
514     * @throws JsonException if unable to set location
515     */
516    public ObjectNode postCar(@Nonnull Car car, JsonNode data, Locale locale, int id) throws JsonException {
517        ObjectNode result = postRollingStock(car, data, locale, id);
518        car.setCaboose(data.path(CABOOSE).asBoolean(car.isCaboose()));
519        car.setCarHazardous(data.path(HAZARDOUS).asBoolean(car.isHazardous()));
520        car.setPassenger(data.path(PASSENGER).asBoolean(car.isPassenger()));
521        car.setFred(data.path(FRED).asBoolean(car.hasFred()));
522        car.setUtility(data.path(UTILITY).asBoolean(car.isUtility()));
523        return utilities.getCar(car, result, locale);
524    }
525
526    /**
527     * Set the properties in the data parameter for the given engine.
528     * <p>
529     * <strong>Note</strong> returns the modified engine because changing the
530     * road or number of an engine changes its name in the JSON representation.
531     *
532     * @param name   the operations id of the engine to change
533     * @param data   engine data to change
534     * @param locale locale to throw exceptions in
535     * @param id     message id set by client
536     * @return the JSON representation of the engine
537     * @throws JsonException if a engine by name cannot be found
538     */
539    public ObjectNode postEngine(String name, JsonNode data, Locale locale, int id) throws JsonException {
540        return postEngine(getEngineByName(name, locale, id), data, locale, id);
541    }
542
543    /**
544     * Set the properties in the data parameter for the given engine.
545     * <p>
546     * <strong>Note</strong> returns the modified engine because changing the
547     * road or number of an engine changes its name in the JSON representation.
548     *
549     * @param engine the engine to change
550     * @param data   engine data to change
551     * @param locale locale to throw exceptions in
552     * @param id     message id set by client
553     * @return the JSON representation of the engine
554     * @throws JsonException if unable to set location
555     */
556    public ObjectNode postEngine(@Nonnull Engine engine, JsonNode data, Locale locale, int id) throws JsonException {
557        // set model early, since setting other values depend on it
558        engine.setModel(data.path(MODEL).asText(engine.getModel()));
559        ObjectNode result = postRollingStock(engine, data, locale, id);
560        return utilities.getEngine(engine, result, locale);
561    }
562
563    /**
564     * Set the properties in the data parameter for the given rolling stock.
565     * <p>
566     * <strong>Note</strong> returns the modified rolling stock because changing
567     * the road or number of a rolling stock changes its name in the JSON
568     * representation.
569     *
570     * @param rs     the rolling stock to change
571     * @param data   rolling stock data to change
572     * @param locale locale to throw exceptions in
573     * @param id     message id set by client
574     * @return the JSON representation of the rolling stock
575     * @throws JsonException if unable to set location
576     */
577    public ObjectNode postRollingStock(@Nonnull RollingStock rs, JsonNode data, Locale locale, int id)
578            throws JsonException {
579        // make changes that can throw an exception first
580        String name = rs.getId();
581        //handle removal (only) from Train
582        JsonNode node = data.path(TRAIN_ID);
583        if (!node.isMissingNode()) {
584            //new value must be null, adding or changing train not supported here
585            if (node.isNull()) {
586                if (rs.getTrain() != null) {
587                    rs.setTrain(null);
588                    rs.setDestination(null, null);
589                    rs.setRouteLocation(null);
590                    rs.setRouteDestination(null);
591                }
592            } else {
593                throw new JsonException(HttpServletResponse.SC_CONFLICT,
594                        Bundle.getMessage(locale, "ErrorRemovingTrain", rs.getId()), id);                 
595            }
596        }
597        //handle change in Location
598        node = data.path(LOCATION);
599        if (!node.isMissingNode()) {
600            //can't move a car that is on a train
601            if (rs.getTrain() != null) {
602                throw new JsonException(HttpServletResponse.SC_CONFLICT,
603                        Bundle.getMessage(locale, "ErrorIsOnTrain", rs.getId(), rs.getTrainName()), id);                 
604            }
605            if (!node.isNull()) {
606                //move car to new location and track
607                Location location = locationManager().getLocationById(node.path(NAME).asText());
608                if (location != null) {
609                    String trackId = node.path(TRACK).path(NAME).asText();
610                    Track track = location.getTrackById(trackId);
611                    if (trackId.isEmpty() || track != null) {
612                        String status = rs.setLocation(location, track);
613                        if (!status.equals(Track.OKAY)) {
614                            throw new JsonException(HttpServletResponse.SC_CONFLICT,
615                                    Bundle.getMessage(locale, "ErrorMovingCar",
616                                            rs.getId(), LOCATION, location.getId(), trackId, status), id);
617                        }
618                    } else {
619                        throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
620                                Bundle.getMessage(locale, "ErrorNotFound", TRACK, trackId), id);
621                    }
622                } else {
623                    throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
624                            Bundle.getMessage(locale, "ErrorNotFound", LOCATION, node.path(NAME).asText()), id);
625                }
626            } else { 
627                //if new location is null, remove car from current location
628                String status = rs.setLocation(null, null);
629                if (!status.equals(Track.OKAY)) {
630                    throw new JsonException(HttpServletResponse.SC_CONFLICT,
631                            Bundle.getMessage(locale, "ErrorMovingCar",
632                                    rs.getId(), LOCATION, null, null, status), id);
633                }                
634            }
635        }
636        //handle change in LocationUnknown
637        node = data.path(LOCATION_UNKNOWN);
638        if (!node.isMissingNode()) {
639            //can't move a car that is on a train
640            if (rs.getTrain() != null) {
641                throw new JsonException(HttpServletResponse.SC_CONFLICT,
642                        Bundle.getMessage(locale, "ErrorIsOnTrain", rs.getId(), rs.getTrainName()), id);                 
643            }            
644            //set LocationUnknown flag to new value
645            rs.setLocationUnknown(data.path(LOCATION_UNKNOWN).asBoolean()); 
646        }
647        //handle change in DESTINATION
648        node = data.path(DESTINATION);
649        if (!node.isMissingNode()) {
650            Location location = locationManager().getLocationById(node.path(NAME).asText());
651            if (location != null) {
652                String trackId = node.path(TRACK).path(NAME).asText();
653                Track track = location.getTrackById(trackId);
654                if (trackId.isEmpty() || track != null) {
655                    String status = rs.setDestination(location, track);
656                    if (!status.equals(Track.OKAY)) {
657                        throw new JsonException(HttpServletResponse.SC_CONFLICT,
658                                Bundle.getMessage(locale, "ErrorMovingCar", rs.getId(),
659                                        DESTINATION, location.getId(), trackId, status),
660                                id);
661                    }
662                } else {
663                    throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
664                            Bundle.getMessage(locale, "ErrorNotFound", TRACK, trackId), id);
665                }
666            } else {
667                throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
668                        Bundle.getMessage(locale, "ErrorNotFound", DESTINATION, node.path(NAME).asText()), id);
669            }
670        }
671        // set properties using the existing property as the default
672        rs.setRoadName(data.path(JsonOperations.ROAD).asText(rs.getRoadName()));
673        rs.setNumber(data.path(JsonOperations.NUMBER).asText(rs.getNumber()));
674        rs.setColor(data.path(COLOR).asText(rs.getColor()));
675        rs.setComment(data.path(JsonOperations.COMMENT).asText(rs.getComment()));
676        rs.setOwnerName(data.path(JsonOperations.OWNER).asText(rs.getOwnerName()));
677        rs.setBuilt(data.path(BUILT).asText(rs.getBuilt()));
678
679        rs.setWeightTons(data.path(WEIGHT_TONS).asText());
680        rs.setRfid(data.path(RFID).asText(rs.getRfid()));
681        rs.setLength(Integer.toString(data.path(LENGTH).asInt(rs.getLengthInteger())));
682        rs.setOutOfService(data.path(OUT_OF_SERVICE).asBoolean(rs.isOutOfService()));
683        rs.setTypeName(data.path(JsonOperations.TYPE).asText(rs.getTypeName()));
684        ObjectNode result = utilities.getRollingStock(rs, locale);
685        if (!rs.getId().equals(name)) {
686            result.put(RENAME, name);
687        }
688        return result;
689    }
690
691    public void deleteCar(@Nonnull String name, @Nonnull Locale locale, int id)
692            throws JsonException {
693        carManager().deregister(getCarByName(name, locale, id));
694    }
695
696    public void deleteEngine(@Nonnull String name, @Nonnull Locale locale, int id)
697            throws JsonException {
698        engineManager().deregister(getEngineByName(name, locale, id));
699    }
700
701    public void deleteLocation(@Nonnull String name, @Nonnull Locale locale, int id)
702            throws JsonException {
703        locationManager().deregister(getLocationByName(name, locale, id));
704    }
705
706    public void deleteTrack(@Nonnull String name, @Nonnull JsonNode data, @Nonnull Locale locale, int id)
707            throws JsonException {
708        Track track = getTrackByName(name, data, locale, id);
709        track.getLocation().deleteTrack(track);
710    }
711
712    @Nonnull
713    protected Car getCarByName(@Nonnull String name, @Nonnull Locale locale, int id) throws JsonException {
714        Car car = carManager().getById(name);
715        if (car == null) {
716            throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
717                    Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, CAR, name), id);
718        }
719        return car;
720    }
721
722    @Nonnull
723    protected Engine getEngineByName(@Nonnull String name, @Nonnull Locale locale, int id)
724            throws JsonException {
725        Engine engine = engineManager().getById(name);
726        if (engine == null) {
727            throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
728                    Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, ENGINE, name), id);
729        }
730        return engine;
731    }
732
733    @Nonnull
734    protected Location getLocationByName(@Nonnull String name, @Nonnull Locale locale, int id)
735            throws JsonException {
736        Location location = locationManager().getLocationById(name);
737        if (location == null) {
738            throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
739                    Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, LOCATION, name), id);
740        }
741        return location;
742    }
743
744    @Nonnull
745    protected Track getTrackByName(@Nonnull String name, @Nonnull JsonNode data, @Nonnull Locale locale,
746            int id) throws JsonException {
747        if (data.path(LOCATION).isMissingNode()) {
748            throw new JsonException(HttpServletResponse.SC_BAD_REQUEST,
749                    Bundle.getMessage(locale, "ErrorMissingAttribute", LOCATION, TRACK), id);
750        }
751        Location location = getLocationByName(data.path(LOCATION).asText(), locale, id);
752        Track track = location.getTrackById(name);
753        if (track == null) {
754            throw new JsonException(HttpServletResponse.SC_NOT_FOUND,
755                    Bundle.getMessage(locale, JsonException.ERROR_NOT_FOUND, TRACK, name), id);
756        }
757        return track;
758    }
759
760    protected CarManager carManager() {
761        return InstanceManager.getDefault(CarManager.class);
762    }
763
764    protected EngineManager engineManager() {
765        return InstanceManager.getDefault(EngineManager.class);
766    }
767
768    protected LocationManager locationManager() {
769        return InstanceManager.getDefault(LocationManager.class);
770    }
771
772    protected TrainManager trainManager() {
773        return InstanceManager.getDefault(TrainManager.class);
774    }
775
776    @Override
777    public JsonNode doSchema(String type, boolean server, JsonRequest request) throws JsonException {
778        int id = request.id;
779        switch (type) {
780            case CAR:
781            case CARS:
782                return doSchema(type,
783                        server,
784                        "jmri/server/json/operations/car-server.json",
785                        "jmri/server/json/operations/car-client.json",
786                        id);
787            case CAR_TYPE:
788            case KERNEL:
789            case ROLLING_STOCK:
790            case TRACK:
791                return doSchema(type,
792                        server,
793                        "jmri/server/json/operations/" + type + "-server.json",
794                        "jmri/server/json/operations/" + type + "-client.json",
795                        id);
796            case ENGINE:
797            case ENGINES:
798                return doSchema(type,
799                        server,
800                        "jmri/server/json/operations/engine-server.json",
801                        "jmri/server/json/operations/engine-client.json",
802                        id);
803            case LOCATION:
804            case LOCATIONS:
805                return doSchema(type,
806                        server,
807                        "jmri/server/json/operations/location-server.json",
808                        "jmri/server/json/operations/location-client.json",
809                        id);
810            case TRAIN:
811            case TRAINS:
812                return doSchema(type,
813                        server,
814                        "jmri/server/json/operations/train-server.json",
815                        "jmri/server/json/operations/train-client.json",
816                        id);
817            default:
818                throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
819                        Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), id);
820        }
821    }
822
823}