001package jmri.server.json;
002
003import static jmri.server.json.JSON.DATA;
004import static jmri.server.json.JSON.DELETE;
005import static jmri.server.json.JSON.GET;
006import static jmri.server.json.JSON.NAME;
007import static jmri.server.json.JSON.POST;
008import static jmri.server.json.JSON.PUT;
009
010import com.fasterxml.jackson.databind.JsonNode;
011import java.beans.PropertyChangeEvent;
012import java.beans.PropertyChangeListener;
013import java.io.IOException;
014import java.util.HashMap;
015import java.util.HashSet;
016import jmri.InstanceManager;
017import jmri.JmriException;
018import jmri.NamedBean;
019import jmri.ReporterManager;
020
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024/**
025 * Abstract implementation of JsonSocketService with specific support for
026 * {@link jmri.NamedBean} objects. Note that services requiring support for
027 * multiple classes of NamedBean cannot extend this class.
028 *
029 * @author Randall Wood (C) 2019
030 * @param <T> the NamedBean class supported by this service
031 * @param <H> the supporting JsonNamedBeanHttpService class
032 */
033public class JsonNamedBeanSocketService<T extends NamedBean, H extends JsonNamedBeanHttpService<T>> extends JsonSocketService<H> {
034
035    protected final HashMap<T, NamedBeanListener> beanListeners = new HashMap<>();
036    protected final ManagerListener managerListener = new ManagerListener();
037    private static final Logger log = LoggerFactory.getLogger(JsonNamedBeanSocketService.class);
038
039    public JsonNamedBeanSocketService(JsonConnection connection, H service) {
040        super(connection, service);
041        service.getManager().addPropertyChangeListener(managerListener);
042    }
043
044    @Override
045    public void onMessage(String type, JsonNode data, JsonRequest request)
046            throws IOException, JmriException, JsonException {
047        String name = data.path(NAME).asText();
048        T bean = null;
049        // protect against a request made with a user name instead of a system name
050        if (!request.method.equals(PUT)) {
051            bean = service.getManager().getBySystemName(name);
052            if (bean == null) {
053                bean = service.getManager().getByUserName(name);
054                if (bean != null) {
055                    // set to warn so users can provide specific feedback to developers of JSON clients
056                    log.warn("{} request for {} made with user name \"{}\"; should use system name", request.method, type, name);
057                    name = bean.getSystemName();
058                } // service will throw appropriate error to client later if bean is still null
059            }
060        }
061        switch (request.method) {
062            case DELETE:
063                service.doDelete(type, name, data, request);
064                break;
065            case POST:
066                connection.sendMessage(service.doPost(type, name, data, request), request.id);
067                break;
068            case PUT:
069                JsonNode message = service.doPut(type, name, data, request);
070                connection.sendMessage(message, request.id);
071                bean = service.getManager().getBySystemName(message.path(DATA).path(NAME).asText());
072                break;
073            case GET:
074            default:
075                connection.sendMessage(service.doGet(type, name, data, request), request.id);
076        }
077        if (!beanListeners.containsKey(bean)) {
078            addListenerToBean(bean);
079        }
080    }
081
082    @Override
083    public void onList(String type, JsonNode data, JsonRequest request) throws IOException, JmriException, JsonException {
084        connection.sendMessage(service.doGetList(type, data, request), request.id);
085    }
086
087    @Override
088    public void onClose() {
089        beanListeners.values().stream().forEach(listener -> listener.bean.removePropertyChangeListener(listener));
090        beanListeners.clear();
091        service.getManager().removePropertyChangeListener(managerListener);
092    }
093
094    protected void addListenerToBean(String name) {
095        addListenerToBean(service.getManager().getBySystemName(name));
096    }
097
098    protected void addListenerToBean(T bean) {
099        if (bean != null) {
100            NamedBeanListener listener = new NamedBeanListener(bean);
101            bean.addPropertyChangeListener(listener);
102            this.beanListeners.put(bean, listener);
103        }
104    }
105
106    protected void removeListenersFromRemovedBeans() {
107        for (T bean : new HashSet<>(beanListeners.keySet())) {
108            if (service.getManager().getBySystemName(bean.getSystemName()) == null) {
109                beanListeners.remove(bean);
110            }
111        }
112    }
113
114    protected class NamedBeanListener implements PropertyChangeListener {
115
116        public final T bean;
117
118        public NamedBeanListener(T bean) {
119            this.bean = bean;
120        }
121
122        @Override
123        public void propertyChange(PropertyChangeEvent evt) {
124            try {
125                connection.sendMessage(service.doGet(this.bean, this.bean.getSystemName(), service.getType(), new JsonRequest(getLocale(), getVersion(), JSON.GET, 0)), 0);
126            } catch (
127                    IOException |
128                    JsonException ex) {
129                // if we get an error, unregister as listener
130                this.bean.removePropertyChangeListener(this);
131                beanListeners.remove(this.bean);
132            }
133        }
134    }
135
136    protected class ManagerListener implements PropertyChangeListener {
137
138        @Override
139        public void propertyChange(PropertyChangeEvent evt) {
140            try {
141                handleChange(evt);
142            } catch (IOException ex) {
143                // if we get an error, unregister as listener
144                log.debug("deregistering reportersListener due to IOException");
145                InstanceManager.getDefault(ReporterManager.class).removePropertyChangeListener(this);
146            }
147        }
148
149        private void handleChange(PropertyChangeEvent evt) throws IOException {
150            try {
151                // send the new list
152                connection.sendMessage(service.doGetList(service.getType(),
153                        service.getObjectMapper().createObjectNode(), new JsonRequest(getLocale(), getVersion(), JSON.GET, 0)), 0);
154                //child added or removed, reset listeners
155                if (evt.getPropertyName().equals("length")) { // NOI18N
156                    removeListenersFromRemovedBeans();
157                }
158            } catch (JsonException ex) {
159                log.warn("json error sending {}: {}", service.getType(), ex.getJsonMessage());
160                connection.sendMessage(ex.getJsonMessage(), 0);
161            }
162        }
163    }
164}