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