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