ONOS-1235 Enhanced UI extension mechanism to provide message handler factory and took a first cut at the core UiWebSocket mechanism.

Change-Id: Iaad080c5371c3aa5e24a23489b1679d373ec0720
diff --git a/core/api/pom.xml b/core/api/pom.xml
index e02bb78..f6a1572 100644
--- a/core/api/pom.xml
+++ b/core/api/pom.xml
@@ -55,6 +55,10 @@
             <artifactId>easymock</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
+            <artifactId>onlab-osgi</artifactId>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/core/api/src/main/java/org/onosproject/ui/UiConnection.java b/core/api/src/main/java/org/onosproject/ui/UiConnection.java
new file mode 100644
index 0000000..719b788
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/UiConnection.java
@@ -0,0 +1,32 @@
+/*
+ * 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
diff --git a/core/api/src/main/java/org/onosproject/ui/UiExtension.java b/core/api/src/main/java/org/onosproject/ui/UiExtension.java
index e97a4d0..d4589a4 100644
--- a/core/api/src/main/java/org/onosproject/ui/UiExtension.java
+++ b/core/api/src/main/java/org/onosproject/ui/UiExtension.java
@@ -32,16 +32,20 @@
     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 @@
      * 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 @@
         return is;
     }
 
+    /**
+     * Returns message handler factory.
+     *
+     * @return message handlers
+     */
+    public UiMessageHandlerFactory messageHandlerFactory() {
+        return messageHandlerFactory;
+    }
 }
diff --git a/core/api/src/main/java/org/onosproject/ui/UiMessageHandler.java b/core/api/src/main/java/org/onosproject/ui/UiMessageHandler.java
new file mode 100644
index 0000000..14d4f3d
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/UiMessageHandler.java
@@ -0,0 +1,124 @@
+/*
+ * 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);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onosproject/ui/UiMessageHandlerFactory.java b/core/api/src/main/java/org/onosproject/ui/UiMessageHandlerFactory.java
new file mode 100644
index 0000000..522daa8
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/UiMessageHandlerFactory.java
@@ -0,0 +1,33 @@
+/*
+ * 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();
+
+}
diff --git a/core/api/src/test/java/org/onosproject/ui/UiExtensionTest.java b/core/api/src/test/java/org/onosproject/ui/UiExtensionTest.java
index 1ea1d2c..3bd9797 100644
--- a/core/api/src/test/java/org/onosproject/ui/UiExtensionTest.java
+++ b/core/api/src/test/java/org/onosproject/ui/UiExtensionTest.java
@@ -21,8 +21,7 @@
 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 @@
     @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
diff --git a/utils/osgi/src/main/java/org/onlab/osgi/ServiceDirectory.java b/utils/osgi/src/main/java/org/onlab/osgi/ServiceDirectory.java
index 80591c0..ffff4a7 100644
--- a/utils/osgi/src/main/java/org/onlab/osgi/ServiceDirectory.java
+++ b/utils/osgi/src/main/java/org/onlab/osgi/ServiceDirectory.java
@@ -23,8 +23,9 @@
 
     /**
      * 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
      */
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
index 3604919..3b9156d 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
@@ -24,6 +24,7 @@
 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 @@
     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() {
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
new file mode 100644
index 0000000..9b11b42
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
@@ -0,0 +1,161 @@
+/*
+ * 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();
+    }
+}
+
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java
new file mode 100644
index 0000000..f262202
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java
@@ -0,0 +1,75 @@
+/*
+ * 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();
+                    }
+                }
+            }
+        }
+    }
+}