001package jmri.server.json.roster;
002
003import static jmri.server.json.JSON.ADD;
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;
009import static jmri.server.json.JSON.REMOVE;
010
011import com.fasterxml.jackson.databind.JsonNode;
012import com.fasterxml.jackson.databind.node.NullNode;
013import com.fasterxml.jackson.databind.node.ObjectNode;
014import java.beans.PropertyChangeEvent;
015import java.beans.PropertyChangeListener;
016import java.io.IOException;
017import javax.servlet.http.HttpServletResponse;
018import jmri.JmriException;
019import jmri.beans.PropertyChangeProvider;
020import jmri.jmrit.roster.Roster;
021import jmri.jmrit.roster.RosterEntry;
022import jmri.server.json.JsonConnection;
023import jmri.server.json.JsonException;
024import jmri.server.json.JsonRequest;
025import jmri.server.json.JsonSocketService;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029/**
030 * Listen for changes in the roster and notify subscribed clients of changes to
031 * the roster, including roster groups
032 *
033 * @author Randall Wood Copyright (C) 2014, 2016
034 */
035public class JsonRosterSocketService extends JsonSocketService<JsonRosterHttpService> {
036
037    private static final Logger log = LoggerFactory.getLogger(JsonRosterSocketService.class);
038    private final JsonRosterListener rosterListener = new JsonRosterListener();
039    private final JsonRosterEntryListener rosterEntryListener = new JsonRosterEntryListener();
040    private final JsonRosterGroupsListener rosterGroupsListener = new JsonRosterGroupsListener();
041    private boolean listening = false;
042
043    public JsonRosterSocketService(JsonConnection connection) {
044        super(connection, new JsonRosterHttpService(connection.getObjectMapper()));
045    }
046
047    public void listen() {
048        if (!listening) {
049            Roster.getDefault().addPropertyChangeListener(rosterListener);
050            Roster.getDefault().addPropertyChangeListener(rosterGroupsListener);
051            Roster.getDefault().getEntriesInGroup(Roster.ALLENTRIES).stream().forEach(re -> {
052                re.addPropertyChangeListener(rosterEntryListener);
053                re.addPropertyChangeListener(rosterGroupsListener);
054            });
055            listening = true;
056        }
057    }
058
059    @Override
060    public void onMessage(String type, JsonNode data, JsonRequest request)
061            throws IOException, JmriException, JsonException {
062        switch (request.method) {
063            case DELETE:
064                throw new JsonException(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
065                        Bundle.getMessage(request.locale, "DeleteNotAllowed", type), request.id);
066            case POST:
067                if (JsonRoster.ROSTER_ENTRY.equals(type)) {
068                    connection
069                            .sendMessage(service.postRosterEntry(request.locale, data.path(NAME).asText(), data, request.id), request.id);
070                } else {
071                    throw new JsonException(HttpServletResponse.SC_NOT_IMPLEMENTED,
072                            Bundle.getMessage(request.locale, "MethodNotImplemented", request.method, type), request.id);
073                }
074                break;
075            case PUT:
076                throw new JsonException(HttpServletResponse.SC_NOT_IMPLEMENTED,
077                        Bundle.getMessage(request.locale, "MethodNotImplemented", request.method, type), request.id);
078            case GET:
079                switch (type) {
080                    case JsonRoster.ROSTER:
081                        connection.sendMessage(service.getRoster(request.locale, data, request.id), request.id);
082                        break;
083                    case JsonRoster.ROSTER_ENTRY:
084                        connection.sendMessage(service.getRosterEntry(request.locale, data.path(NAME).asText(), request.id),
085                                request.id);
086                        break;
087                    case JsonRoster.ROSTER_GROUP:
088                        connection.sendMessage(service.getRosterGroup(request.locale, data.path(NAME).asText(), request.id),
089                                request.id);
090                        break;
091                    case JsonRoster.ROSTER_GROUPS:
092                        connection.sendMessage(service.getRosterGroups(request), request.id);
093                        break;
094                    default:
095                        throw new JsonException(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
096                                Bundle.getMessage(request.locale, JsonException.ERROR_UNKNOWN_TYPE, type), request.id);
097                }
098                break;
099            default:
100                throw new JsonException(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
101                        Bundle.getMessage(request.locale, "UnknownMethod", request.method), request.id);
102        }
103        listen();
104    }
105
106    @Override
107    public void onList(String type, JsonNode data, JsonRequest request)
108            throws IOException, JmriException, JsonException {
109        connection.sendMessage(service.doGetList(type, data, request), request.id);
110        listen();
111    }
112
113    @Override
114    public void onClose() {
115        Roster.getDefault().removePropertyChangeListener(rosterListener);
116        Roster.getDefault().removePropertyChangeListener(rosterGroupsListener);
117
118        Roster.getDefault().getEntriesInGroup(Roster.ALLENTRIES).stream().forEach(re -> {
119            re.removePropertyChangeListener(rosterEntryListener);
120            re.removePropertyChangeListener(rosterGroupsListener);
121        });
122        listening = false;
123    }
124
125    private class JsonRosterEntryListener implements PropertyChangeListener {
126
127        @Override
128        public void propertyChange(PropertyChangeEvent evt) {
129            try {
130                sendRosterUpdate(evt);
131            } catch (IOException ex) {
132                onClose();
133            }
134        }
135
136        private void sendRosterUpdate(PropertyChangeEvent evt) throws IOException {
137            try {
138                if (evt.getPropertyName().equals(RosterEntry.ID)) {
139                    // send old roster entry and new roster entry to client
140                    // as roster changes
141                    ObjectNode data = connection.getObjectMapper().createObjectNode();
142                    RosterEntry old = new RosterEntry((RosterEntry) evt.getSource(), (String) evt.getOldValue());
143                    data.set(ADD, service.getRosterEntry(connection.getLocale(), (RosterEntry) evt.getSource(), 0));
144                    data.set(REMOVE, service.getRosterEntry(connection.getLocale(), old, 0));
145                    log.debug("Sending add and remove rosterEntry for {} ({} => {})", evt.getPropertyName(),
146                            evt.getOldValue(), evt.getNewValue());
147                    connection.sendMessage(service.message(JsonRoster.ROSTER, data, 0), 0);
148                } else if (!evt.getPropertyName().equals(RosterEntry.DATE_UPDATED) &&
149                        !evt.getPropertyName().equals(RosterEntry.FILENAME) &&
150                        !evt.getPropertyName().equals(RosterEntry.COMMENT)) {
151                    // don't send comment changes
152                    log.debug("Sending updated rosterEntry for {} ({} => {})", evt.getPropertyName(),
153                            evt.getOldValue(), evt.getNewValue());
154                    connection.sendMessage(
155                            service.getRosterEntry(connection.getLocale(), (RosterEntry) evt.getSource(), 0), 0);
156                }
157            } catch (JsonException ex) {
158                connection.sendMessage(ex.getJsonMessage(), 0);
159            }
160        }
161    }
162
163    private class JsonRosterListener implements PropertyChangeListener {
164
165        @Override
166        public void propertyChange(PropertyChangeEvent evt) {
167            try {
168                sendRosterUpdate(evt);
169            } catch (IOException ex) {
170                onClose();
171            }
172        }
173
174        private void sendRosterUpdate(PropertyChangeEvent evt) throws IOException {
175            try {
176                ObjectNode data = connection.getObjectMapper().createObjectNode();
177                if (evt.getPropertyName().equals(Roster.ADD)) {
178                    data.set(ADD,
179                            service.getRosterEntry(connection.getLocale(), (RosterEntry) evt.getNewValue(), 0));
180                    ((PropertyChangeProvider) evt.getNewValue()).addPropertyChangeListener(rosterEntryListener);
181                    connection.sendMessage(service.message(JsonRoster.ROSTER, data, 0), 0);
182                } else if (evt.getPropertyName().equals(Roster.REMOVE)) {
183                    data.set(REMOVE,
184                            service.getRosterEntry(connection.getLocale(), (RosterEntry) evt.getOldValue(), 0));
185                    connection.sendMessage(service.message(JsonRoster.ROSTER, data, 0), 0);
186                } else if (!evt.getPropertyName().equals(Roster.SAVED) &&
187                        !evt.getPropertyName().equals(Roster.ROSTER_GROUP_ADDED) &&
188                        !evt.getPropertyName().equals(Roster.ROSTER_GROUP_REMOVED) &&
189                        !evt.getPropertyName().equals(Roster.ROSTER_GROUP_RENAMED)) {
190                    // catch all events other than SAVED and ROSTER_GROUP_*
191                    // (handled elsewhere)
192                    connection.sendMessage(service.getRoster(connection.getLocale(), NullNode.getInstance(), 0), 0);
193                }
194            } catch (JsonException ex) {
195                connection.sendMessage(ex.getJsonMessage(), 0);
196            }
197        }
198    }
199
200    private class JsonRosterGroupsListener implements PropertyChangeListener {
201
202        @Override
203        public void propertyChange(PropertyChangeEvent evt) {
204            try {
205                // handle direct roster change events
206                if (evt.getPropertyName().equals(Roster.ROSTER_GROUP_ADDED) ||
207                        evt.getPropertyName().equals(Roster.ROSTER_GROUP_REMOVED) ||
208                        evt.getPropertyName().equals(Roster.ROSTER_GROUP_RENAMED)) {
209                    sendGroupsUpdate();
210                    // handle event names of format
211                    // "attributeUpdated:RosterGroup:GROUPNAME"
212                } else if (evt.getPropertyName().startsWith(RosterEntry.ATTRIBUTE_UPDATED)) {
213                    String attrName = evt.getPropertyName().substring(RosterEntry.ATTRIBUTE_UPDATED.length());
214                    if (attrName.startsWith(Roster.ROSTER_GROUP_PREFIX)) {
215                        String groupName = attrName.substring(Roster.ROSTER_GROUP_PREFIX.length());
216                        if (Roster.getDefault().getRosterGroups().containsKey(groupName)) {
217                            sendGroupNameUpdate(groupName);
218                        }
219                    }
220                    // handle attribute deleted, old value is of form
221                    // "RosterGroup:GROUPNAME"
222                } else if (evt.getPropertyName().startsWith(RosterEntry.ATTRIBUTE_DELETED) &&
223                        ((String) evt.getOldValue()).startsWith(Roster.ROSTER_GROUP_PREFIX)) {
224                    String groupName = ((String) evt.getOldValue()).substring(Roster.ROSTER_GROUP_PREFIX.length());
225                    if (Roster.getDefault().getRosterGroups().containsKey(groupName)) {
226                        sendGroupNameUpdate(groupName);
227                    }
228                }
229            } catch (IOException ex) {
230                onClose();
231            }
232        }
233
234        private void sendGroupsUpdate() throws IOException {
235            try {
236                connection.sendMessage(service.getRosterGroups(new JsonRequest(getLocale(), getVersion(), GET, 0)), 0);
237            } catch (JsonException ex) {
238                connection.sendMessage(ex.getJsonMessage(), 0);
239            }
240        }
241
242        private void sendGroupNameUpdate(String groupName) throws IOException {
243            try {
244                log.debug("sending changed rosterGroup {} and updated group array", groupName);
245                connection.sendMessage(service.getRosterGroup(getLocale(), groupName, 0), 0);
246                sendGroupsUpdate();
247            } catch (JsonException ex) {
248                connection.sendMessage(ex.getJsonMessage(), 0);
249            }
250        }
251    }
252
253}