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