Thomas Vachuska
Committed by Gerrit Code Review

ONOS-1235 Enhanced UI extension mechanism to provide message handler factory and…

… took a first cut at the core UiWebSocket mechanism.

Change-Id: Iaad080c5371c3aa5e24a23489b1679d373ec0720
...@@ -55,6 +55,10 @@ ...@@ -55,6 +55,10 @@
55 <artifactId>easymock</artifactId> 55 <artifactId>easymock</artifactId>
56 <scope>test</scope> 56 <scope>test</scope>
57 </dependency> 57 </dependency>
58 + <dependency>
59 + <groupId>org.onosproject</groupId>
60 + <artifactId>onlab-osgi</artifactId>
61 + </dependency>
58 </dependencies> 62 </dependencies>
59 63
60 </project> 64 </project>
......
1 +/*
2 + * Copyright 2015 Open Networking Laboratory
3 + *
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 + */
16 +package org.onosproject.ui;
17 +
18 +import com.fasterxml.jackson.databind.node.ObjectNode;
19 +
20 +/**
21 + * Abstraction of a user interface session connection.
22 + */
23 +public interface UiConnection {
24 +
25 + /**
26 + * Sends the specified JSON message to the user interface client.
27 + *
28 + * @param message message to send
29 + */
30 + void sendMessage(ObjectNode message);
31 +
32 +}
...\ No newline at end of file ...\ No newline at end of file
...@@ -32,16 +32,20 @@ public class UiExtension { ...@@ -32,16 +32,20 @@ public class UiExtension {
32 private final String prefix; 32 private final String prefix;
33 private final ClassLoader classLoader; 33 private final ClassLoader classLoader;
34 private final List<UiView> views; 34 private final List<UiView> views;
35 + private final UiMessageHandlerFactory messageHandlerFactory;
35 36
36 /** 37 /**
37 * Creates a user interface extension for loading CSS and JS injections 38 * Creates a user interface extension for loading CSS and JS injections
38 * from {@code css.html} and {@code js.html} resources, respectively. 39 * from {@code css.html} and {@code js.html} resources, respectively.
39 * 40 *
40 * @param views list of contributed views 41 * @param views list of contributed views
42 + * @param messageHandlerFactory optional message handler factory
41 * @param classLoader class-loader for user interface resources 43 * @param classLoader class-loader for user interface resources
42 */ 44 */
43 - public UiExtension(List<UiView> views, ClassLoader classLoader) { 45 + public UiExtension(List<UiView> views,
44 - this(views, null, classLoader); 46 + UiMessageHandlerFactory messageHandlerFactory,
47 + ClassLoader classLoader) {
48 + this(views, messageHandlerFactory, null, classLoader);
45 } 49 }
46 50
47 /** 51 /**
...@@ -50,11 +54,15 @@ public class UiExtension { ...@@ -50,11 +54,15 @@ public class UiExtension {
50 * {@code prefix/js.html} resources, respectively. 54 * {@code prefix/js.html} resources, respectively.
51 * 55 *
52 * @param views list of user interface views 56 * @param views list of user interface views
57 + * @param messageHandlerFactory optional message handler factory
53 * @param path resource path prefix 58 * @param path resource path prefix
54 * @param classLoader class-loader for user interface resources 59 * @param classLoader class-loader for user interface resources
55 */ 60 */
56 - public UiExtension(List<UiView> views, String path, ClassLoader classLoader) { 61 + public UiExtension(List<UiView> views,
62 + UiMessageHandlerFactory messageHandlerFactory,
63 + String path, ClassLoader classLoader) {
57 this.views = checkNotNull(ImmutableList.copyOf(views), "Views cannot be null"); 64 this.views = checkNotNull(ImmutableList.copyOf(views), "Views cannot be null");
65 + this.messageHandlerFactory = messageHandlerFactory;
58 this.prefix = path != null ? (path + "/") : ""; 66 this.prefix = path != null ? (path + "/") : "";
59 this.classLoader = checkNotNull(classLoader, "Class loader must be specified"); 67 this.classLoader = checkNotNull(classLoader, "Class loader must be specified");
60 } 68 }
...@@ -98,4 +106,12 @@ public class UiExtension { ...@@ -98,4 +106,12 @@ public class UiExtension {
98 return is; 106 return is;
99 } 107 }
100 108
109 + /**
110 + * Returns message handler factory.
111 + *
112 + * @return message handlers
113 + */
114 + public UiMessageHandlerFactory messageHandlerFactory() {
115 + return messageHandlerFactory;
116 + }
101 } 117 }
......
1 +/*
2 + * Copyright 2015 Open Networking Laboratory
3 + *
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 + */
16 +package org.onosproject.ui;
17 +
18 +import com.fasterxml.jackson.databind.node.ObjectNode;
19 +import org.onlab.osgi.ServiceDirectory;
20 +
21 +import java.util.Set;
22 +
23 +import static com.google.common.base.Preconditions.checkArgument;
24 +import static com.google.common.base.Preconditions.checkNotNull;
25 +
26 +/**
27 + * Abstraction of an entity capable of processing a JSON message from the user
28 + * interface client.
29 + * <p>
30 + * The message is a JSON object with the following structure:
31 + * <pre>
32 + * {
33 + * "type": "<em>event-type</em>",
34 + * "sid": "<em>sequence-number</em>",
35 + * "payload": {
36 + * <em>arbitrary JSON object structure</em>
37 + * }
38 + * }
39 + * </pre>
40 + */
41 +public abstract class UiMessageHandler {
42 +
43 + private final Set<String> messageTypes;
44 + private UiConnection connection;
45 + private ServiceDirectory directory;
46 +
47 + /**
48 + * Creates a new message handler for the specified set of message types.
49 + *
50 + * @param messageTypes set of message types
51 + */
52 + protected UiMessageHandler(Set<String> messageTypes) {
53 + this.messageTypes = checkNotNull(messageTypes, "Message types cannot be null");
54 + checkArgument(!messageTypes.isEmpty(), "Message types cannot be empty");
55 + }
56 +
57 + /**
58 + * Returns the set of message types which this handler is capable of
59 + * processing.
60 + *
61 + * @return set of message types
62 + */
63 + public Set<String> messageTypes() {
64 + return messageTypes;
65 + }
66 +
67 + /**
68 + * Processes a JSON message from the user interface client.
69 + *
70 + * @param message JSON message
71 + */
72 + public abstract void process(ObjectNode message);
73 +
74 + /**
75 + * Initializes the handler with the user interface connection and
76 + * service directory context.
77 + *
78 + * @param connection user interface connection
79 + * @param directory service directory
80 + */
81 + public void init(UiConnection connection, ServiceDirectory directory) {
82 + this.connection = connection;
83 + this.directory = directory;
84 + }
85 +
86 + /**
87 + * Destroys the message handler context.
88 + */
89 + public void destroy() {
90 + this.connection = null;
91 + this.directory = null;
92 + }
93 +
94 + /**
95 + * Returns the user interface connection with which this handler was primed.
96 + *
97 + * @return user interface connection
98 + */
99 + public UiConnection connection() {
100 + return connection;
101 + }
102 +
103 + /**
104 + * Returns the user interface connection with which this handler was primed.
105 + *
106 + * @return user interface connection
107 + */
108 + public ServiceDirectory directory() {
109 + return directory;
110 + }
111 +
112 + /**
113 + * Returns implementation of the specified service class.
114 + *
115 + * @param serviceClass service class
116 + * @param <T> type of service
117 + * @return implementation class
118 + * @throws org.onlab.osgi.ServiceNotFoundException if no implementation found
119 + */
120 + protected <T> T get(Class<T> serviceClass) {
121 + return directory.get(serviceClass);
122 + }
123 +
124 +}
1 +/*
2 + * Copyright 2015 Open Networking Laboratory
3 + *
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 + */
16 +package org.onosproject.ui;
17 +
18 +import java.util.Collection;
19 +
20 +/**
21 + * Abstraction of an entity capable of producing a set of message handlers
22 + * specific to the given user interface connection.
23 + */
24 +public interface UiMessageHandlerFactory {
25 +
26 + /**
27 + * Produces a collection of new message handlers.
28 + *
29 + * @return collection of new handlers
30 + */
31 + Collection<UiMessageHandler> newHandlers();
32 +
33 +}
...@@ -21,8 +21,7 @@ import org.junit.Test; ...@@ -21,8 +21,7 @@ import org.junit.Test;
21 import java.io.IOException; 21 import java.io.IOException;
22 22
23 import static com.google.common.io.ByteStreams.toByteArray; 23 import static com.google.common.io.ByteStreams.toByteArray;
24 -import static org.junit.Assert.assertEquals; 24 +import static org.junit.Assert.*;
25 -import static org.junit.Assert.assertTrue;
26 25
27 /** 26 /**
28 * Tests the default user interface extension descriptor. 27 * Tests the default user interface extension descriptor.
...@@ -32,22 +31,25 @@ public class UiExtensionTest { ...@@ -32,22 +31,25 @@ public class UiExtensionTest {
32 @Test 31 @Test
33 public void basics() throws IOException { 32 public void basics() throws IOException {
34 UiExtension ext = new UiExtension(ImmutableList.of(new UiView("foo", "Foo View")), 33 UiExtension ext = new UiExtension(ImmutableList.of(new UiView("foo", "Foo View")),
34 + null,
35 getClass().getClassLoader()); 35 getClass().getClassLoader());
36 String css = new String(toByteArray(ext.css())); 36 String css = new String(toByteArray(ext.css()));
37 assertTrue("incorrect css stream", css.contains("foo-css")); 37 assertTrue("incorrect css stream", css.contains("foo-css"));
38 String js = new String(toByteArray(ext.js())); 38 String js = new String(toByteArray(ext.js()));
39 assertTrue("incorrect js stream", js.contains("foo-js")); 39 assertTrue("incorrect js stream", js.contains("foo-js"));
40 assertEquals("incorrect views stream", "foo", ext.views().get(0).id()); 40 assertEquals("incorrect views stream", "foo", ext.views().get(0).id());
41 + assertNull("incorrect handler factory", ext.messageHandlerFactory());
41 } 42 }
42 43
43 @Test 44 @Test
44 public void withPath() throws IOException { 45 public void withPath() throws IOException {
45 UiExtension ext = new UiExtension(ImmutableList.of(new UiView("foo", "Foo View")), 46 UiExtension ext = new UiExtension(ImmutableList.of(new UiView("foo", "Foo View")),
46 - "custom", getClass().getClassLoader()); 47 + null, "custom", getClass().getClassLoader());
47 String css = new String(toByteArray(ext.css())); 48 String css = new String(toByteArray(ext.css()));
48 assertTrue("incorrect css stream", css.contains("custom-css")); 49 assertTrue("incorrect css stream", css.contains("custom-css"));
49 String js = new String(toByteArray(ext.js())); 50 String js = new String(toByteArray(ext.js()));
50 assertTrue("incorrect js stream", js.contains("custom-js")); 51 assertTrue("incorrect js stream", js.contains("custom-js"));
51 assertEquals("incorrect views stream", "foo", ext.views().get(0).id()); 52 assertEquals("incorrect views stream", "foo", ext.views().get(0).id());
53 + assertNull("incorrect handler factory", ext.messageHandlerFactory());
52 } 54 }
53 } 55 }
...\ No newline at end of file ...\ No newline at end of file
......
...@@ -23,6 +23,7 @@ public interface ServiceDirectory { ...@@ -23,6 +23,7 @@ public interface ServiceDirectory {
23 23
24 /** 24 /**
25 * Returns implementation of the specified service class. 25 * Returns implementation of the specified service class.
26 + *
26 * @param serviceClass service class 27 * @param serviceClass service class
27 * @param <T> type of service 28 * @param <T> type of service
28 * @return implementation class 29 * @return implementation class
......
...@@ -24,6 +24,7 @@ import org.apache.felix.scr.annotations.Deactivate; ...@@ -24,6 +24,7 @@ import org.apache.felix.scr.annotations.Deactivate;
24 import org.apache.felix.scr.annotations.Service; 24 import org.apache.felix.scr.annotations.Service;
25 import org.onosproject.ui.UiExtension; 25 import org.onosproject.ui.UiExtension;
26 import org.onosproject.ui.UiExtensionService; 26 import org.onosproject.ui.UiExtensionService;
27 +import org.onosproject.ui.UiMessageHandlerFactory;
27 import org.onosproject.ui.UiView; 28 import org.onosproject.ui.UiView;
28 import org.slf4j.Logger; 29 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory; 30 import org.slf4j.LoggerFactory;
...@@ -50,12 +51,18 @@ public class UiExtensionManager implements UiExtensionService { ...@@ -50,12 +51,18 @@ public class UiExtensionManager implements UiExtensionService {
50 private final Map<String, UiExtension> views = Maps.newHashMap(); 51 private final Map<String, UiExtension> views = Maps.newHashMap();
51 52
52 // Core views & core extension 53 // Core views & core extension
53 - private final List<UiView> coreViews = of(new UiView("sample", "Sample"), 54 + private final UiExtension core = createCoreExtension();
55 +
56 +
57 + // Creates core UI extension
58 + private static UiExtension createCoreExtension() {
59 + List<UiView> coreViews = of(new UiView("sample", "Sample"),
54 new UiView("topo", "Topology View"), 60 new UiView("topo", "Topology View"),
55 new UiView("device", "Devices")); 61 new UiView("device", "Devices"));
56 - 62 + UiMessageHandlerFactory messageHandlerFactory = null;
57 - private final UiExtension core = new UiExtension(coreViews, "core", 63 + return new UiExtension(coreViews, messageHandlerFactory, "core",
58 - getClass().getClassLoader()); 64 + UiExtensionManager.class.getClassLoader());
65 + }
59 66
60 @Activate 67 @Activate
61 public void activate() { 68 public void activate() {
......
1 +/*
2 + * Copyright 2015 Open Networking Laboratory
3 + *
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 + */
16 +package org.onosproject.ui.impl;
17 +
18 +import com.fasterxml.jackson.databind.ObjectMapper;
19 +import com.fasterxml.jackson.databind.node.ObjectNode;
20 +import org.eclipse.jetty.websocket.WebSocket;
21 +import org.onlab.osgi.ServiceDirectory;
22 +import org.onosproject.ui.UiConnection;
23 +import org.onosproject.ui.UiExtensionService;
24 +import org.onosproject.ui.UiMessageHandler;
25 +import org.slf4j.Logger;
26 +import org.slf4j.LoggerFactory;
27 +
28 +import java.io.IOException;
29 +import java.util.HashMap;
30 +import java.util.Map;
31 +
32 +/**
33 + * Web socket capable of interacting with the GUI.
34 + */
35 +public class UiWebSocket
36 + implements UiConnection, WebSocket.OnTextMessage, WebSocket.OnControl {
37 +
38 + private static final Logger log = LoggerFactory.getLogger(UiWebSocket.class);
39 +
40 + private static final long MAX_AGE_MS = 15000;
41 +
42 + private static final byte PING = 0x9;
43 + private static final byte PONG = 0xA;
44 + private static final byte[] PING_DATA = new byte[]{(byte) 0xde, (byte) 0xad};
45 +
46 + private final ServiceDirectory directory;
47 +
48 + private Connection connection;
49 + private FrameConnection control;
50 +
51 + private final ObjectMapper mapper = new ObjectMapper();
52 +
53 + private long lastActive = System.currentTimeMillis();
54 +
55 + private Map<String, UiMessageHandler> handlers;
56 +
57 + /**
58 + * Creates a new web-socket for serving data to GUI.
59 + *
60 + * @param directory service directory
61 + */
62 + public UiWebSocket(ServiceDirectory directory) {
63 + this.directory = directory;
64 + }
65 +
66 + /**
67 + * Issues a close on the connection.
68 + */
69 + synchronized void close() {
70 + destroyHandlers();
71 + if (connection.isOpen()) {
72 + connection.close();
73 + }
74 + }
75 +
76 + /**
77 + * Indicates if this connection is idle.
78 + *
79 + * @return true if idle or closed
80 + */
81 + synchronized boolean isIdle() {
82 + boolean idle = (System.currentTimeMillis() - lastActive) > MAX_AGE_MS;
83 + if (idle || (connection != null && !connection.isOpen())) {
84 + return true;
85 + } else if (connection != null) {
86 + try {
87 + control.sendControl(PING, PING_DATA, 0, PING_DATA.length);
88 + } catch (IOException e) {
89 + log.warn("Unable to send ping message due to: ", e);
90 + }
91 + }
92 + return false;
93 + }
94 +
95 + @Override
96 + public void onOpen(Connection connection) {
97 + log.info("GUI client connected");
98 + this.connection = connection;
99 + this.control = (FrameConnection) connection;
100 + createHandlers();
101 + }
102 +
103 + @Override
104 + public synchronized void onClose(int closeCode, String message) {
105 + destroyHandlers();
106 + log.info("GUI client disconnected");
107 + }
108 +
109 + @Override
110 + public boolean onControl(byte controlCode, byte[] data, int offset, int length) {
111 + lastActive = System.currentTimeMillis();
112 + return true;
113 + }
114 +
115 + @Override
116 + public void onMessage(String data) {
117 + lastActive = System.currentTimeMillis();
118 + try {
119 + ObjectNode message = (ObjectNode) mapper.reader().readTree(data);
120 + String type = message.path("type").asText("unknown");
121 + UiMessageHandler handler = handlers.get(type);
122 + if (handler != null) {
123 + handler.process(message);
124 + } else {
125 + log.warn("No GUI message handler for type {}", type);
126 + }
127 + } catch (Exception e) {
128 + log.warn("Unable to parse GUI message {} due to {}", data, e);
129 + log.debug("Boom!!!", e);
130 + }
131 + }
132 +
133 + @Override
134 + public void sendMessage(ObjectNode message) {
135 + try {
136 + if (connection.isOpen()) {
137 + connection.sendMessage(message.toString());
138 + }
139 + } catch (IOException e) {
140 + log.warn("Unable to send message {} to GUI due to {}", message, e);
141 + log.debug("Boom!!!", e);
142 + }
143 + }
144 +
145 + // Creates new message handlers.
146 + private void createHandlers() {
147 + handlers = new HashMap<>();
148 + UiExtensionService service = directory.get(UiExtensionService.class);
149 + service.getExtensions().forEach(ext -> ext.messageHandlerFactory().newHandlers().forEach(handler -> {
150 + handler.init(this, directory);
151 + handler.messageTypes().forEach(type -> handlers.put(type, handler));
152 + }));
153 + }
154 +
155 + // Destroys message handlers.
156 + private synchronized void destroyHandlers() {
157 + handlers.forEach((type, handler) -> handler.destroy());
158 + handlers.clear();
159 + }
160 +}
161 +
1 +/*
2 + * Copyright 2015 Open Networking Laboratory
3 + *
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 + */
16 +package org.onosproject.ui.impl;
17 +
18 +import org.eclipse.jetty.websocket.WebSocket;
19 +import org.eclipse.jetty.websocket.WebSocketServlet;
20 +import org.onlab.osgi.DefaultServiceDirectory;
21 +import org.onlab.osgi.ServiceDirectory;
22 +
23 +import javax.servlet.ServletException;
24 +import javax.servlet.http.HttpServletRequest;
25 +import java.util.HashSet;
26 +import java.util.Iterator;
27 +import java.util.Set;
28 +import java.util.Timer;
29 +import java.util.TimerTask;
30 +
31 +/**
32 + * Web socket servlet capable of creating web sockets for the user interface.
33 + */
34 +public class UiWebSocketServlet extends WebSocketServlet {
35 +
36 + private static final long PING_DELAY_MS = 5000;
37 +
38 + private ServiceDirectory directory = new DefaultServiceDirectory();
39 +
40 + private final Set<UiWebSocket> sockets = new HashSet<>();
41 + private final Timer timer = new Timer();
42 + private final TimerTask pruner = new Pruner();
43 +
44 + @Override
45 + public void init() throws ServletException {
46 + super.init();
47 + timer.schedule(pruner, PING_DELAY_MS, PING_DELAY_MS);
48 + }
49 +
50 + @Override
51 + public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
52 + UiWebSocket socket = new UiWebSocket(directory);
53 + synchronized (sockets) {
54 + sockets.add(socket);
55 + }
56 + return socket;
57 + }
58 +
59 + // Task for pruning web-sockets that are idle.
60 + private class Pruner extends TimerTask {
61 + @Override
62 + public void run() {
63 + synchronized (sockets) {
64 + Iterator<UiWebSocket> it = sockets.iterator();
65 + while (it.hasNext()) {
66 + UiWebSocket socket = it.next();
67 + if (socket.isIdle()) {
68 + it.remove();
69 + socket.close();
70 + }
71 + }
72 + }
73 + }
74 + }
75 +}