001package jmri.server.json.operations;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.io.IOException;
006import java.util.HashMap;
007import java.util.Objects;
008
009import javax.annotation.Nonnull;
010import javax.servlet.http.HttpServletResponse;
011
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015import com.fasterxml.jackson.databind.JsonNode;
016
017import jmri.InstanceManager;
018import jmri.JmriException;
019import jmri.beans.Identifiable;
020import jmri.beans.PropertyChangeProvider;
021import jmri.jmrit.operations.locations.Location;
022import jmri.jmrit.operations.locations.LocationManager;
023import jmri.jmrit.operations.rollingstock.RollingStock;
024import jmri.jmrit.operations.rollingstock.cars.Car;
025import jmri.jmrit.operations.rollingstock.cars.CarManager;
026import jmri.jmrit.operations.rollingstock.engines.Engine;
027import jmri.jmrit.operations.rollingstock.engines.EngineManager;
028import jmri.jmrit.operations.trains.Train;
029import jmri.jmrit.operations.trains.TrainManager;
030import jmri.server.json.*;
031
032/**
033 * @author Randall Wood (C) 2016, 2019, 2020
034 */
035public class JsonOperationsSocketService extends JsonSocketService<JsonOperationsHttpService> {
036
037    private final HashMap<String, BeanListener<Car>> carListeners = new HashMap<>();
038    private final HashMap<String, BeanListener<Engine>> engineListeners = new HashMap<>();
039    private final HashMap<String, BeanListener<Location>> locationListeners = new HashMap<>();
040    private final HashMap<String, BeanListener<Train>> trainListeners = new HashMap<>();
041    private final CarsListener carsListener = new CarsListener();
042    private final EnginesListener enginesListener = new EnginesListener();
043    private final LocationsListener locationsListener = new LocationsListener();
044    private final TrainsListener trainsListener = new TrainsListener();
045
046    private static final Logger log = LoggerFactory.getLogger(JsonOperationsSocketService.class);
047
048    public JsonOperationsSocketService(JsonConnection connection) {
049        this(connection, new JsonOperationsHttpService(connection.getObjectMapper()));
050    }
051
052    protected JsonOperationsSocketService(JsonConnection connection, JsonOperationsHttpService service) {
053        super(connection, service);
054    }
055
056    @Override
057    public void onMessage(String type, JsonNode data, JsonRequest request)
058            throws IOException, JmriException, JsonException {
059        String name = data.path(JSON.NAME).asText();
060        switch (request.method) {
061            case JSON.GET:
062                connection.sendMessage(service.doGet(type, name, data, request), request.id);
063                break;
064            case JSON.DELETE:
065                service.doDelete(type, name, data, request);
066                // remove listener to object being deleted
067                switch (type) {
068                    case JsonOperations.CAR:
069                        carListeners.remove(name);
070                        break;
071                    case JsonOperations.ENGINE:
072                        engineListeners.remove(name);
073                        break;
074                    case JSON.LOCATION:
075                        locationListeners.remove(name);
076                        break;
077                    case JsonOperations.TRAIN:
078                        trainListeners.remove(name);
079                        break;
080                    default:
081                        // other types ignored
082                        break;
083                }
084                break;
085            case JSON.PUT:
086                connection.sendMessage(service.doPut(type, name, data, request), request.id);
087                break;
088            case JSON.POST:
089            default:
090                connection.sendMessage(service.doPost(type, name, data, request), request.id);
091        }
092        // add listener to name if not already listening
093        if (!request.method.equals(JSON.DELETE)) {
094            if (request.method.equals(JSON.PUT) && name.isEmpty()) {
095                // cover situations where object was just created, so client could not specify correct name
096                if (JsonOperations.CAR.equals(type) || JsonOperations.ENGINE.equals(type)) {
097                    name = RollingStock.createId(data.path(JSON.ROAD).asText(), data.path(JSON.NUMBER).asText());
098                } else if (JSON.LOCATION.equals(type)) {
099                    name = InstanceManager.getDefault(LocationManager.class).getLocationByName(data.path(JSON.USERNAME).asText()).getId();
100                } else {
101                    throw new JsonException(HttpServletResponse.SC_BAD_REQUEST, "ErrorMissingName", request.id);
102                }
103            }
104            switch (type) {
105                case JsonOperations.CAR:
106                    carListeners.computeIfAbsent(name, id -> {
107                        CarListener l = new CarListener(id);
108                        InstanceManager.getDefault(CarManager.class).getById(id).addPropertyChangeListener(l);
109                        return l;
110                    });
111                    break;
112                case JsonOperations.ENGINE:
113                    engineListeners.computeIfAbsent(name, id -> {
114                        EngineListener l = new EngineListener(id);
115                        InstanceManager.getDefault(EngineManager.class).getById(id).addPropertyChangeListener(l);
116                        return l;
117                    });
118                    break;
119                case JSON.LOCATION:
120                    locationListeners.computeIfAbsent(name, id -> {
121                        LocationListener l = new LocationListener(id);
122                        InstanceManager.getDefault(LocationManager.class).getLocationById(id).addPropertyChangeListener(l);
123                        return l;
124                    });
125                    break;
126                case JsonOperations.TRAIN:
127                    trainListeners.computeIfAbsent(name, id -> {
128                        TrainListener l = new TrainListener(id);
129                        InstanceManager.getDefault(TrainManager.class).getTrainById(id).addPropertyChangeListener(l);
130                        return l;
131                    });
132                    break;
133                default:
134                    // other types ignored
135                    break;
136            }
137        }
138    }
139
140    @Override
141    public void onList(String type, JsonNode data, JsonRequest request)
142            throws IOException, JmriException, JsonException {
143        connection.sendMessage(service.doGetList(type, data, request), request.id);
144        switch (type) {
145            case JsonOperations.CAR:
146            case JsonOperations.CARS:
147                log.debug("adding CarsListener");
148                InstanceManager.getDefault(CarManager.class).addPropertyChangeListener(carsListener);
149                break;
150            case JsonOperations.ENGINE:
151            case JsonOperations.ENGINES:
152                log.debug("adding EnginesListener");
153                InstanceManager.getDefault(EngineManager.class).addPropertyChangeListener(enginesListener);
154                break;
155            case JSON.LOCATION:
156            case JSON.LOCATIONS:
157                log.debug("adding LocationsListener");
158                InstanceManager.getDefault(LocationManager.class).addPropertyChangeListener(locationsListener);
159                break;
160            case JsonOperations.TRAIN:
161            case JsonOperations.TRAINS:
162                log.debug("adding TrainsListener");
163                InstanceManager.getDefault(TrainManager.class).addPropertyChangeListener(trainsListener);
164                break;
165            default:
166                break;
167        }
168    }
169
170    @Override
171    public void onClose() {
172        carListeners.values().forEach(listener -> listener.bean.removePropertyChangeListener(listener));
173        carListeners.clear();
174        engineListeners.values().forEach(listener -> listener.bean.removePropertyChangeListener(listener));
175        engineListeners.clear();
176        locationListeners.values().forEach(listener -> listener.bean.removePropertyChangeListener(listener));
177        locationListeners.clear();
178        trainListeners.values().forEach(listener -> listener.bean.removePropertyChangeListener(listener));
179        trainListeners.clear();
180        InstanceManager.getDefault(CarManager.class).removePropertyChangeListener(carsListener);
181        InstanceManager.getDefault(EngineManager.class).removePropertyChangeListener(enginesListener);
182        InstanceManager.getDefault(LocationManager.class).removePropertyChangeListener(locationsListener);
183        InstanceManager.getDefault(TrainManager.class).removePropertyChangeListener(trainsListener);
184    }
185
186    protected abstract class BeanListener<B extends Identifiable & PropertyChangeProvider> implements PropertyChangeListener {
187        
188        protected final B bean;
189        
190        protected BeanListener(@Nonnull B bean) {
191            this.bean = bean;
192        }
193        
194        protected void propertyChange(String type, HashMap<String, BeanListener<B>> map) {
195            try {
196                sendSingleChange(type);
197            } catch (IOException ex) {
198                // stop listening to this object on error
199                bean.removePropertyChangeListener(this);
200                map.remove(bean.getId());
201            }
202        }
203
204        private void sendSingleChange(String type) throws IOException {
205            try {
206                connection.sendMessage(service.doGet(type, bean.getId(),
207                        connection.getObjectMapper().createObjectNode(),
208                        new JsonRequest(getLocale(), getVersion(), JSON.GET, 0)), 0);
209            } catch (JsonException ex) {
210                log.warn("json error sending {}: {}", type, ex.getJsonMessage());
211                connection.sendMessage(ex.getJsonMessage(), 0);
212            }
213        }
214    }
215
216    protected abstract class ManagerListener<M extends PropertyChangeProvider> implements PropertyChangeListener {
217    
218        protected final M manager;
219        
220        protected ManagerListener(@Nonnull M mgr) {
221            Objects.requireNonNull(mgr);
222            this.manager = mgr;
223        }
224
225        protected void propertyChange(String type) {
226            try {
227                sendListChange(type);
228            } catch (IOException ex) {
229                manager.removePropertyChangeListener(this);
230            }
231        }
232
233        private void sendListChange(String type) throws IOException {
234            try {
235                connection.sendMessage(service.doGetList(type, service.getObjectMapper().createObjectNode(),
236                        new JsonRequest(getLocale(), getVersion(), JSON.GET, 0)), 0);
237            } catch (JsonException ex) {
238                log.warn("json error sending {}: {}", type, ex.getJsonMessage());
239                connection.sendMessage(ex.getJsonMessage(), 0);
240            }
241        }
242    }
243
244    private class CarListener extends BeanListener<Car> {
245
246        protected CarListener(String id) {
247            super(InstanceManager.getDefault(CarManager.class).getById(id));
248        }
249
250        @Override
251        public void propertyChange(PropertyChangeEvent evt) {
252            propertyChange(JsonOperations.CAR, carListeners);
253        }
254    }
255
256    private class CarsListener extends ManagerListener<CarManager> {
257
258        protected CarsListener() {
259            super(InstanceManager.getDefault(CarManager.class));
260        }
261
262        @Override
263        public void propertyChange(PropertyChangeEvent evt) {
264            propertyChange(JsonOperations.CAR);
265        }
266    }
267
268    private class EngineListener extends BeanListener<Engine> {
269
270        protected EngineListener(String id) {
271            super(InstanceManager.getDefault(EngineManager.class).getById(id));
272        }
273
274        @Override
275        public void propertyChange(PropertyChangeEvent evt) {
276            propertyChange(JsonOperations.ENGINE, engineListeners);
277        }
278    }
279
280    private class EnginesListener extends ManagerListener<EngineManager> {
281
282        protected EnginesListener() {
283            super(InstanceManager.getDefault(EngineManager.class));
284        }
285
286        @Override
287        public void propertyChange(PropertyChangeEvent evt) {
288            propertyChange(JsonOperations.ENGINE);
289        }
290    }
291
292    private class LocationListener extends BeanListener<Location> {
293
294        protected LocationListener(String id) {
295            super(InstanceManager.getDefault(LocationManager.class).getLocationById(id));
296        }
297
298        @Override
299        public void propertyChange(PropertyChangeEvent evt) {
300            propertyChange(JSON.LOCATION, locationListeners);
301        }
302    }
303
304    private class LocationsListener extends ManagerListener<LocationManager> {
305
306        protected LocationsListener() {
307            super(InstanceManager.getDefault(LocationManager.class));
308        }
309
310        @Override
311        public void propertyChange(PropertyChangeEvent evt) {
312            propertyChange(JSON.LOCATION);
313        }
314    }
315
316    private class TrainListener extends BeanListener<Train> {
317
318        protected TrainListener(String id) {
319            super(InstanceManager.getDefault(TrainManager.class).getTrainById(id));
320        }
321
322        @Override
323        public void propertyChange(PropertyChangeEvent evt) {
324            propertyChange(JsonOperations.TRAIN, trainListeners);
325        }
326    }
327
328    private class TrainsListener extends ManagerListener<TrainManager> {
329
330        protected TrainsListener() {
331            super(InstanceManager.getDefault(TrainManager.class));
332        }
333
334        @Override
335        public void propertyChange(PropertyChangeEvent evt) {
336            propertyChange(JsonOperations.TRAIN);
337        }
338    }
339}