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 @@
<artifactId>easymock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.onosproject</groupId>
<artifactId>onlab-osgi</artifactId>
</dependency>
</dependencies>
</project>
......
/*
* Copyright 2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.ui;
import com.fasterxml.jackson.databind.node.ObjectNode;
/**
* Abstraction of a user interface session connection.
*/
public interface UiConnection {
/**
* Sends the specified JSON message to the user interface client.
*
* @param message message to send
*/
void sendMessage(ObjectNode message);
}
\ No newline at end of file
......@@ -32,16 +32,20 @@ public class UiExtension {
private final String prefix;
private final ClassLoader classLoader;
private final List<UiView> views;
private final UiMessageHandlerFactory messageHandlerFactory;
/**
* Creates a user interface extension for loading CSS and JS injections
* from {@code css.html} and {@code js.html} resources, respectively.
*
* @param views list of contributed views
* @param classLoader class-loader for user interface resources
* @param views list of contributed views
* @param messageHandlerFactory optional message handler factory
* @param classLoader class-loader for user interface resources
*/
public UiExtension(List<UiView> views, ClassLoader classLoader) {
this(views, null, classLoader);
public UiExtension(List<UiView> views,
UiMessageHandlerFactory messageHandlerFactory,
ClassLoader classLoader) {
this(views, messageHandlerFactory, null, classLoader);
}
/**
......@@ -49,12 +53,16 @@ public class UiExtension {
* loads CSS and JS injections from {@code path/css.html} and
* {@code prefix/js.html} resources, respectively.
*
* @param views list of user interface views
* @param path resource path prefix
* @param classLoader class-loader for user interface resources
* @param views list of user interface views
* @param messageHandlerFactory optional message handler factory
* @param path resource path prefix
* @param classLoader class-loader for user interface resources
*/
public UiExtension(List<UiView> views, String path, ClassLoader classLoader) {
public UiExtension(List<UiView> views,
UiMessageHandlerFactory messageHandlerFactory,
String path, ClassLoader classLoader) {
this.views = checkNotNull(ImmutableList.copyOf(views), "Views cannot be null");
this.messageHandlerFactory = messageHandlerFactory;
this.prefix = path != null ? (path + "/") : "";
this.classLoader = checkNotNull(classLoader, "Class loader must be specified");
}
......@@ -98,4 +106,12 @@ public class UiExtension {
return is;
}
/**
* Returns message handler factory.
*
* @return message handlers
*/
public UiMessageHandlerFactory messageHandlerFactory() {
return messageHandlerFactory;
}
}
......
/*
* Copyright 2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.ui;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.onlab.osgi.ServiceDirectory;
import java.util.Set;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Abstraction of an entity capable of processing a JSON message from the user
* interface client.
* <p>
* The message is a JSON object with the following structure:
* <pre>
* {
* "type": "<em>event-type</em>",
* "sid": "<em>sequence-number</em>",
* "payload": {
* <em>arbitrary JSON object structure</em>
* }
* }
* </pre>
*/
public abstract class UiMessageHandler {
private final Set<String> messageTypes;
private UiConnection connection;
private ServiceDirectory directory;
/**
* Creates a new message handler for the specified set of message types.
*
* @param messageTypes set of message types
*/
protected UiMessageHandler(Set<String> messageTypes) {
this.messageTypes = checkNotNull(messageTypes, "Message types cannot be null");
checkArgument(!messageTypes.isEmpty(), "Message types cannot be empty");
}
/**
* Returns the set of message types which this handler is capable of
* processing.
*
* @return set of message types
*/
public Set<String> messageTypes() {
return messageTypes;
}
/**
* Processes a JSON message from the user interface client.
*
* @param message JSON message
*/
public abstract void process(ObjectNode message);
/**
* Initializes the handler with the user interface connection and
* service directory context.
*
* @param connection user interface connection
* @param directory service directory
*/
public void init(UiConnection connection, ServiceDirectory directory) {
this.connection = connection;
this.directory = directory;
}
/**
* Destroys the message handler context.
*/
public void destroy() {
this.connection = null;
this.directory = null;
}
/**
* Returns the user interface connection with which this handler was primed.
*
* @return user interface connection
*/
public UiConnection connection() {
return connection;
}
/**
* Returns the user interface connection with which this handler was primed.
*
* @return user interface connection
*/
public ServiceDirectory directory() {
return directory;
}
/**
* Returns implementation of the specified service class.
*
* @param serviceClass service class
* @param <T> type of service
* @return implementation class
* @throws org.onlab.osgi.ServiceNotFoundException if no implementation found
*/
protected <T> T get(Class<T> serviceClass) {
return directory.get(serviceClass);
}
}
/*
* Copyright 2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.ui;
import java.util.Collection;
/**
* Abstraction of an entity capable of producing a set of message handlers
* specific to the given user interface connection.
*/
public interface UiMessageHandlerFactory {
/**
* Produces a collection of new message handlers.
*
* @return collection of new handlers
*/
Collection<UiMessageHandler> newHandlers();
}
......@@ -21,8 +21,7 @@ import org.junit.Test;
import java.io.IOException;
import static com.google.common.io.ByteStreams.toByteArray;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.*;
/**
* Tests the default user interface extension descriptor.
......@@ -32,22 +31,25 @@ public class UiExtensionTest {
@Test
public void basics() throws IOException {
UiExtension ext = new UiExtension(ImmutableList.of(new UiView("foo", "Foo View")),
null,
getClass().getClassLoader());
String css = new String(toByteArray(ext.css()));
assertTrue("incorrect css stream", css.contains("foo-css"));
String js = new String(toByteArray(ext.js()));
assertTrue("incorrect js stream", js.contains("foo-js"));
assertEquals("incorrect views stream", "foo", ext.views().get(0).id());
assertNull("incorrect handler factory", ext.messageHandlerFactory());
}
@Test
public void withPath() throws IOException {
UiExtension ext = new UiExtension(ImmutableList.of(new UiView("foo", "Foo View")),
"custom", getClass().getClassLoader());
null, "custom", getClass().getClassLoader());
String css = new String(toByteArray(ext.css()));
assertTrue("incorrect css stream", css.contains("custom-css"));
String js = new String(toByteArray(ext.js()));
assertTrue("incorrect js stream", js.contains("custom-js"));
assertEquals("incorrect views stream", "foo", ext.views().get(0).id());
assertNull("incorrect handler factory", ext.messageHandlerFactory());
}
}
\ No newline at end of file
......
......@@ -23,8 +23,9 @@ public interface ServiceDirectory {
/**
* Returns implementation of the specified service class.
*
* @param serviceClass service class
* @param <T> type of service
* @param <T> type of service
* @return implementation class
* @throws ServiceNotFoundException if no implementation found
*/
......
......@@ -24,6 +24,7 @@ import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Service;
import org.onosproject.ui.UiExtension;
import org.onosproject.ui.UiExtensionService;
import org.onosproject.ui.UiMessageHandlerFactory;
import org.onosproject.ui.UiView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -50,12 +51,18 @@ public class UiExtensionManager implements UiExtensionService {
private final Map<String, UiExtension> views = Maps.newHashMap();
// Core views & core extension
private final List<UiView> coreViews = of(new UiView("sample", "Sample"),
new UiView("topo", "Topology View"),
new UiView("device", "Devices"));
private final UiExtension core = createCoreExtension();
private final UiExtension core = new UiExtension(coreViews, "core",
getClass().getClassLoader());
// Creates core UI extension
private static UiExtension createCoreExtension() {
List<UiView> coreViews = of(new UiView("sample", "Sample"),
new UiView("topo", "Topology View"),
new UiView("device", "Devices"));
UiMessageHandlerFactory messageHandlerFactory = null;
return new UiExtension(coreViews, messageHandlerFactory, "core",
UiExtensionManager.class.getClassLoader());
}
@Activate
public void activate() {
......
/*
* Copyright 2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.ui.impl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.jetty.websocket.WebSocket;
import org.onlab.osgi.ServiceDirectory;
import org.onosproject.ui.UiConnection;
import org.onosproject.ui.UiExtensionService;
import org.onosproject.ui.UiMessageHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* Web socket capable of interacting with the GUI.
*/
public class UiWebSocket
implements UiConnection, WebSocket.OnTextMessage, WebSocket.OnControl {
private static final Logger log = LoggerFactory.getLogger(UiWebSocket.class);
private static final long MAX_AGE_MS = 15000;
private static final byte PING = 0x9;
private static final byte PONG = 0xA;
private static final byte[] PING_DATA = new byte[]{(byte) 0xde, (byte) 0xad};
private final ServiceDirectory directory;
private Connection connection;
private FrameConnection control;
private final ObjectMapper mapper = new ObjectMapper();
private long lastActive = System.currentTimeMillis();
private Map<String, UiMessageHandler> handlers;
/**
* Creates a new web-socket for serving data to GUI.
*
* @param directory service directory
*/
public UiWebSocket(ServiceDirectory directory) {
this.directory = directory;
}
/**
* Issues a close on the connection.
*/
synchronized void close() {
destroyHandlers();
if (connection.isOpen()) {
connection.close();
}
}
/**
* Indicates if this connection is idle.
*
* @return true if idle or closed
*/
synchronized boolean isIdle() {
boolean idle = (System.currentTimeMillis() - lastActive) > MAX_AGE_MS;
if (idle || (connection != null && !connection.isOpen())) {
return true;
} else if (connection != null) {
try {
control.sendControl(PING, PING_DATA, 0, PING_DATA.length);
} catch (IOException e) {
log.warn("Unable to send ping message due to: ", e);
}
}
return false;
}
@Override
public void onOpen(Connection connection) {
log.info("GUI client connected");
this.connection = connection;
this.control = (FrameConnection) connection;
createHandlers();
}
@Override
public synchronized void onClose(int closeCode, String message) {
destroyHandlers();
log.info("GUI client disconnected");
}
@Override
public boolean onControl(byte controlCode, byte[] data, int offset, int length) {
lastActive = System.currentTimeMillis();
return true;
}
@Override
public void onMessage(String data) {
lastActive = System.currentTimeMillis();
try {
ObjectNode message = (ObjectNode) mapper.reader().readTree(data);
String type = message.path("type").asText("unknown");
UiMessageHandler handler = handlers.get(type);
if (handler != null) {
handler.process(message);
} else {
log.warn("No GUI message handler for type {}", type);
}
} catch (Exception e) {
log.warn("Unable to parse GUI message {} due to {}", data, e);
log.debug("Boom!!!", e);
}
}
@Override
public void sendMessage(ObjectNode message) {
try {
if (connection.isOpen()) {
connection.sendMessage(message.toString());
}
} catch (IOException e) {
log.warn("Unable to send message {} to GUI due to {}", message, e);
log.debug("Boom!!!", e);
}
}
// Creates new message handlers.
private void createHandlers() {
handlers = new HashMap<>();
UiExtensionService service = directory.get(UiExtensionService.class);
service.getExtensions().forEach(ext -> ext.messageHandlerFactory().newHandlers().forEach(handler -> {
handler.init(this, directory);
handler.messageTypes().forEach(type -> handlers.put(type, handler));
}));
}
// Destroys message handlers.
private synchronized void destroyHandlers() {
handlers.forEach((type, handler) -> handler.destroy());
handlers.clear();
}
}
/*
* Copyright 2015 Open Networking Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onosproject.ui.impl;
import org.eclipse.jetty.websocket.WebSocket;
import org.eclipse.jetty.websocket.WebSocketServlet;
import org.onlab.osgi.DefaultServiceDirectory;
import org.onlab.osgi.ServiceDirectory;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
/**
* Web socket servlet capable of creating web sockets for the user interface.
*/
public class UiWebSocketServlet extends WebSocketServlet {
private static final long PING_DELAY_MS = 5000;
private ServiceDirectory directory = new DefaultServiceDirectory();
private final Set<UiWebSocket> sockets = new HashSet<>();
private final Timer timer = new Timer();
private final TimerTask pruner = new Pruner();
@Override
public void init() throws ServletException {
super.init();
timer.schedule(pruner, PING_DELAY_MS, PING_DELAY_MS);
}
@Override
public WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) {
UiWebSocket socket = new UiWebSocket(directory);
synchronized (sockets) {
sockets.add(socket);
}
return socket;
}
// Task for pruning web-sockets that are idle.
private class Pruner extends TimerTask {
@Override
public void run() {
synchronized (sockets) {
Iterator<UiWebSocket> it = sockets.iterator();
while (it.hasNext()) {
UiWebSocket socket = it.next();
if (socket.isIdle()) {
it.remove();
socket.close();
}
}
}
}
}
}