blob: 328207d172b503f577930ee3d933211cd5e8513f [file] [log] [blame]
Thomas Vachuska3553b302015-03-07 14:49:43 -08001/*
Brian O'Connor5ab426f2016-04-09 01:19:45 -07002 * Copyright 2015-present Open Networking Laboratory
Thomas Vachuska3553b302015-03-07 14:49:43 -08003 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package org.onosproject.ui.impl;
17
18import com.fasterxml.jackson.databind.ObjectMapper;
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -070019import com.fasterxml.jackson.databind.node.ArrayNode;
Thomas Vachuska3553b302015-03-07 14:49:43 -080020import com.fasterxml.jackson.databind.node.ObjectNode;
21import org.eclipse.jetty.websocket.WebSocket;
22import org.onlab.osgi.ServiceDirectory;
Thomas Vachuskafc52fec2015-05-18 19:13:56 -070023import org.onlab.osgi.ServiceNotFoundException;
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -070024import org.onosproject.cluster.ClusterService;
25import org.onosproject.cluster.ControllerNode;
Simon Hunt8add9ee2016-09-20 17:05:07 -070026import org.onosproject.ui.GlyphConstants;
Thomas Vachuska3553b302015-03-07 14:49:43 -080027import org.onosproject.ui.UiConnection;
28import org.onosproject.ui.UiExtensionService;
Simon Hunta0ddb022015-05-01 09:53:01 -070029import org.onosproject.ui.UiMessageHandler;
Simon Hunt7715e892016-04-12 19:55:32 -070030import org.onosproject.ui.UiMessageHandlerFactory;
Thomas Vachuska92b016b2016-05-20 11:37:57 -070031import org.onosproject.ui.UiTopoLayoutService;
Simon Hunte05cae42015-07-23 17:35:24 -070032import org.onosproject.ui.UiTopoOverlayFactory;
Simon Hunt537bc762016-12-20 12:15:13 -080033import org.onosproject.ui.impl.topo.Topo2Jsonifier;
Thomas Vachuska92b016b2016-05-20 11:37:57 -070034import org.onosproject.ui.impl.topo.UiTopoSession;
35import org.onosproject.ui.impl.topo.model.UiSharedTopologyModel;
36import org.onosproject.ui.model.topo.UiTopoLayout;
Thomas Vachuska3553b302015-03-07 14:49:43 -080037import org.slf4j.Logger;
38import org.slf4j.LoggerFactory;
39
40import java.io.IOException;
41import java.util.HashMap;
42import java.util.Map;
43
44/**
Simon Hunt7092cc42016-04-06 18:40:17 -070045 * Web socket capable of interacting with the Web UI.
Thomas Vachuska3553b302015-03-07 14:49:43 -080046 */
47public class UiWebSocket
48 implements UiConnection, WebSocket.OnTextMessage, WebSocket.OnControl {
49
50 private static final Logger log = LoggerFactory.getLogger(UiWebSocket.class);
51
Simon Hunt7715e892016-04-12 19:55:32 -070052 private static final String EVENT = "event";
53 private static final String SID = "sid";
54 private static final String PAYLOAD = "payload";
55 private static final String UNKNOWN = "unknown";
56
57 private static final String ID = "id";
58 private static final String IP = "ip";
59 private static final String CLUSTER_NODES = "clusterNodes";
60 private static final String USER = "user";
61 private static final String BOOTSTRAP = "bootstrap";
62
Simon Huntd7395c82016-10-20 17:54:01 -070063 private static final String TOPO = "topo";
Thomas Vachuska92b016b2016-05-20 11:37:57 -070064
Thomas Vachuska1a989c12015-06-09 18:29:22 -070065 private static final long MAX_AGE_MS = 30_000;
Thomas Vachuska3553b302015-03-07 14:49:43 -080066
67 private static final byte PING = 0x9;
68 private static final byte PONG = 0xA;
69 private static final byte[] PING_DATA = new byte[]{(byte) 0xde, (byte) 0xad};
70
Simon Hunt7092cc42016-04-06 18:40:17 -070071 private final ObjectMapper mapper = new ObjectMapper();
Thomas Vachuska3553b302015-03-07 14:49:43 -080072 private final ServiceDirectory directory;
Thomas Vachuska92b016b2016-05-20 11:37:57 -070073 private final UiTopoSession topoSession;
Thomas Vachuska3553b302015-03-07 14:49:43 -080074
75 private Connection connection;
76 private FrameConnection control;
Thomas Vachuska0af26912016-03-21 21:37:30 -070077 private String userName;
Thomas Vachuska92b016b2016-05-20 11:37:57 -070078 private String currentView;
Thomas Vachuska3553b302015-03-07 14:49:43 -080079
Thomas Vachuska3553b302015-03-07 14:49:43 -080080 private long lastActive = System.currentTimeMillis();
81
Simon Hunta0ddb022015-05-01 09:53:01 -070082 private Map<String, UiMessageHandler> handlers;
Simon Hunte05cae42015-07-23 17:35:24 -070083 private TopoOverlayCache overlayCache;
Thomas Vachuska3553b302015-03-07 14:49:43 -080084
85 /**
Simon Huntcda9c032016-04-11 10:32:54 -070086 * Creates a new web-socket for serving data to the Web UI.
Thomas Vachuska3553b302015-03-07 14:49:43 -080087 *
88 * @param directory service directory
Simon Hunt7715e892016-04-12 19:55:32 -070089 * @param userName user name of the logged-in user
Thomas Vachuska3553b302015-03-07 14:49:43 -080090 */
Thomas Vachuska0af26912016-03-21 21:37:30 -070091 public UiWebSocket(ServiceDirectory directory, String userName) {
Thomas Vachuska3553b302015-03-07 14:49:43 -080092 this.directory = directory;
Thomas Vachuska0af26912016-03-21 21:37:30 -070093 this.userName = userName;
Simon Hunt537bc762016-12-20 12:15:13 -080094
95 Topo2Jsonifier t2json = new Topo2Jsonifier(directory);
96 UiSharedTopologyModel sharedModel = directory.get(UiSharedTopologyModel.class);
97 UiTopoLayoutService layoutService = directory.get(UiTopoLayoutService.class);
Simon Huntbbd0f462017-01-10 14:50:22 -080098
99 topoSession = new UiTopoSession(this, t2json, sharedModel, layoutService);
100
101 // FIXME: this is temporary to prevent unhandled events being set to GUI...
102 // while Topo2 is still under development
103 topoSession.enableEvent(false);
Thomas Vachuska0af26912016-03-21 21:37:30 -0700104 }
105
106 @Override
107 public String userName() {
108 return userName;
Thomas Vachuska3553b302015-03-07 14:49:43 -0800109 }
110
Thomas Vachuska92b016b2016-05-20 11:37:57 -0700111 @Override
112 public UiTopoLayout currentLayout() {
113 return topoSession.currentLayout();
114 }
115
116 @Override
117 public void setCurrentLayout(UiTopoLayout topoLayout) {
118 topoSession.setCurrentLayout(topoLayout);
119 }
120
121 @Override
122 public String currentView() {
123 return currentView;
124 }
125
126 @Override
127 public void setCurrentView(String viewId) {
128 currentView = viewId;
129 topoSession.enableEvent(viewId.equals(TOPO));
130 }
131
Thomas Vachuska3553b302015-03-07 14:49:43 -0800132 /**
Simon Huntd5b96732016-07-08 13:22:27 -0700133 * Provides a reference to the topology session.
134 *
135 * @return topo session reference
136 */
137 public UiTopoSession topoSession() {
138 return topoSession;
139 }
140
141 /**
Thomas Vachuska3553b302015-03-07 14:49:43 -0800142 * Issues a close on the connection.
143 */
144 synchronized void close() {
Simon Hunte05cae42015-07-23 17:35:24 -0700145 destroyHandlersAndOverlays();
Thomas Vachuska3553b302015-03-07 14:49:43 -0800146 if (connection.isOpen()) {
147 connection.close();
148 }
149 }
150
151 /**
152 * Indicates if this connection is idle.
153 *
154 * @return true if idle or closed
155 */
156 synchronized boolean isIdle() {
Simon Huntda580882015-05-12 20:58:18 -0700157 long quietFor = System.currentTimeMillis() - lastActive;
158 boolean idle = quietFor > MAX_AGE_MS;
Thomas Vachuska3553b302015-03-07 14:49:43 -0800159 if (idle || (connection != null && !connection.isOpen())) {
Simon Huntda580882015-05-12 20:58:18 -0700160 log.debug("IDLE (or closed) websocket [{} ms]", quietFor);
Thomas Vachuska3553b302015-03-07 14:49:43 -0800161 return true;
162 } else if (connection != null) {
163 try {
164 control.sendControl(PING, PING_DATA, 0, PING_DATA.length);
165 } catch (IOException e) {
166 log.warn("Unable to send ping message due to: ", e);
167 }
168 }
169 return false;
170 }
171
172 @Override
Satish K598c28d2015-11-24 17:20:40 +0530173 public synchronized void onOpen(Connection connection) {
Thomas Vachuska3553b302015-03-07 14:49:43 -0800174 this.connection = connection;
175 this.control = (FrameConnection) connection;
Thomas Vachuskafc52fec2015-05-18 19:13:56 -0700176 try {
Thomas Vachuska92b016b2016-05-20 11:37:57 -0700177 topoSession.init();
Simon Hunte05cae42015-07-23 17:35:24 -0700178 createHandlersAndOverlays();
Simon Hunt7715e892016-04-12 19:55:32 -0700179 sendBootstrapData();
180 log.info("GUI client connected -- user <{}>", userName);
Thomas Vachuskafc52fec2015-05-18 19:13:56 -0700181
182 } catch (ServiceNotFoundException e) {
Brian O'Connor75deea62015-06-24 16:09:17 -0400183 log.warn("Unable to open GUI connection; services have been shut-down", e);
Thomas Vachuskafc52fec2015-05-18 19:13:56 -0700184 this.connection.close();
185 this.connection = null;
186 this.control = null;
187 }
Thomas Vachuska3553b302015-03-07 14:49:43 -0800188 }
189
190 @Override
191 public synchronized void onClose(int closeCode, String message) {
Thomas Vachuska92b016b2016-05-20 11:37:57 -0700192 topoSession.destroy();
Simon Hunte05cae42015-07-23 17:35:24 -0700193 destroyHandlersAndOverlays();
Simon Huntda580882015-05-12 20:58:18 -0700194 log.info("GUI client disconnected [close-code={}, message={}]",
Simon Huntd5b96732016-07-08 13:22:27 -0700195 closeCode, message);
Thomas Vachuska3553b302015-03-07 14:49:43 -0800196 }
197
198 @Override
199 public boolean onControl(byte controlCode, byte[] data, int offset, int length) {
200 lastActive = System.currentTimeMillis();
201 return true;
202 }
203
204 @Override
205 public void onMessage(String data) {
206 lastActive = System.currentTimeMillis();
207 try {
208 ObjectNode message = (ObjectNode) mapper.reader().readTree(data);
Simon Hunt7715e892016-04-12 19:55:32 -0700209 String type = message.path(EVENT).asText(UNKNOWN);
Simon Hunta0ddb022015-05-01 09:53:01 -0700210 UiMessageHandler handler = handlers.get(type);
Thomas Vachuska3553b302015-03-07 14:49:43 -0800211 if (handler != null) {
Simon Huntd5b96732016-07-08 13:22:27 -0700212 log.debug("RX message: {}", message);
Thomas Vachuska3553b302015-03-07 14:49:43 -0800213 handler.process(message);
214 } else {
215 log.warn("No GUI message handler for type {}", type);
216 }
217 } catch (Exception e) {
218 log.warn("Unable to parse GUI message {} due to {}", data, e);
219 log.debug("Boom!!!", e);
220 }
221 }
222
223 @Override
Thomas Vachuska35fa3d42015-04-30 10:11:47 -0700224 public synchronized void sendMessage(ObjectNode message) {
Thomas Vachuska3553b302015-03-07 14:49:43 -0800225 try {
226 if (connection.isOpen()) {
227 connection.sendMessage(message.toString());
Simon Huntd5b96732016-07-08 13:22:27 -0700228 log.debug("TX message: {}", message);
Thomas Vachuska3553b302015-03-07 14:49:43 -0800229 }
230 } catch (IOException e) {
231 log.warn("Unable to send message {} to GUI due to {}", message, e);
232 log.debug("Boom!!!", e);
233 }
234 }
235
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700236 @Override
Simon Huntc5368182017-01-10 13:32:04 -0800237 public synchronized void sendMessage(String type, ObjectNode payload) {
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700238 ObjectNode message = mapper.createObjectNode();
Simon Hunt7715e892016-04-12 19:55:32 -0700239 message.put(EVENT, type);
Simon Hunt7715e892016-04-12 19:55:32 -0700240 message.set(PAYLOAD, payload != null ? payload : mapper.createObjectNode());
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700241 sendMessage(message);
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700242 }
243
Thomas Vachuska3553b302015-03-07 14:49:43 -0800244 // Creates new message handlers.
Simon Hunte05cae42015-07-23 17:35:24 -0700245 private synchronized void createHandlersAndOverlays() {
Simon Hunt7715e892016-04-12 19:55:32 -0700246 log.debug("Creating handlers and overlays...");
Thomas Vachuska3553b302015-03-07 14:49:43 -0800247 handlers = new HashMap<>();
Simon Hunte05cae42015-07-23 17:35:24 -0700248 overlayCache = new TopoOverlayCache();
249
Thomas Vachuska3553b302015-03-07 14:49:43 -0800250 UiExtensionService service = directory.get(UiExtensionService.class);
Thomas Vachuska329af532015-03-10 02:08:33 -0700251 service.getExtensions().forEach(ext -> {
252 UiMessageHandlerFactory factory = ext.messageHandlerFactory();
253 if (factory != null) {
254 factory.newHandlers().forEach(handler -> {
Thomas Vachuskac4178cc2015-12-10 11:43:32 -0800255 try {
256 handler.init(this, directory);
257 handler.messageTypes().forEach(type -> handlers.put(type, handler));
Simon Hunte05cae42015-07-23 17:35:24 -0700258
Thomas Vachuskac4178cc2015-12-10 11:43:32 -0800259 // need to inject the overlay cache into topology message handler
Simon Hunt537bc762016-12-20 12:15:13 -0800260 // TODO: code for Topo2ViewMessageHandler required here
Thomas Vachuskac4178cc2015-12-10 11:43:32 -0800261 if (handler instanceof TopologyViewMessageHandler) {
262 ((TopologyViewMessageHandler) handler).setOverlayCache(overlayCache);
263 }
264 } catch (Exception e) {
265 log.warn("Unable to setup handler {} due to", handler, e);
Simon Hunte05cae42015-07-23 17:35:24 -0700266 }
Thomas Vachuska329af532015-03-10 02:08:33 -0700267 });
268 }
Simon Hunte05cae42015-07-23 17:35:24 -0700269
270 UiTopoOverlayFactory overlayFactory = ext.topoOverlayFactory();
271 if (overlayFactory != null) {
272 overlayFactory.newOverlays().forEach(overlayCache::add);
273 }
Thomas Vachuska329af532015-03-10 02:08:33 -0700274 });
Simon Hunte05cae42015-07-23 17:35:24 -0700275 log.debug("#handlers = {}, #overlays = {}", handlers.size(),
Simon Huntd5b96732016-07-08 13:22:27 -0700276 overlayCache.size());
Thomas Vachuska3553b302015-03-07 14:49:43 -0800277 }
278
279 // Destroys message handlers.
Simon Hunte05cae42015-07-23 17:35:24 -0700280 private synchronized void destroyHandlersAndOverlays() {
Simon Hunt7715e892016-04-12 19:55:32 -0700281 log.debug("Destroying handlers and overlays...");
Thomas Vachuska3553b302015-03-07 14:49:43 -0800282 handlers.forEach((type, handler) -> handler.destroy());
283 handlers.clear();
Simon Hunte05cae42015-07-23 17:35:24 -0700284
285 if (overlayCache != null) {
286 overlayCache.destroy();
287 overlayCache = null;
288 }
Thomas Vachuska3553b302015-03-07 14:49:43 -0800289 }
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700290
Simon Hunt7715e892016-04-12 19:55:32 -0700291 // Sends initial information (username and cluster member information)
292 // to allow GUI to display logged-in user, and to be able to
293 // fail-over to an alternate cluster member if necessary.
294 private void sendBootstrapData() {
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700295 ClusterService service = directory.get(ClusterService.class);
296 ArrayNode instances = mapper.createArrayNode();
297
298 for (ControllerNode node : service.getNodes()) {
299 ObjectNode instance = mapper.createObjectNode()
Simon Hunt7715e892016-04-12 19:55:32 -0700300 .put(ID, node.id().toString())
301 .put(IP, node.ip().toString())
Simon Hunt8add9ee2016-09-20 17:05:07 -0700302 .put(GlyphConstants.UI_ATTACHED,
Simon Huntd5b96732016-07-08 13:22:27 -0700303 node.equals(service.getLocalNode()));
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700304 instances.add(instance);
305 }
306
307 ObjectNode payload = mapper.createObjectNode();
Simon Hunt7715e892016-04-12 19:55:32 -0700308 payload.set(CLUSTER_NODES, instances);
309 payload.put(USER, userName);
Simon Huntc5368182017-01-10 13:32:04 -0800310 sendMessage(BOOTSTRAP, payload);
Thomas Vachuskab6acc7b2015-03-11 09:11:21 -0700311 }
312
Thomas Vachuska3553b302015-03-07 14:49:43 -0800313}
314