Adding support for user interface extensions.

Change-Id: I1e41d16efc11be31ad4c2fb0c09e86e3dfd26706
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/AbstractInjectionResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/AbstractInjectionResource.java
new file mode 100644
index 0000000..2ed4ecd
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/AbstractInjectionResource.java
@@ -0,0 +1,78 @@
+/*
+ * 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.onlab.rest.BaseResource;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Resource for serving semi-static resources.
+ */
+public class AbstractInjectionResource extends BaseResource {
+
+    /**
+     * Returns the index into the supplied string where the end of the
+     * specified pattern is located.
+     *
+     * @param string      string to split
+     * @param start       index where to start looking for pattern
+     * @param stopPattern optional pattern where to stop
+     */
+    protected int split(String string, int start, String stopPattern) {
+        int i = stopPattern != null ? string.indexOf(stopPattern, start) : string.length();
+        checkArgument(i > 0, "Unable to locate stop pattern %s", stopPattern);
+        return i + (stopPattern != null ? stopPattern.length() : 0);
+    }
+
+    /**
+     * Produces an input stream from the bytes of the specified sub-string.
+     *
+     * @param string source string
+     * @param start  index where to start stream
+     * @param end    index where to end stream
+     */
+    protected InputStream stream(String string, int start, int end) {
+        return new ByteArrayInputStream(string.substring(start, end).getBytes());
+    }
+
+    /**
+     * Auxiliary enumeration to sequence input streams.
+     */
+    protected class StreamEnumeration implements Enumeration<InputStream> {
+        private final Iterator<InputStream> iterator;
+
+        StreamEnumeration(List<InputStream> streams) {
+            this.iterator = streams.iterator();
+        }
+
+        @Override
+        public boolean hasMoreElements() {
+            return iterator.hasNext();
+        }
+
+        @Override
+        public InputStream nextElement() {
+            return iterator.next();
+        }
+    }
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/AbstractTableRow.java b/web/gui/src/main/java/org/onosproject/ui/impl/AbstractTableRow.java
new file mode 100644
index 0000000..66db400
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/AbstractTableRow.java
@@ -0,0 +1,65 @@
+/*
+ * 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 java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * Provides a partial implementation of {@link TableRow}.
+ */
+public abstract class AbstractTableRow implements TableRow {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private final Map<String, String> data = new HashMap<>();
+
+    @Override
+    public String get(String key) {
+        return data.get(key);
+    }
+
+    @Override
+    public ObjectNode toJsonNode() {
+        ObjectNode result = MAPPER.createObjectNode();
+        for (String id : columnIds()) {
+            result.put(id, data.get(id));
+        }
+        return result;
+    }
+
+    /**
+     * Subclasses must provide the list of column IDs.
+     *
+     * @return array of column IDs
+     */
+    protected abstract String[] columnIds();
+
+    /**
+     * Add a column ID to value binding.
+     *
+     * @param id the column ID
+     * @param value the cell value
+     */
+    protected void add(String id, String value) {
+        data.put(id, value);
+    }
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/DeviceGuiResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceGuiResource.java
new file mode 100644
index 0000000..d409ebf
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceGuiResource.java
@@ -0,0 +1,79 @@
+/*
+ * 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.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.rest.BaseResource;
+import org.onosproject.net.Device;
+import org.onosproject.net.device.DeviceService;
+
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * UI REST resource for interacting with the inventory of infrastructure devices.
+ */
+@Path("device")
+public class DeviceGuiResource extends BaseResource {
+
+    private static final String DEVICES = "devices";
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+
+    // return the list of devices in appropriate sorted order
+    @GET
+    @Produces("application/json")
+    public Response getDevices(
+            @DefaultValue("id") @QueryParam("sortCol") String colId,
+            @DefaultValue("asc") @QueryParam("sortDir") String dir
+    ) {
+        DeviceService service = get(DeviceService.class);
+        TableRow[] rows = generateTableRows(service);
+        RowComparator rc = new RowComparator(colId, RowComparator.direction(dir));
+        Arrays.sort(rows, rc);
+        ArrayNode devices = generateArrayNode(rows);
+        ObjectNode rootNode = MAPPER.createObjectNode();
+        rootNode.set(DEVICES, devices);
+
+        return Response.ok(rootNode.toString()).build();
+    }
+
+    private ArrayNode generateArrayNode(TableRow[] rows) {
+        ArrayNode devices = MAPPER.createArrayNode();
+        for (TableRow r : rows) {
+            devices.add(r.toJsonNode());
+        }
+        return devices;
+    }
+
+    private TableRow[] generateTableRows(DeviceService service) {
+        List<TableRow> list = new ArrayList<>();
+        for (Device dev : service.getDevices()) {
+            list.add(new DeviceTableRow(service, dev));
+        }
+        return list.toArray(new TableRow[list.size()]);
+    }
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/DeviceTableRow.java b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceTableRow.java
new file mode 100644
index 0000000..7e67e67
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceTableRow.java
@@ -0,0 +1,73 @@
+/*
+ * 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.onosproject.net.Device;
+import org.onosproject.net.device.DeviceService;
+
+/**
+ * TableRow implementation for {@link Device devices}.
+ */
+public class DeviceTableRow extends AbstractTableRow {
+
+    private static final String ID = "id";
+    private static final String AVAILABLE = "available";
+    private static final String AVAILABLE_IID = "_iconid_available";
+    private static final String TYPE_IID = "_iconid_type";
+    private static final String DEV_ICON_PREFIX = "devIcon_";
+    private static final String ROLE = "role";
+    private static final String MFR = "mfr";
+    private static final String HW = "hw";
+    private static final String SW = "sw";
+    private static final String SERIAL = "serial";
+    private static final String PROTOCOL = "protocol";
+    private static final String CHASSISID = "chassisid";
+
+    private static final String[] COL_IDS = {
+            ID, AVAILABLE, AVAILABLE_IID, TYPE_IID, ROLE,
+            MFR, HW, SW, SERIAL, PROTOCOL, CHASSISID
+    };
+
+    private static final String ICON_ID_ONLINE = "deviceOnline";
+    private static final String ICON_ID_OFFLINE = "deviceOffline";
+
+    public DeviceTableRow(DeviceService service, Device d) {
+        boolean available = service.isAvailable(d.id());
+        String iconId = available ? ICON_ID_ONLINE : ICON_ID_OFFLINE;
+
+        add(ID, d.id().toString());
+        add(AVAILABLE, Boolean.toString(available));
+        add(AVAILABLE_IID, iconId);
+        add(TYPE_IID, getTypeIconId(d));
+        add(ROLE, service.getRole(d.id()).toString());
+        add(MFR, d.manufacturer());
+        add(HW, d.hwVersion());
+        add(SW, d.swVersion());
+        add(SERIAL, d.serialNumber());
+        add(PROTOCOL, d.annotations().value(PROTOCOL));
+        add(CHASSISID, d.chassisId().toString());
+    }
+
+    private String getTypeIconId(Device d) {
+        return DEV_ICON_PREFIX + d.type().toString();
+    }
+
+    @Override
+    protected String[] columnIds() {
+        return COL_IDS;
+    }
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/GuiWebSocketServlet.java b/web/gui/src/main/java/org/onosproject/ui/impl/GuiWebSocketServlet.java
new file mode 100644
index 0000000..5a660e0
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/GuiWebSocketServlet.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2014,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 various sockets for the user interface.
+ */
+public class GuiWebSocketServlet extends WebSocketServlet {
+
+    private static final long PING_DELAY_MS = 5000;
+
+    private ServiceDirectory directory = new DefaultServiceDirectory();
+
+    private final Set<TopologyViewWebSocket> 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) {
+        TopologyViewWebSocket socket = new TopologyViewWebSocket(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<TopologyViewWebSocket> it = sockets.iterator();
+                while (it.hasNext()) {
+                    TopologyViewWebSocket socket = it.next();
+                    if (socket.isIdle()) {
+                        it.remove();
+                        socket.close();
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/MainExtResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/MainExtResource.java
new file mode 100644
index 0000000..a1eae41
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/MainExtResource.java
@@ -0,0 +1,109 @@
+/*
+ * 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.onosproject.ui.UiExtension;
+import org.onosproject.ui.UiExtensionService;
+import org.onosproject.ui.UiView;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.SequenceInputStream;
+
+import static com.google.common.collect.ImmutableList.of;
+import static com.google.common.io.ByteStreams.toByteArray;
+
+/**
+ * Resource for serving the dynamically composed onos.js.
+ */
+@Path("/")
+public class MainExtResource extends AbstractInjectionResource {
+
+    private static final String MAIN_JS = "/onos-template.js";
+    private static final String NAV_HTML = "/nav-template.html";
+
+    private static final String INJECT_VIEW_IDS = "// {INJECTED-VIEW-IDS}";
+    private static final String INJECT_VIEW_ITEMS = "<!-- {INJECTED-VIEW-NAV} -->";
+
+    private static final String NAV_FORMAT =
+            "    <li> <a ng-click=\"navCtrl.hideNav()\" href=\"#/%s\">%s</a></li>";
+
+    @Path("/onos.js")
+    @GET
+    @Produces(MediaType.TEXT_HTML)
+    public Response getMainModule() throws IOException {
+        UiExtensionService service = get(UiExtensionService.class);
+        InputStream jsTemplate = getClass().getClassLoader().getResourceAsStream(MAIN_JS);
+        String js = new String(toByteArray(jsTemplate));
+
+        int p1 = split(js, 0, INJECT_VIEW_IDS);
+        int p2 = split(js, p1, null);
+
+        StreamEnumeration streams =
+                new StreamEnumeration(of(stream(js, 0, p1),
+                                         includeViewIds(service),
+                                         stream(js, p1, p2)));
+
+        return Response.ok(new SequenceInputStream(streams)).build();
+    }
+
+    // Produces an input stream including view id injections from all extensions.
+    private InputStream includeViewIds(UiExtensionService service) {
+        StringBuilder sb = new StringBuilder("\n");
+        for (UiExtension extension : service.getExtensions()) {
+            for (UiView view : extension.views()) {
+                sb.append("        '").append(view.id()).append("',");
+            }
+        }
+        return new ByteArrayInputStream(sb.toString().getBytes());
+    }
+
+    @Path("/nav/nav.html")
+    @GET
+    @Produces(MediaType.TEXT_HTML)
+    public Response getNavigation() throws IOException {
+        UiExtensionService service = get(UiExtensionService.class);
+        InputStream navTemplate = getClass().getClassLoader().getResourceAsStream(NAV_HTML);
+        String js = new String(toByteArray(navTemplate));
+
+        int p1 = split(js, 0, INJECT_VIEW_ITEMS);
+        int p2 = split(js, p1, null);
+
+        StreamEnumeration streams =
+                new StreamEnumeration(of(stream(js, 0, p1),
+                                         includeNavItems(service),
+                                         stream(js, p1, p2)));
+
+        return Response.ok(new SequenceInputStream(streams)).build();
+    }
+
+    // Produces an input stream including nav item injections from all extensions.
+    private InputStream includeNavItems(UiExtensionService service) {
+        StringBuilder sb = new StringBuilder("\n");
+        for (UiExtension extension : service.getExtensions()) {
+            for (UiView view : extension.views()) {
+                sb.append(String.format(NAV_FORMAT, view.id(), view.label()));
+            }
+        }
+        return new ByteArrayInputStream(sb.toString().getBytes());
+    }
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java
new file mode 100644
index 0000000..da4079f
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java
@@ -0,0 +1,85 @@
+/*
+ * 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.google.common.collect.ImmutableList;
+import org.onosproject.ui.UiExtension;
+import org.onosproject.ui.UiExtensionService;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.SequenceInputStream;
+
+import static com.google.common.collect.ImmutableList.of;
+import static com.google.common.io.ByteStreams.toByteArray;
+
+/**
+ * Resource for serving the dynamically composed index.html.
+ */
+@Path("/")
+public class MainIndexResource extends AbstractInjectionResource {
+
+    private static final String INDEX = "/index-template.html";
+
+    private static final String INJECT_CSS = "<!-- {INJECTED-STYLESHEETS} -->";
+    private static final String INJECT_JS = "<!-- {INJECTED-JAVASCRIPT} -->";
+
+    @Path("/")
+    @GET
+    @Produces(MediaType.TEXT_HTML)
+    public Response getMainIndex() throws IOException {
+        UiExtensionService service = get(UiExtensionService.class);
+        InputStream indexTemplate = getClass().getClassLoader().getResourceAsStream(INDEX);
+        String index = new String(toByteArray(indexTemplate));
+
+        int p1 = split(index, 0, INJECT_JS);
+        int p2 = split(index, p1, INJECT_CSS);
+        int p3 = split(index, p2, null);
+
+        StreamEnumeration streams =
+                new StreamEnumeration(of(stream(index, 0, p1),
+                                         includeJs(service),
+                                         stream(index, p1, p2),
+                                         includeCss(service),
+                                         stream(index, p2, p3)));
+
+        return Response.ok(new SequenceInputStream(streams)).build();
+    }
+
+    // Produces an input stream including CSS injections from all extensions.
+    private InputStream includeCss(UiExtensionService service) {
+        ImmutableList.Builder<InputStream> builder = ImmutableList.builder();
+        for (UiExtension extension : service.getExtensions()) {
+            builder.add(extension.css());
+        }
+        return new SequenceInputStream(new StreamEnumeration(builder.build()));
+    }
+
+    // Produces an input stream including JS injections from all extensions.
+    private InputStream includeJs(UiExtensionService service) {
+        ImmutableList.Builder<InputStream> builder = ImmutableList.builder();
+        for (UiExtension extension : service.getExtensions()) {
+            builder.add(extension.js());
+        }
+        return new SequenceInputStream(new StreamEnumeration(builder.build()));
+    }
+
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/MainViewResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/MainViewResource.java
new file mode 100644
index 0000000..26b5d8c
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/MainViewResource.java
@@ -0,0 +1,59 @@
+/*
+ * 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.onosproject.ui.UiExtension;
+import org.onosproject.ui.UiExtensionService;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_OCTET_STREAM;
+import static javax.ws.rs.core.MediaType.TEXT_HTML;
+
+/**
+ * Resource for serving the dynamically composed onos.js.
+ */
+@Path("/")
+public class MainViewResource extends AbstractInjectionResource {
+
+    private static final String CONTENT_TYPE = "Content-Type";
+    private static final String STYLESHEET = "text/css";
+    private static final String SCRIPT = "text/javascript";
+
+    @Path("{view}/{resource}")
+    @GET
+    public Response getViewResource(@PathParam("view") String viewId,
+                                    @PathParam("resource") String resource) throws IOException {
+        UiExtensionService service = get(UiExtensionService.class);
+        UiExtension extension = service.getViewExtension(viewId);
+        return extension != null ?
+                Response.ok(extension.resource(viewId, resource))
+                        .header(CONTENT_TYPE, contentType(resource)).build() :
+                Response.status(Response.Status.NOT_FOUND).build();
+    }
+
+    static String contentType(String resource) {
+        return resource.endsWith(".html") ? TEXT_HTML :
+                resource.endsWith(".css") ? STYLESHEET :
+                        resource.endsWith(".js") ? SCRIPT :
+                                APPLICATION_OCTET_STREAM;
+    }
+
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/RowComparator.java b/web/gui/src/main/java/org/onosproject/ui/impl/RowComparator.java
new file mode 100644
index 0000000..b588f48
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/RowComparator.java
@@ -0,0 +1,68 @@
+/*
+ * 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 java.util.Comparator;
+
+/**
+ * Comparator for {@link TableRow}.
+ */
+public class RowComparator implements Comparator<TableRow> {
+    public static enum Direction { ASC, DESC }
+
+    public static final String DESC_STR = "desc";
+
+    private final String colId;
+    private final Direction dir;
+
+    /**
+     * Constructs a comparator for table rows that uses the given
+     * column ID and direction.
+     *
+     * @param colId the column to sort on
+     * @param dir the direction to sort in
+     */
+    public RowComparator(String colId, Direction dir) {
+        if (colId == null || dir == null) {
+            throw new NullPointerException("Null parameters not allowed");
+        }
+        this.colId = colId;
+        this.dir = dir;
+    }
+
+    @Override
+    public int compare(TableRow a, TableRow b) {
+        String cellA = a.get(colId);
+        String cellB = b.get(colId);
+
+        if (dir.equals(Direction.ASC)) {
+            return cellA.compareTo(cellB);
+        }
+        return cellB.compareTo(cellA);
+    }
+
+    /**
+     * Returns the sort direction constant for the given string.
+     * The expected strings are "asc" and "desc"; defaults to "asc".
+     *
+     * @param s the direction as a string
+     * @return the constant
+     */
+    public static Direction direction(String s) {
+        return DESC_STR.equals(s) ? Direction.DESC : Direction.ASC;
+    }
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TableRow.java b/web/gui/src/main/java/org/onosproject/ui/impl/TableRow.java
new file mode 100644
index 0000000..81a14ba
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TableRow.java
@@ -0,0 +1,40 @@
+/*
+ * 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.node.ObjectNode;
+
+/**
+ * Defines a table row abstraction to support sortable tables on the GUI.
+ */
+public interface TableRow {
+    /**
+     * Returns the value of the cell for the given column ID.
+     *
+     * @param key the column ID
+     * @return the cell value
+     */
+    String get(String key);
+
+    /**
+     * Returns this table row in the form of a JSON object.
+     *
+     * @return the JSON node
+     */
+    ObjectNode toJsonNode();
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyResource.java
new file mode 100644
index 0000000..95f9a07
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyResource.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2014,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.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.rest.BaseResource;
+import org.slf4j.Logger;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import java.util.Map;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Topology viewer resource.
+ */
+@Path("topology")
+public class TopologyResource extends BaseResource {
+
+    private static final Logger log = getLogger(TopologyResource.class);
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+
+    @Path("/geoloc")
+    @GET
+    @Produces("application/json")
+    public Response getGeoLocations() {
+        ObjectNode rootNode = mapper.createObjectNode();
+        ArrayNode devices = mapper.createArrayNode();
+        ArrayNode hosts = mapper.createArrayNode();
+
+        Map<String, ObjectNode> metaUi = TopologyViewMessages.getMetaUi();
+        for (String id : metaUi.keySet()) {
+            ObjectNode memento = metaUi.get(id);
+            if (id.charAt(17) == '/') {
+                addGeoData(hosts, "id", id, memento);
+            } else {
+                addGeoData(devices, "uri", id, memento);
+            }
+        }
+
+        rootNode.set("devices", devices);
+        rootNode.set("hosts", hosts);
+        return Response.ok(rootNode.toString()).build();
+    }
+
+    private void addGeoData(ArrayNode array, String idField, String id,
+                            ObjectNode memento) {
+        ObjectNode node = mapper.createObjectNode().put(idField, id);
+        ObjectNode annot = mapper.createObjectNode();
+        node.set("annotations", annot);
+        try {
+            annot.put("latitude", memento.get("lat").asDouble())
+                    .put("longitude", memento.get("lng").asDouble());
+            array.add(node);
+        } catch (Exception e) {
+            log.debug("Skipping geo entry");
+        }
+    }
+
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewIntentFilter.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewIntentFilter.java
new file mode 100644
index 0000000..f8b1d6c
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewIntentFilter.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright 2014,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.onosproject.net.ConnectPoint;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.Link;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.intent.HostToHostIntent;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentService;
+import org.onosproject.net.intent.LinkCollectionIntent;
+import org.onosproject.net.intent.MultiPointToSinglePointIntent;
+import org.onosproject.net.intent.OpticalConnectivityIntent;
+import org.onosproject.net.intent.PathIntent;
+import org.onosproject.net.intent.PointToPointIntent;
+import org.onosproject.net.link.LinkService;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import static org.onosproject.net.intent.IntentState.INSTALLED;
+
+/**
+ * Auxiliary facility to query the intent service based on the specified
+ * set of end-station hosts, edge points or infrastructure devices.
+ */
+public class TopologyViewIntentFilter {
+
+    private final IntentService intentService;
+    private final DeviceService deviceService;
+    private final HostService hostService;
+    private final LinkService linkService;
+
+    /**
+     * Crreates an intent filter.
+     *
+     * @param intentService intent service reference
+     * @param deviceService device service reference
+     * @param hostService   host service reference
+     * @param linkService   link service reference
+     */
+    TopologyViewIntentFilter(IntentService intentService,
+                             DeviceService deviceService,
+                             HostService hostService, LinkService linkService) {
+        this.intentService = intentService;
+        this.deviceService = deviceService;
+        this.hostService = hostService;
+        this.linkService = linkService;
+    }
+
+    /**
+     * Finds all path (host-to-host or point-to-point) intents that pertains
+     * to the given hosts.
+     *
+     * @param hosts         set of hosts to query by
+     * @param devices       set of devices to query by
+     * @param sourceIntents collection of intents to search
+     * @return set of intents that 'match' all hosts and devices given
+     */
+    List<Intent> findPathIntents(Set<Host> hosts, Set<Device> devices,
+                                 Iterable<Intent> sourceIntents) {
+        // Derive from this the set of edge connect points.
+        Set<ConnectPoint> edgePoints = getEdgePoints(hosts);
+
+        // Iterate over all intents and produce a set that contains only those
+        // intents that target all selected hosts or derived edge connect points.
+        return getIntents(hosts, devices, edgePoints, sourceIntents);
+    }
+
+
+    // Produces a set of edge points from the specified set of hosts.
+    private Set<ConnectPoint> getEdgePoints(Set<Host> hosts) {
+        Set<ConnectPoint> edgePoints = new HashSet<>();
+        for (Host host : hosts) {
+            edgePoints.add(host.location());
+        }
+        return edgePoints;
+    }
+
+    // Produces a list of intents that target all selected hosts, devices or connect points.
+    private List<Intent> getIntents(Set<Host> hosts, Set<Device> devices,
+                                    Set<ConnectPoint> edgePoints,
+                                    Iterable<Intent> sourceIntents) {
+        List<Intent> intents = new ArrayList<>();
+        if (hosts.isEmpty() && devices.isEmpty()) {
+            return intents;
+        }
+
+        Set<OpticalConnectivityIntent> opticalIntents = new HashSet<>();
+
+        // Search through all intents and see if they are relevant to our search.
+        for (Intent intent : sourceIntents) {
+            if (intentService.getIntentState(intent.key()) == INSTALLED) {
+                boolean isRelevant = false;
+                if (intent instanceof HostToHostIntent) {
+                    isRelevant = isIntentRelevantToHosts((HostToHostIntent) intent, hosts) &&
+                            isIntentRelevantToDevices(intent, devices);
+                } else if (intent instanceof PointToPointIntent) {
+                    isRelevant = isIntentRelevant((PointToPointIntent) intent, edgePoints) &&
+                            isIntentRelevantToDevices(intent, devices);
+                } else if (intent instanceof MultiPointToSinglePointIntent) {
+                    isRelevant = isIntentRelevant((MultiPointToSinglePointIntent) intent, edgePoints) &&
+                            isIntentRelevantToDevices(intent, devices);
+                } else if (intent instanceof OpticalConnectivityIntent) {
+                    opticalIntents.add((OpticalConnectivityIntent) intent);
+                }
+                // TODO: add other intents, e.g. SinglePointToMultiPointIntent
+
+                if (isRelevant) {
+                    intents.add(intent);
+                }
+            }
+        }
+
+        // As a second pass, try to link up any optical intents with the
+        // packet-level ones.
+        for (OpticalConnectivityIntent intent : opticalIntents) {
+            if (isIntentRelevant(intent, intents) &&
+                    isIntentRelevantToDevices(intent, devices)) {
+                intents.add(intent);
+            }
+        }
+        return intents;
+    }
+
+    // Indicates whether the specified intent involves all of the given hosts.
+    private boolean isIntentRelevantToHosts(HostToHostIntent intent, Iterable<Host> hosts) {
+        for (Host host : hosts) {
+            HostId id = host.id();
+            // Bail if intent does not involve this host.
+            if (!id.equals(intent.one()) && !id.equals(intent.two())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // Indicates whether the specified intent involves all of the given devices.
+    private boolean isIntentRelevantToDevices(Intent intent, Iterable<Device> devices) {
+        List<Intent> installables = intentService.getInstallableIntents(intent.key());
+        for (Device device : devices) {
+            if (!isIntentRelevantToDevice(installables, device)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // Indicates whether the specified intent involves the given device.
+    private boolean isIntentRelevantToDevice(List<Intent> installables, Device device) {
+        if (installables != null) {
+            for (Intent installable : installables) {
+                if (installable instanceof PathIntent) {
+                    PathIntent pathIntent = (PathIntent) installable;
+                    if (pathContainsDevice(pathIntent.path().links(), device.id())) {
+                        return true;
+                    }
+                } else if (installable instanceof LinkCollectionIntent) {
+                    LinkCollectionIntent linksIntent = (LinkCollectionIntent) installable;
+                    if (pathContainsDevice(linksIntent.links(), device.id())) {
+                        return true;
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    // Indicates whether the specified intent involves the given device.
+    private boolean pathContainsDevice(Iterable<Link> links, DeviceId id) {
+        for (Link link : links) {
+            if (link.src().elementId().equals(id) || link.dst().elementId().equals(id)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isIntentRelevant(PointToPointIntent intent,
+                                     Iterable<ConnectPoint> edgePoints) {
+        for (ConnectPoint point : edgePoints) {
+            // Bail if intent does not involve this edge point.
+            if (!point.equals(intent.egressPoint()) &&
+                    !point.equals(intent.ingressPoint())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // Indicates whether the specified intent involves all of the given edge points.
+    private boolean isIntentRelevant(MultiPointToSinglePointIntent intent,
+                                     Iterable<ConnectPoint> edgePoints) {
+        for (ConnectPoint point : edgePoints) {
+            // Bail if intent does not involve this edge point.
+            if (!point.equals(intent.egressPoint()) &&
+                    !intent.ingressPoints().contains(point)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // Indicates whether the specified intent involves all of the given edge points.
+    private boolean isIntentRelevant(OpticalConnectivityIntent opticalIntent,
+                                     Iterable<Intent> intents) {
+        Link ccSrc = getFirstLink(opticalIntent.getSrc(), false);
+        Link ccDst = getFirstLink(opticalIntent.getDst(), true);
+
+        for (Intent intent : intents) {
+            List<Intent> installables = intentService.getInstallableIntents(intent.key());
+            for (Intent installable : installables) {
+                if (installable instanceof PathIntent) {
+                    List<Link> links = ((PathIntent) installable).path().links();
+                    if (links.size() == 3) {
+                        Link tunnel = links.get(1);
+                        if (tunnel.src().equals(ccSrc.src()) &&
+                                tunnel.dst().equals(ccDst.dst())) {
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    private Link getFirstLink(ConnectPoint point, boolean ingress) {
+        for (Link link : linkService.getLinks(point)) {
+            if (point.equals(ingress ? link.src() : link.dst())) {
+                return link;
+            }
+        }
+        return null;
+    }
+
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessages.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessages.java
new file mode 100644
index 0000000..dd891cd
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessages.java
@@ -0,0 +1,884 @@
+/*
+ * Copyright 2014,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.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.osgi.ServiceDirectory;
+import org.onlab.packet.IpAddress;
+import org.onosproject.cluster.ClusterEvent;
+import org.onosproject.cluster.ClusterService;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.cluster.NodeId;
+import org.onosproject.core.CoreService;
+import org.onosproject.mastership.MastershipService;
+import org.onosproject.net.Annotated;
+import org.onosproject.net.AnnotationKeys;
+import org.onosproject.net.Annotations;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.DefaultEdgeLink;
+import org.onosproject.net.Device;
+import org.onosproject.net.DeviceId;
+import org.onosproject.net.EdgeLink;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.Link;
+import org.onosproject.net.LinkKey;
+import org.onosproject.net.PortNumber;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceService;
+import org.onosproject.net.flow.FlowEntry;
+import org.onosproject.net.flow.FlowRuleService;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.flow.instructions.Instruction;
+import org.onosproject.net.flow.instructions.Instructions.OutputInstruction;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostService;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentService;
+import org.onosproject.net.intent.LinkCollectionIntent;
+import org.onosproject.net.intent.OpticalConnectivityIntent;
+import org.onosproject.net.intent.OpticalPathIntent;
+import org.onosproject.net.intent.PathIntent;
+import org.onosproject.net.link.LinkEvent;
+import org.onosproject.net.link.LinkService;
+import org.onosproject.net.provider.ProviderId;
+import org.onosproject.net.statistic.Load;
+import org.onosproject.net.statistic.StatisticService;
+import org.onosproject.net.topology.Topology;
+import org.onosproject.net.topology.TopologyService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ADDED;
+import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_REMOVED;
+import static org.onosproject.cluster.ControllerNode.State.ACTIVE;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.HostId.hostId;
+import static org.onosproject.net.LinkKey.linkKey;
+import static org.onosproject.net.PortNumber.P0;
+import static org.onosproject.net.PortNumber.portNumber;
+import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
+import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_REMOVED;
+import static org.onosproject.net.host.HostEvent.Type.HOST_ADDED;
+import static org.onosproject.net.host.HostEvent.Type.HOST_REMOVED;
+import static org.onosproject.net.link.LinkEvent.Type.LINK_ADDED;
+import static org.onosproject.net.link.LinkEvent.Type.LINK_REMOVED;
+
+/**
+ * Facility for creating messages bound for the topology viewer.
+ */
+public abstract class TopologyViewMessages {
+
+    protected static final Logger log = LoggerFactory.getLogger(TopologyViewMessages.class);
+
+    private static final ProviderId PID = new ProviderId("core", "org.onosproject.core", true);
+    private static final String COMPACT = "%s/%s-%s/%s";
+
+    private static final double KB = 1024;
+    private static final double MB = 1024 * KB;
+    private static final double GB = 1024 * MB;
+
+    private static final String GB_UNIT = "GB";
+    private static final String MB_UNIT = "MB";
+    private static final String KB_UNIT = "KB";
+    private static final String B_UNIT = "B";
+
+    protected final ServiceDirectory directory;
+    protected final ClusterService clusterService;
+    protected final DeviceService deviceService;
+    protected final LinkService linkService;
+    protected final HostService hostService;
+    protected final MastershipService mastershipService;
+    protected final IntentService intentService;
+    protected final FlowRuleService flowService;
+    protected final StatisticService statService;
+    protected final TopologyService topologyService;
+
+    protected final ObjectMapper mapper = new ObjectMapper();
+    private final String version;
+
+    // TODO: extract into an external & durable state; good enough for now and demo
+    private static Map<String, ObjectNode> metaUi = new ConcurrentHashMap<>();
+
+    /**
+     * Returns read-only view of the meta-ui information.
+     *
+     * @return map of id to meta-ui mementos
+     */
+    static Map<String, ObjectNode> getMetaUi() {
+        return Collections.unmodifiableMap(metaUi);
+    }
+
+    /**
+     * Creates a messaging facility for creating messages for topology viewer.
+     *
+     * @param directory service directory
+     */
+    protected TopologyViewMessages(ServiceDirectory directory) {
+        this.directory = checkNotNull(directory, "Directory cannot be null");
+        clusterService = directory.get(ClusterService.class);
+        deviceService = directory.get(DeviceService.class);
+        linkService = directory.get(LinkService.class);
+        hostService = directory.get(HostService.class);
+        mastershipService = directory.get(MastershipService.class);
+        intentService = directory.get(IntentService.class);
+        flowService = directory.get(FlowRuleService.class);
+        statService = directory.get(StatisticService.class);
+        topologyService = directory.get(TopologyService.class);
+
+        String ver = directory.get(CoreService.class).version().toString();
+        version = ver.replace(".SNAPSHOT", "*").replaceFirst("~.*$", "");
+    }
+
+    // Retrieves the payload from the specified event.
+    protected ObjectNode payload(ObjectNode event) {
+        return (ObjectNode) event.path("payload");
+    }
+
+    // Returns the specified node property as a number
+    protected long number(ObjectNode node, String name) {
+        return node.path(name).asLong();
+    }
+
+    // Returns the specified node property as a string.
+    protected String string(ObjectNode node, String name) {
+        return node.path(name).asText();
+    }
+
+    // Returns the specified node property as a string.
+    protected String string(ObjectNode node, String name, String defaultValue) {
+        return node.path(name).asText(defaultValue);
+    }
+
+    // Returns the specified set of IP addresses as a string.
+    private String ip(Set<IpAddress> ipAddresses) {
+        Iterator<IpAddress> it = ipAddresses.iterator();
+        return it.hasNext() ? it.next().toString() : "unknown";
+    }
+
+    // Produces JSON structure from annotations.
+    private JsonNode props(Annotations annotations) {
+        ObjectNode props = mapper.createObjectNode();
+        if (annotations != null) {
+            for (String key : annotations.keys()) {
+                props.put(key, annotations.value(key));
+            }
+        }
+        return props;
+    }
+
+    // Produces an informational log message event bound to the client.
+    protected ObjectNode info(long id, String message) {
+        return message("info", id, message);
+    }
+
+    // Produces a warning log message event bound to the client.
+    protected ObjectNode warning(long id, String message) {
+        return message("warning", id, message);
+    }
+
+    // Produces an error log message event bound to the client.
+    protected ObjectNode error(long id, String message) {
+        return message("error", id, message);
+    }
+
+    // Produces a log message event bound to the client.
+    private ObjectNode message(String severity, long id, String message) {
+        return envelope("message", id,
+                        mapper.createObjectNode()
+                                .put("severity", severity)
+                                .put("message", message));
+    }
+
+    // Puts the payload into an envelope and returns it.
+    protected ObjectNode envelope(String type, long sid, ObjectNode payload) {
+        ObjectNode event = mapper.createObjectNode();
+        event.put("event", type);
+        if (sid > 0) {
+            event.put("sid", sid);
+        }
+        event.set("payload", payload);
+        return event;
+    }
+
+    // Produces a set of all hosts listed in the specified JSON array.
+    protected Set<Host> getHosts(ArrayNode array) {
+        Set<Host> hosts = new HashSet<>();
+        if (array != null) {
+            for (JsonNode node : array) {
+                try {
+                    addHost(hosts, hostId(node.asText()));
+                } catch (IllegalArgumentException e) {
+                    log.debug("Skipping ID {}", node.asText());
+                }
+            }
+        }
+        return hosts;
+    }
+
+    // Adds the specified host to the set of hosts.
+    private void addHost(Set<Host> hosts, HostId hostId) {
+        Host host = hostService.getHost(hostId);
+        if (host != null) {
+            hosts.add(host);
+        }
+    }
+
+
+    // Produces a set of all devices listed in the specified JSON array.
+    protected Set<Device> getDevices(ArrayNode array) {
+        Set<Device> devices = new HashSet<>();
+        if (array != null) {
+            for (JsonNode node : array) {
+                try {
+                    addDevice(devices, deviceId(node.asText()));
+                } catch (IllegalArgumentException e) {
+                    log.debug("Skipping ID {}", node.asText());
+                }
+            }
+        }
+        return devices;
+    }
+
+    private void addDevice(Set<Device> devices, DeviceId deviceId) {
+        Device device = deviceService.getDevice(deviceId);
+        if (device != null) {
+            devices.add(device);
+        }
+    }
+
+    protected void addHover(Set<Host> hosts, Set<Device> devices, String hover) {
+        try {
+            addHost(hosts, hostId(hover));
+        } catch (IllegalArgumentException e) {
+            try {
+                addDevice(devices, deviceId(hover));
+            } catch (IllegalArgumentException ne) {
+                log.debug("Skipping ID {}", hover);
+            }
+        }
+    }
+
+    // Produces a cluster instance message to the client.
+    protected ObjectNode instanceMessage(ClusterEvent event, String messageType) {
+        ControllerNode node = event.subject();
+        int switchCount = mastershipService.getDevicesOf(node.id()).size();
+        ObjectNode payload = mapper.createObjectNode()
+                .put("id", node.id().toString())
+                .put("ip", node.ip().toString())
+                .put("online", clusterService.getState(node.id()) == ACTIVE)
+                .put("uiAttached", event.subject().equals(clusterService.getLocalNode()))
+                .put("switches", switchCount);
+
+        ArrayNode labels = mapper.createArrayNode();
+        labels.add(node.id().toString());
+        labels.add(node.ip().toString());
+
+        // Add labels, props and stuff the payload into envelope.
+        payload.set("labels", labels);
+        addMetaUi(node.id().toString(), payload);
+
+        String type = messageType != null ? messageType :
+                ((event.type() == INSTANCE_ADDED) ? "addInstance" :
+                        ((event.type() == INSTANCE_REMOVED ? "removeInstance" :
+                                "addInstance")));
+        return envelope(type, 0, payload);
+    }
+
+    // Produces a device event message to the client.
+    protected ObjectNode deviceMessage(DeviceEvent event) {
+        Device device = event.subject();
+        ObjectNode payload = mapper.createObjectNode()
+                .put("id", device.id().toString())
+                .put("type", device.type().toString().toLowerCase())
+                .put("online", deviceService.isAvailable(device.id()))
+                .put("master", master(device.id()));
+
+        // Generate labels: id, chassis id, no-label, optional-name
+        String name = device.annotations().value(AnnotationKeys.NAME);
+        ArrayNode labels = mapper.createArrayNode();
+        labels.add("");
+        labels.add(isNullOrEmpty(name) ? device.id().toString() : name);
+        labels.add(device.id().toString());
+
+        // Add labels, props and stuff the payload into envelope.
+        payload.set("labels", labels);
+        payload.set("props", props(device.annotations()));
+        addGeoLocation(device, payload);
+        addMetaUi(device.id().toString(), payload);
+
+        String type = (event.type() == DEVICE_ADDED) ? "addDevice" :
+                ((event.type() == DEVICE_REMOVED) ? "removeDevice" : "updateDevice");
+        return envelope(type, 0, payload);
+    }
+
+    // Produces a link event message to the client.
+    protected ObjectNode linkMessage(LinkEvent event) {
+        Link link = event.subject();
+        ObjectNode payload = mapper.createObjectNode()
+                .put("id", compactLinkString(link))
+                .put("type", link.type().toString().toLowerCase())
+                .put("online", link.state() == Link.State.ACTIVE)
+                .put("linkWidth", 1.2)
+                .put("src", link.src().deviceId().toString())
+                .put("srcPort", link.src().port().toString())
+                .put("dst", link.dst().deviceId().toString())
+                .put("dstPort", link.dst().port().toString());
+        String type = (event.type() == LINK_ADDED) ? "addLink" :
+                ((event.type() == LINK_REMOVED) ? "removeLink" : "updateLink");
+        return envelope(type, 0, payload);
+    }
+
+    // Produces a host event message to the client.
+    protected ObjectNode hostMessage(HostEvent event) {
+        Host host = event.subject();
+        String hostType = host.annotations().value(AnnotationKeys.TYPE);
+        ObjectNode payload = mapper.createObjectNode()
+                .put("id", host.id().toString())
+                .put("type", isNullOrEmpty(hostType) ? "endstation" : hostType)
+                .put("ingress", compactLinkString(edgeLink(host, true)))
+                .put("egress", compactLinkString(edgeLink(host, false)));
+        payload.set("cp", hostConnect(mapper, host.location()));
+        payload.set("labels", labels(mapper, ip(host.ipAddresses()),
+                                     host.mac().toString()));
+        payload.set("props", props(host.annotations()));
+        addGeoLocation(host, payload);
+        addMetaUi(host.id().toString(), payload);
+
+        String type = (event.type() == HOST_ADDED) ? "addHost" :
+                ((event.type() == HOST_REMOVED) ? "removeHost" : "updateHost");
+        return envelope(type, 0, payload);
+    }
+
+    // Encodes the specified host location into a JSON object.
+    private ObjectNode hostConnect(ObjectMapper mapper, HostLocation location) {
+        return mapper.createObjectNode()
+                .put("device", location.deviceId().toString())
+                .put("port", location.port().toLong());
+    }
+
+    // Encodes the specified list of labels a JSON array.
+    private ArrayNode labels(ObjectMapper mapper, String... labels) {
+        ArrayNode json = mapper.createArrayNode();
+        for (String label : labels) {
+            json.add(label);
+        }
+        return json;
+    }
+
+    // Returns the name of the master node for the specified device id.
+    private String master(DeviceId deviceId) {
+        NodeId master = mastershipService.getMasterFor(deviceId);
+        return master != null ? master.toString() : "";
+    }
+
+    // Generates an edge link from the specified host location.
+    private EdgeLink edgeLink(Host host, boolean ingress) {
+        return new DefaultEdgeLink(PID, new ConnectPoint(host.id(), portNumber(0)),
+                                   host.location(), ingress);
+    }
+
+    // Adds meta UI information for the specified object.
+    private void addMetaUi(String id, ObjectNode payload) {
+        ObjectNode meta = metaUi.get(id);
+        if (meta != null) {
+            payload.set("metaUi", meta);
+        }
+    }
+
+    // Adds a geo location JSON to the specified payload object.
+    private void addGeoLocation(Annotated annotated, ObjectNode payload) {
+        Annotations annotations = annotated.annotations();
+        if (annotations == null) {
+            return;
+        }
+
+        String slat = annotations.value(AnnotationKeys.LATITUDE);
+        String slng = annotations.value(AnnotationKeys.LONGITUDE);
+        try {
+            if (slat != null && slng != null && !slat.isEmpty() && !slng.isEmpty()) {
+                double lat = Double.parseDouble(slat);
+                double lng = Double.parseDouble(slng);
+                ObjectNode loc = mapper.createObjectNode()
+                        .put("type", "latlng").put("lat", lat).put("lng", lng);
+                payload.set("location", loc);
+            }
+        } catch (NumberFormatException e) {
+            log.warn("Invalid geo data latitude={}; longiture={}", slat, slng);
+        }
+    }
+
+    // Updates meta UI information for the specified object.
+    protected void updateMetaUi(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        metaUi.put(string(payload, "id"), (ObjectNode) payload.path("memento"));
+    }
+
+    // Returns summary response.
+    protected ObjectNode summmaryMessage(long sid) {
+        Topology topology = topologyService.currentTopology();
+        return envelope("showSummary", sid,
+                        json("ONOS Summary", "node",
+                             new Prop("Devices", format(topology.deviceCount())),
+                             new Prop("Links", format(topology.linkCount())),
+                             new Prop("Hosts", format(hostService.getHostCount())),
+                             new Prop("Topology SCCs", format(topology.clusterCount())),
+                             new Separator(),
+                             new Prop("Intents", format(intentService.getIntentCount())),
+                             new Prop("Flows", format(flowService.getFlowRuleCount())),
+                             new Prop("Version", version)));
+    }
+
+    // Returns device details response.
+    protected ObjectNode deviceDetails(DeviceId deviceId, long sid) {
+        Device device = deviceService.getDevice(deviceId);
+        Annotations annot = device.annotations();
+        String name = annot.value(AnnotationKeys.NAME);
+        int portCount = deviceService.getPorts(deviceId).size();
+        int flowCount = getFlowCount(deviceId);
+        return envelope("showDetails", sid,
+                        json(isNullOrEmpty(name) ? deviceId.toString() : name,
+                             device.type().toString().toLowerCase(),
+                             new Prop("URI", deviceId.toString()),
+                             new Prop("Vendor", device.manufacturer()),
+                             new Prop("H/W Version", device.hwVersion()),
+                             new Prop("S/W Version", device.swVersion()),
+                             new Prop("Serial Number", device.serialNumber()),
+                             new Prop("Protocol", annot.value(AnnotationKeys.PROTOCOL)),
+                             new Separator(),
+                             new Prop("Master", master(deviceId)),
+                             new Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)),
+                             new Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE)),
+                             new Separator(),
+                             new Prop("Ports", Integer.toString(portCount)),
+                             new Prop("Flows", Integer.toString(flowCount))));
+    }
+
+    protected int getFlowCount(DeviceId deviceId) {
+        int count = 0;
+        Iterator<FlowEntry> it = flowService.getFlowEntries(deviceId).iterator();
+        while (it.hasNext()) {
+            count++;
+            it.next();
+        }
+        return count;
+    }
+
+    // Counts all entries that egress on the given device links.
+    protected Map<Link, Integer> getFlowCounts(DeviceId deviceId) {
+        List<FlowEntry> entries = new ArrayList<>();
+        Set<Link> links = new HashSet<>(linkService.getDeviceEgressLinks(deviceId));
+        Set<Host> hosts = hostService.getConnectedHosts(deviceId);
+        Iterator<FlowEntry> it = flowService.getFlowEntries(deviceId).iterator();
+        while (it.hasNext()) {
+            entries.add(it.next());
+        }
+
+        // Add all edge links to the set
+        if (hosts != null) {
+            for (Host host : hosts) {
+                links.add(new DefaultEdgeLink(host.providerId(),
+                                              new ConnectPoint(host.id(), P0),
+                                              host.location(), false));
+            }
+        }
+
+        Map<Link, Integer> counts = new HashMap<>();
+        for (Link link : links) {
+            counts.put(link, getEgressFlows(link, entries));
+        }
+        return counts;
+    }
+
+    // Counts all entries that egress on the link source port.
+    private Integer getEgressFlows(Link link, List<FlowEntry> entries) {
+        int count = 0;
+        PortNumber out = link.src().port();
+        for (FlowEntry entry : entries) {
+            TrafficTreatment treatment = entry.treatment();
+            for (Instruction instruction : treatment.instructions()) {
+                if (instruction.type() == Instruction.Type.OUTPUT &&
+                        ((OutputInstruction) instruction).port().equals(out)) {
+                    count++;
+                }
+            }
+        }
+        return count;
+    }
+
+
+    // Returns host details response.
+    protected ObjectNode hostDetails(HostId hostId, long sid) {
+        Host host = hostService.getHost(hostId);
+        Annotations annot = host.annotations();
+        String type = annot.value(AnnotationKeys.TYPE);
+        String name = annot.value(AnnotationKeys.NAME);
+        String vlan = host.vlan().toString();
+        return envelope("showDetails", sid,
+                        json(isNullOrEmpty(name) ? hostId.toString() : name,
+                             isNullOrEmpty(type) ? "endstation" : type,
+                             new Prop("MAC", host.mac().toString()),
+                             new Prop("IP", host.ipAddresses().toString().replaceAll("[\\[\\]]", "")),
+                             new Prop("VLAN", vlan.equals("-1") ? "none" : vlan),
+                             new Separator(),
+                             new Prop("Latitude", annot.value(AnnotationKeys.LATITUDE)),
+                             new Prop("Longitude", annot.value(AnnotationKeys.LONGITUDE))));
+    }
+
+
+    // Produces JSON message to trigger traffic overview visualization
+    protected ObjectNode trafficSummaryMessage(long sid) {
+        ObjectNode payload = mapper.createObjectNode();
+        ArrayNode paths = mapper.createArrayNode();
+        payload.set("paths", paths);
+
+        ObjectNode pathNodeN = mapper.createObjectNode();
+        ArrayNode linksNodeN = mapper.createArrayNode();
+        ArrayNode labelsN = mapper.createArrayNode();
+
+        pathNodeN.put("class", "plain").put("traffic", false);
+        pathNodeN.set("links", linksNodeN);
+        pathNodeN.set("labels", labelsN);
+        paths.add(pathNodeN);
+
+        ObjectNode pathNodeT = mapper.createObjectNode();
+        ArrayNode linksNodeT = mapper.createArrayNode();
+        ArrayNode labelsT = mapper.createArrayNode();
+
+        pathNodeT.put("class", "secondary").put("traffic", true);
+        pathNodeT.set("links", linksNodeT);
+        pathNodeT.set("labels", labelsT);
+        paths.add(pathNodeT);
+
+        for (BiLink link : consolidateLinks(linkService.getLinks())) {
+            boolean bi = link.two != null;
+            if (isInfrastructureEgress(link.one) ||
+                    (bi && isInfrastructureEgress(link.two))) {
+                link.addLoad(statService.load(link.one));
+                link.addLoad(bi ? statService.load(link.two) : null);
+                if (link.hasTraffic) {
+                    linksNodeT.add(compactLinkString(link.one));
+                    labelsT.add(formatBytes(link.bytes));
+                } else {
+                    linksNodeN.add(compactLinkString(link.one));
+                    labelsN.add("");
+                }
+            }
+        }
+        return envelope("showTraffic", sid, payload);
+    }
+
+    private Collection<BiLink> consolidateLinks(Iterable<Link> links) {
+        Map<LinkKey, BiLink> biLinks = new HashMap<>();
+        for (Link link : links) {
+            addLink(biLinks, link);
+        }
+        return biLinks.values();
+    }
+
+    // Produces JSON message to trigger flow overview visualization
+    protected ObjectNode flowSummaryMessage(long sid, Set<Device> devices) {
+        ObjectNode payload = mapper.createObjectNode();
+        ArrayNode paths = mapper.createArrayNode();
+        payload.set("paths", paths);
+
+        for (Device device : devices) {
+            Map<Link, Integer> counts = getFlowCounts(device.id());
+            for (Link link : counts.keySet()) {
+                addLinkFlows(link, paths, counts.get(link));
+            }
+        }
+        return envelope("showTraffic", sid, payload);
+    }
+
+    private void addLinkFlows(Link link, ArrayNode paths, Integer count) {
+        ObjectNode pathNode = mapper.createObjectNode();
+        ArrayNode linksNode = mapper.createArrayNode();
+        ArrayNode labels = mapper.createArrayNode();
+        boolean noFlows = count == null || count == 0;
+        pathNode.put("class", noFlows ? "secondary" : "primary");
+        pathNode.put("traffic", false);
+        pathNode.set("links", linksNode.add(compactLinkString(link)));
+        pathNode.set("labels", labels.add(noFlows ? "" : (count.toString() +
+                (count == 1 ? " flow" : " flows"))));
+        paths.add(pathNode);
+    }
+
+
+    // Produces JSON message to trigger traffic visualization
+    protected ObjectNode trafficMessage(long sid, TrafficClass... trafficClasses) {
+        ObjectNode payload = mapper.createObjectNode();
+        ArrayNode paths = mapper.createArrayNode();
+        payload.set("paths", paths);
+
+        // Classify links based on their traffic traffic first...
+        Map<LinkKey, BiLink> biLinks = classifyLinkTraffic(trafficClasses);
+
+        // Then separate the links into their respective classes and send them out.
+        Map<String, ObjectNode> pathNodes = new HashMap<>();
+        for (BiLink biLink : biLinks.values()) {
+            boolean hasTraffic = biLink.hasTraffic;
+            String tc = (biLink.classes + (hasTraffic ? " animated" : "")).trim();
+            ObjectNode pathNode = pathNodes.get(tc);
+            if (pathNode == null) {
+                pathNode = mapper.createObjectNode()
+                        .put("class", tc).put("traffic", hasTraffic);
+                pathNode.set("links", mapper.createArrayNode());
+                pathNode.set("labels", mapper.createArrayNode());
+                pathNodes.put(tc, pathNode);
+                paths.add(pathNode);
+            }
+            ((ArrayNode) pathNode.path("links")).add(compactLinkString(biLink.one));
+            ((ArrayNode) pathNode.path("labels")).add(hasTraffic ? formatBytes(biLink.bytes) : "");
+        }
+
+        return envelope("showTraffic", sid, payload);
+    }
+
+    // Classifies the link traffic according to the specified classes.
+    private Map<LinkKey, BiLink> classifyLinkTraffic(TrafficClass... trafficClasses) {
+        Map<LinkKey, BiLink> biLinks = new HashMap<>();
+        for (TrafficClass trafficClass : trafficClasses) {
+            for (Intent intent : trafficClass.intents) {
+                boolean isOptical = intent instanceof OpticalConnectivityIntent;
+                List<Intent> installables = intentService.getInstallableIntents(intent.key());
+                if (installables != null) {
+                    for (Intent installable : installables) {
+                        String type = isOptical ? trafficClass.type + " optical" : trafficClass.type;
+                        if (installable instanceof PathIntent) {
+                            classifyLinks(type, biLinks, trafficClass.showTraffic,
+                                          ((PathIntent) installable).path().links());
+                        } else if (installable instanceof LinkCollectionIntent) {
+                            classifyLinks(type, biLinks, trafficClass.showTraffic,
+                                          ((LinkCollectionIntent) installable).links());
+                        } else if (installable instanceof OpticalPathIntent) {
+                            classifyLinks(type, biLinks, trafficClass.showTraffic,
+                                    ((OpticalPathIntent) installable).path().links());
+                        }
+                    }
+                }
+            }
+        }
+        return biLinks;
+    }
+
+
+    // Adds the link segments (path or tree) associated with the specified
+    // connectivity intent
+    private void classifyLinks(String type, Map<LinkKey, BiLink> biLinks,
+                               boolean showTraffic, Iterable<Link> links) {
+        if (links != null) {
+            for (Link link : links) {
+                BiLink biLink = addLink(biLinks, link);
+                if (isInfrastructureEgress(link)) {
+                    if (showTraffic) {
+                        biLink.addLoad(statService.load(link));
+                    }
+                    biLink.addClass(type);
+                }
+            }
+        }
+    }
+
+
+    private BiLink addLink(Map<LinkKey, BiLink> biLinks, Link link) {
+        LinkKey key = canonicalLinkKey(link);
+        BiLink biLink = biLinks.get(key);
+        if (biLink != null) {
+            biLink.setOther(link);
+        } else {
+            biLink = new BiLink(key, link);
+            biLinks.put(key, biLink);
+        }
+        return biLink;
+    }
+
+
+    // Adds the link segments (path or tree) associated with the specified
+    // connectivity intent
+    protected void addPathTraffic(ArrayNode paths, String type, String trafficType,
+                                  Iterable<Link> links) {
+        ObjectNode pathNode = mapper.createObjectNode();
+        ArrayNode linksNode = mapper.createArrayNode();
+
+        if (links != null) {
+            ArrayNode labels = mapper.createArrayNode();
+            boolean hasTraffic = false;
+            for (Link link : links) {
+                if (isInfrastructureEgress(link)) {
+                    linksNode.add(compactLinkString(link));
+                    Load load = statService.load(link);
+                    String label = "";
+                    if (load.rate() > 0) {
+                        hasTraffic = true;
+                        label = formatBytes(load.latest());
+                    }
+                    labels.add(label);
+                }
+            }
+            pathNode.put("class", hasTraffic ? type + " " + trafficType : type);
+            pathNode.put("traffic", hasTraffic);
+            pathNode.set("links", linksNode);
+            pathNode.set("labels", labels);
+            paths.add(pathNode);
+        }
+    }
+
+    // Poor-mans formatting to get the labels with byte counts looking nice.
+    private String formatBytes(long bytes) {
+        String unit;
+        double value;
+        if (bytes > GB) {
+            value = bytes / GB;
+            unit = GB_UNIT;
+        } else if (bytes > MB) {
+            value = bytes / MB;
+            unit = MB_UNIT;
+        } else if (bytes > KB) {
+            value = bytes / KB;
+            unit = KB_UNIT;
+        } else {
+            value = bytes;
+            unit = B_UNIT;
+        }
+        DecimalFormat format = new DecimalFormat("#,###.##");
+        return format.format(value) + " " + unit;
+    }
+
+    // Formats the given number into a string.
+    private String format(Number number) {
+        DecimalFormat format = new DecimalFormat("#,###");
+        return format.format(number);
+    }
+
+    private boolean isInfrastructureEgress(Link link) {
+        return link.src().elementId() instanceof DeviceId;
+    }
+
+    // Produces compact string representation of a link.
+    private static String compactLinkString(Link link) {
+        return String.format(COMPACT, link.src().elementId(), link.src().port(),
+                             link.dst().elementId(), link.dst().port());
+    }
+
+    // Produces JSON property details.
+    private ObjectNode json(String id, String type, Prop... props) {
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode result = mapper.createObjectNode()
+                .put("id", id).put("type", type);
+        ObjectNode pnode = mapper.createObjectNode();
+        ArrayNode porder = mapper.createArrayNode();
+        for (Prop p : props) {
+            porder.add(p.key);
+            pnode.put(p.key, p.value);
+        }
+        result.set("propOrder", porder);
+        result.set("props", pnode);
+        return result;
+    }
+
+    // Produces canonical link key, i.e. one that will match link and its inverse.
+    private LinkKey canonicalLinkKey(Link link) {
+        String sn = link.src().elementId().toString();
+        String dn = link.dst().elementId().toString();
+        return sn.compareTo(dn) < 0 ?
+                linkKey(link.src(), link.dst()) : linkKey(link.dst(), link.src());
+    }
+
+    // Representation of link and its inverse and any traffic data.
+    private class BiLink {
+        public final LinkKey key;
+        public final Link one;
+        public Link two;
+        public boolean hasTraffic = false;
+        public long bytes = 0;
+        public String classes = "";
+
+        BiLink(LinkKey key, Link link) {
+            this.key = key;
+            this.one = link;
+        }
+
+        void setOther(Link link) {
+            this.two = link;
+        }
+
+        void addLoad(Load load) {
+            if (load != null) {
+                this.hasTraffic = hasTraffic || load.rate() > 0;
+                this.bytes += load.latest();
+            }
+        }
+
+        void addClass(String trafficClass) {
+            classes = classes + " " + trafficClass;
+        }
+    }
+
+    // Auxiliary key/value carrier.
+    private class Prop {
+        public final String key;
+        public final String value;
+
+        protected Prop(String key, String value) {
+            this.key = key;
+            this.value = value;
+        }
+    }
+
+    // Auxiliary properties separator
+    private class Separator extends Prop {
+        protected Separator() {
+            super("-", "");
+        }
+    }
+
+    // Auxiliary carrier of data for requesting traffic message.
+    protected class TrafficClass {
+        public final boolean showTraffic;
+        public final String type;
+        public final Iterable<Intent> intents;
+
+        TrafficClass(String type, Iterable<Intent> intents) {
+            this(type, intents, false);
+        }
+
+        TrafficClass(String type, Iterable<Intent> intents, boolean showTraffic) {
+            this.type = type;
+            this.intents = intents;
+            this.showTraffic = showTraffic;
+        }
+    }
+
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewWebSocket.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewWebSocket.java
new file mode 100644
index 0000000..e259cb5
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewWebSocket.java
@@ -0,0 +1,742 @@
+/*
+ * Copyright 2014,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.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.eclipse.jetty.websocket.WebSocket;
+import org.onlab.osgi.ServiceDirectory;
+import org.onlab.util.AbstractAccumulator;
+import org.onlab.util.Accumulator;
+import org.onosproject.cluster.ClusterEvent;
+import org.onosproject.cluster.ClusterEventListener;
+import org.onosproject.cluster.ControllerNode;
+import org.onosproject.core.ApplicationId;
+import org.onosproject.core.CoreService;
+import org.onosproject.event.Event;
+import org.onosproject.mastership.MastershipAdminService;
+import org.onosproject.mastership.MastershipEvent;
+import org.onosproject.mastership.MastershipListener;
+import org.onosproject.net.ConnectPoint;
+import org.onosproject.net.Device;
+import org.onosproject.net.Host;
+import org.onosproject.net.HostId;
+import org.onosproject.net.HostLocation;
+import org.onosproject.net.Link;
+import org.onosproject.net.device.DeviceEvent;
+import org.onosproject.net.device.DeviceListener;
+import org.onosproject.net.flow.DefaultTrafficSelector;
+import org.onosproject.net.flow.DefaultTrafficTreatment;
+import org.onosproject.net.flow.FlowRuleEvent;
+import org.onosproject.net.flow.FlowRuleListener;
+import org.onosproject.net.flow.TrafficSelector;
+import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.net.host.HostEvent;
+import org.onosproject.net.host.HostListener;
+import org.onosproject.net.intent.HostToHostIntent;
+import org.onosproject.net.intent.Intent;
+import org.onosproject.net.intent.IntentEvent;
+import org.onosproject.net.intent.IntentListener;
+import org.onosproject.net.intent.MultiPointToSinglePointIntent;
+import org.onosproject.net.link.LinkEvent;
+import org.onosproject.net.link.LinkListener;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static org.onosproject.cluster.ClusterEvent.Type.INSTANCE_ADDED;
+import static org.onosproject.net.DeviceId.deviceId;
+import static org.onosproject.net.HostId.hostId;
+import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_ADDED;
+import static org.onosproject.net.device.DeviceEvent.Type.DEVICE_UPDATED;
+import static org.onosproject.net.host.HostEvent.Type.HOST_ADDED;
+import static org.onosproject.net.link.LinkEvent.Type.LINK_ADDED;
+
+/**
+ * Web socket capable of interacting with the GUI topology view.
+ */
+public class TopologyViewWebSocket
+        extends TopologyViewMessages
+        implements WebSocket.OnTextMessage, WebSocket.OnControl {
+
+    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 static final String APP_ID = "org.onosproject.gui";
+
+    private static final long TRAFFIC_FREQUENCY = 5000;
+    private static final long SUMMARY_FREQUENCY = 30000;
+
+    private static final Comparator<? super ControllerNode> NODE_COMPARATOR =
+            new Comparator<ControllerNode>() {
+                @Override
+                public int compare(ControllerNode o1, ControllerNode o2) {
+                    return o1.id().toString().compareTo(o2.id().toString());
+                }
+            };
+
+
+    private final Timer timer = new Timer("topology-view");
+
+    private static final int MAX_EVENTS = 1000;
+    private static final int MAX_BATCH_MS = 5000;
+    private static final int MAX_IDLE_MS = 1000;
+
+    private final ApplicationId appId;
+
+    private Connection connection;
+    private FrameConnection control;
+
+    private final ClusterEventListener clusterListener = new InternalClusterListener();
+    private final MastershipListener mastershipListener = new InternalMastershipListener();
+    private final DeviceListener deviceListener = new InternalDeviceListener();
+    private final LinkListener linkListener = new InternalLinkListener();
+    private final HostListener hostListener = new InternalHostListener();
+    private final IntentListener intentListener = new InternalIntentListener();
+    private final FlowRuleListener flowListener = new InternalFlowListener();
+
+    private final Accumulator<Event> eventAccummulator = new InternalEventAccummulator();
+
+    private TimerTask trafficTask;
+    private ObjectNode trafficEvent;
+
+    private TimerTask summaryTask;
+    private ObjectNode summaryEvent;
+
+    private long lastActive = System.currentTimeMillis();
+    private boolean listenersRemoved = false;
+
+    private TopologyViewIntentFilter intentFilter;
+
+    // Current selection context
+    private Set<Host> selectedHosts;
+    private Set<Device> selectedDevices;
+    private List<Intent> selectedIntents;
+    private int currentIntentIndex = -1;
+
+    /**
+     * Creates a new web-socket for serving data to GUI topology view.
+     *
+     * @param directory service directory
+     */
+    public TopologyViewWebSocket(ServiceDirectory directory) {
+        super(directory);
+        intentFilter = new TopologyViewIntentFilter(intentService, deviceService,
+                                                    hostService, linkService);
+        appId = directory.get(CoreService.class).registerApplication(APP_ID);
+    }
+
+    /**
+     * Issues a close on the connection.
+     */
+    synchronized void close() {
+        removeListeners();
+        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;
+        addListeners();
+
+        sendAllInstances(null);
+        sendAllDevices();
+        sendAllLinks();
+        sendAllHosts();
+    }
+
+    @Override
+    public synchronized void onClose(int closeCode, String message) {
+        removeListeners();
+        timer.cancel();
+        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 {
+            processMessage((ObjectNode) mapper.reader().readTree(data));
+        } catch (Exception e) {
+            log.warn("Unable to parse GUI request {} due to {}", data, e);
+            log.debug("Boom!!!", e);
+        }
+    }
+
+    // Processes the specified event.
+    private void processMessage(ObjectNode event) {
+        String type = string(event, "event", "unknown");
+        if (type.equals("requestDetails")) {
+            requestDetails(event);
+        } else if (type.equals("updateMeta")) {
+            updateMetaUi(event);
+
+        } else if (type.equals("addHostIntent")) {
+            createHostIntent(event);
+        } else if (type.equals("addMultiSourceIntent")) {
+            createMultiSourceIntent(event);
+
+        } else if (type.equals("requestRelatedIntents")) {
+            stopTrafficMonitoring();
+            requestRelatedIntents(event);
+
+        } else if (type.equals("requestNextRelatedIntent")) {
+            stopTrafficMonitoring();
+            requestAnotherRelatedIntent(event, +1);
+        } else if (type.equals("requestPrevRelatedIntent")) {
+            stopTrafficMonitoring();
+            requestAnotherRelatedIntent(event, -1);
+        } else if (type.equals("requestSelectedIntentTraffic")) {
+            requestSelectedIntentTraffic(event);
+            startTrafficMonitoring(event);
+
+        } else if (type.equals("requestAllTraffic")) {
+            requestAllTraffic(event);
+            startTrafficMonitoring(event);
+
+        } else if (type.equals("requestDeviceLinkFlows")) {
+            requestDeviceLinkFlows(event);
+            startTrafficMonitoring(event);
+
+        } else if (type.equals("cancelTraffic")) {
+            cancelTraffic(event);
+
+        } else if (type.equals("requestSummary")) {
+            requestSummary(event);
+            startSummaryMonitoring(event);
+        } else if (type.equals("cancelSummary")) {
+            stopSummaryMonitoring();
+
+        } else if (type.equals("equalizeMasters")) {
+            equalizeMasters(event);
+        }
+    }
+
+    // Sends the specified data to the client.
+    protected synchronized void sendMessage(ObjectNode data) {
+        try {
+            if (connection.isOpen()) {
+                connection.sendMessage(data.toString());
+            }
+        } catch (IOException e) {
+            log.warn("Unable to send message {} to GUI due to {}", data, e);
+            log.debug("Boom!!!", e);
+        }
+    }
+
+    // Sends all controller nodes to the client as node-added messages.
+    private void sendAllInstances(String messageType) {
+        List<ControllerNode> nodes = new ArrayList<>(clusterService.getNodes());
+        Collections.sort(nodes, NODE_COMPARATOR);
+        for (ControllerNode node : nodes) {
+            sendMessage(instanceMessage(new ClusterEvent(INSTANCE_ADDED, node),
+                                        messageType));
+        }
+    }
+
+    // Sends all devices to the client as device-added messages.
+    private void sendAllDevices() {
+        // Send optical first, others later for layered rendering
+        for (Device device : deviceService.getDevices()) {
+            if (device.type() == Device.Type.ROADM) {
+                sendMessage(deviceMessage(new DeviceEvent(DEVICE_ADDED, device)));
+            }
+        }
+        for (Device device : deviceService.getDevices()) {
+            if (device.type() != Device.Type.ROADM) {
+                sendMessage(deviceMessage(new DeviceEvent(DEVICE_ADDED, device)));
+            }
+        }
+    }
+
+    // Sends all links to the client as link-added messages.
+    private void sendAllLinks() {
+        // Send optical first, others later for layered rendering
+        for (Link link : linkService.getLinks()) {
+            if (link.type() == Link.Type.OPTICAL) {
+                sendMessage(linkMessage(new LinkEvent(LINK_ADDED, link)));
+            }
+        }
+        for (Link link : linkService.getLinks()) {
+            if (link.type() != Link.Type.OPTICAL) {
+                sendMessage(linkMessage(new LinkEvent(LINK_ADDED, link)));
+            }
+        }
+    }
+
+    // Sends all hosts to the client as host-added messages.
+    private void sendAllHosts() {
+        for (Host host : hostService.getHosts()) {
+            sendMessage(hostMessage(new HostEvent(HOST_ADDED, host)));
+        }
+    }
+
+    // Sends back device or host details.
+    private void requestDetails(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        String type = string(payload, "class", "unknown");
+        long sid = number(event, "sid");
+
+        if (type.equals("device")) {
+            sendMessage(deviceDetails(deviceId(string(payload, "id")), sid));
+        } else if (type.equals("host")) {
+            sendMessage(hostDetails(hostId(string(payload, "id")), sid));
+        }
+    }
+
+
+    // Creates host-to-host intent.
+    private void createHostIntent(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        long id = number(event, "sid");
+        // TODO: add protection against device ids and non-existent hosts.
+        HostId one = hostId(string(payload, "one"));
+        HostId two = hostId(string(payload, "two"));
+
+        HostToHostIntent intent =
+                new HostToHostIntent(appId, one, two,
+                                     DefaultTrafficSelector.builder().build(),
+                                     DefaultTrafficTreatment.builder().build());
+
+        intentService.submit(intent);
+        startMonitoringIntent(event, intent);
+    }
+
+    // Creates multi-source-to-single-dest intent.
+    private void createMultiSourceIntent(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        long id = number(event, "sid");
+        // TODO: add protection against device ids and non-existent hosts.
+        Set<HostId> src = getHostIds((ArrayNode) payload.path("src"));
+        HostId dst = hostId(string(payload, "dst"));
+        Host dstHost = hostService.getHost(dst);
+
+        Set<ConnectPoint> ingressPoints = getHostLocations(src);
+
+        // FIXME: clearly, this is not enough
+        TrafficSelector selector = DefaultTrafficSelector.builder()
+                .matchEthDst(dstHost.mac()).build();
+        TrafficTreatment treatment = DefaultTrafficTreatment.builder().build();
+
+        MultiPointToSinglePointIntent intent =
+                new MultiPointToSinglePointIntent(appId, selector, treatment,
+                                                  ingressPoints, dstHost.location());
+
+        intentService.submit(intent);
+        startMonitoringIntent(event, intent);
+    }
+
+
+    private synchronized void startMonitoringIntent(ObjectNode event, Intent intent) {
+        selectedHosts = new HashSet<>();
+        selectedDevices = new HashSet<>();
+        selectedIntents = new ArrayList<>();
+        selectedIntents.add(intent);
+        currentIntentIndex = -1;
+        requestAnotherRelatedIntent(event, +1);
+        requestSelectedIntentTraffic(event);
+    }
+
+
+    private Set<ConnectPoint> getHostLocations(Set<HostId> hostIds) {
+        Set<ConnectPoint> points = new HashSet<>();
+        for (HostId hostId : hostIds) {
+            points.add(getHostLocation(hostId));
+        }
+        return points;
+    }
+
+    private HostLocation getHostLocation(HostId hostId) {
+        return hostService.getHost(hostId).location();
+    }
+
+    // Produces a list of host ids from the specified JSON array.
+    private Set<HostId> getHostIds(ArrayNode ids) {
+        Set<HostId> hostIds = new HashSet<>();
+        for (JsonNode id : ids) {
+            hostIds.add(hostId(id.asText()));
+        }
+        return hostIds;
+    }
+
+
+    private synchronized long startTrafficMonitoring(ObjectNode event) {
+        stopTrafficMonitoring();
+        trafficEvent = event;
+        trafficTask = new TrafficMonitor();
+        timer.schedule(trafficTask, TRAFFIC_FREQUENCY, TRAFFIC_FREQUENCY);
+        return number(event, "sid");
+    }
+
+    private synchronized void stopTrafficMonitoring() {
+        if (trafficTask != null) {
+            trafficTask.cancel();
+            trafficTask = null;
+            trafficEvent = null;
+        }
+    }
+
+    // Subscribes for host traffic messages.
+    private synchronized void requestAllTraffic(ObjectNode event) {
+        long sid = startTrafficMonitoring(event);
+        sendMessage(trafficSummaryMessage(sid));
+    }
+
+    private void requestDeviceLinkFlows(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        long sid = startTrafficMonitoring(event);
+
+        // Get the set of selected hosts and their intents.
+        ArrayNode ids = (ArrayNode) payload.path("ids");
+        Set<Host> hosts = new HashSet<>();
+        Set<Device> devices = getDevices(ids);
+
+        // If there is a hover node, include it in the hosts and find intents.
+        String hover = string(payload, "hover");
+        if (!isNullOrEmpty(hover)) {
+            addHover(hosts, devices, hover);
+        }
+        sendMessage(flowSummaryMessage(sid, devices));
+    }
+
+
+    // Requests related intents message.
+    private synchronized void requestRelatedIntents(ObjectNode event) {
+        ObjectNode payload = payload(event);
+        if (!payload.has("ids")) {
+            return;
+        }
+
+        long sid = number(event, "sid");
+
+        // Cancel any other traffic monitoring mode.
+        stopTrafficMonitoring();
+
+        // Get the set of selected hosts and their intents.
+        ArrayNode ids = (ArrayNode) payload.path("ids");
+        selectedHosts = getHosts(ids);
+        selectedDevices = getDevices(ids);
+        selectedIntents = intentFilter.findPathIntents(selectedHosts, selectedDevices,
+                                                       intentService.getIntents());
+        currentIntentIndex = -1;
+
+        if (haveSelectedIntents()) {
+            // Send a message to highlight all links of all monitored intents.
+            sendMessage(trafficMessage(sid, new TrafficClass("primary", selectedIntents)));
+        }
+
+        // FIXME: Re-introduce one the client click vs hover gesture stuff is sorted out.
+//        String hover = string(payload, "hover");
+//        if (!isNullOrEmpty(hover)) {
+//            // If there is a hover node, include it in the selection and find intents.
+//            processHoverExtendedSelection(sid, hover);
+//        }
+    }
+
+    private boolean haveSelectedIntents() {
+        return selectedIntents != null && !selectedIntents.isEmpty();
+    }
+
+    // Processes the selection extended with hovered item to segregate items
+    // into primary (those including the hover) vs secondary highlights.
+    private void processHoverExtendedSelection(long sid, String hover) {
+        Set<Host> hoverSelHosts = new HashSet<>(selectedHosts);
+        Set<Device> hoverSelDevices = new HashSet<>(selectedDevices);
+        addHover(hoverSelHosts, hoverSelDevices, hover);
+
+        List<Intent> primary = selectedIntents == null ? new ArrayList<>() :
+                intentFilter.findPathIntents(hoverSelHosts, hoverSelDevices,
+                                             selectedIntents);
+        Set<Intent> secondary = new HashSet<>(selectedIntents);
+        secondary.removeAll(primary);
+
+        // Send a message to highlight all links of all monitored intents.
+        sendMessage(trafficMessage(sid, new TrafficClass("primary", primary),
+                                   new TrafficClass("secondary", secondary)));
+    }
+
+    // Requests next or previous related intent.
+    private void requestAnotherRelatedIntent(ObjectNode event, int offset) {
+        if (haveSelectedIntents()) {
+            currentIntentIndex = currentIntentIndex + offset;
+            if (currentIntentIndex < 0) {
+                currentIntentIndex = selectedIntents.size() - 1;
+            } else if (currentIntentIndex >= selectedIntents.size()) {
+                currentIntentIndex = 0;
+            }
+            sendSelectedIntent(event);
+        }
+    }
+
+    // Sends traffic information on the related intents with the currently
+    // selected intent highlighted.
+    private void sendSelectedIntent(ObjectNode event) {
+        Intent selectedIntent = selectedIntents.get(currentIntentIndex);
+        log.info("Requested next intent {}", selectedIntent.id());
+
+        Set<Intent> primary = new HashSet<>();
+        primary.add(selectedIntent);
+
+        Set<Intent> secondary = new HashSet<>(selectedIntents);
+        secondary.remove(selectedIntent);
+
+        // Send a message to highlight all links of the selected intent.
+        sendMessage(trafficMessage(number(event, "sid"),
+                                   new TrafficClass("primary", primary),
+                                   new TrafficClass("secondary", secondary)));
+    }
+
+    // Requests monitoring of traffic for the selected intent.
+    private void requestSelectedIntentTraffic(ObjectNode event) {
+        if (haveSelectedIntents()) {
+            if (currentIntentIndex < 0) {
+                currentIntentIndex = 0;
+            }
+            Intent selectedIntent = selectedIntents.get(currentIntentIndex);
+            log.info("Requested traffic for selected {}", selectedIntent.id());
+
+            Set<Intent> primary = new HashSet<>();
+            primary.add(selectedIntent);
+
+            // Send a message to highlight all links of the selected intent.
+            sendMessage(trafficMessage(number(event, "sid"),
+                                       new TrafficClass("primary", primary, true)));
+        }
+    }
+
+    // Cancels sending traffic messages.
+    private void cancelTraffic(ObjectNode event) {
+        selectedIntents = null;
+        sendMessage(trafficMessage(number(event, "sid")));
+        stopTrafficMonitoring();
+    }
+
+
+    private synchronized long startSummaryMonitoring(ObjectNode event) {
+        stopSummaryMonitoring();
+        summaryEvent = event;
+        summaryTask = new SummaryMonitor();
+        timer.schedule(summaryTask, SUMMARY_FREQUENCY, SUMMARY_FREQUENCY);
+        return number(event, "sid");
+    }
+
+    private synchronized void stopSummaryMonitoring() {
+        if (summaryEvent != null) {
+            summaryTask.cancel();
+            summaryTask = null;
+            summaryEvent = null;
+        }
+    }
+
+    // Subscribes for summary messages.
+    private synchronized void requestSummary(ObjectNode event) {
+        sendMessage(summmaryMessage(number(event, "sid")));
+    }
+
+
+    // Forces mastership role rebalancing.
+    private void equalizeMasters(ObjectNode event) {
+        directory.get(MastershipAdminService.class).balanceRoles();
+    }
+
+
+    // Adds all internal listeners.
+    private void addListeners() {
+        clusterService.addListener(clusterListener);
+        mastershipService.addListener(mastershipListener);
+        deviceService.addListener(deviceListener);
+        linkService.addListener(linkListener);
+        hostService.addListener(hostListener);
+        intentService.addListener(intentListener);
+        flowService.addListener(flowListener);
+    }
+
+    // Removes all internal listeners.
+    private synchronized void removeListeners() {
+        if (!listenersRemoved) {
+            listenersRemoved = true;
+            clusterService.removeListener(clusterListener);
+            mastershipService.removeListener(mastershipListener);
+            deviceService.removeListener(deviceListener);
+            linkService.removeListener(linkListener);
+            hostService.removeListener(hostListener);
+            intentService.removeListener(intentListener);
+            flowService.removeListener(flowListener);
+        }
+    }
+
+    // Cluster event listener.
+    private class InternalClusterListener implements ClusterEventListener {
+        @Override
+        public void event(ClusterEvent event) {
+            sendMessage(instanceMessage(event, null));
+        }
+    }
+
+    // Mastership change listener
+    private class InternalMastershipListener implements MastershipListener {
+        @Override
+        public void event(MastershipEvent event) {
+            sendAllInstances("updateInstance");
+            Device device = deviceService.getDevice(event.subject());
+            sendMessage(deviceMessage(new DeviceEvent(DEVICE_UPDATED, device)));
+        }
+    }
+
+    // Device event listener.
+    private class InternalDeviceListener implements DeviceListener {
+        @Override
+        public void event(DeviceEvent event) {
+            sendMessage(deviceMessage(event));
+            eventAccummulator.add(event);
+        }
+    }
+
+    // Link event listener.
+    private class InternalLinkListener implements LinkListener {
+        @Override
+        public void event(LinkEvent event) {
+            sendMessage(linkMessage(event));
+            eventAccummulator.add(event);
+        }
+    }
+
+    // Host event listener.
+    private class InternalHostListener implements HostListener {
+        @Override
+        public void event(HostEvent event) {
+            sendMessage(hostMessage(event));
+            eventAccummulator.add(event);
+        }
+    }
+
+    // Intent event listener.
+    private class InternalIntentListener implements IntentListener {
+        @Override
+        public void event(IntentEvent event) {
+            if (trafficEvent != null) {
+                requestSelectedIntentTraffic(trafficEvent);
+            }
+            eventAccummulator.add(event);
+        }
+    }
+
+    // Intent event listener.
+    private class InternalFlowListener implements FlowRuleListener {
+        @Override
+        public void event(FlowRuleEvent event) {
+            eventAccummulator.add(event);
+        }
+    }
+
+    // Periodic update of the traffic information
+    private class TrafficMonitor extends TimerTask {
+        @Override
+        public void run() {
+            try {
+                if (trafficEvent != null) {
+                    String type = string(trafficEvent, "event", "unknown");
+                    if (type.equals("requestAllTraffic")) {
+                        requestAllTraffic(trafficEvent);
+                    } else if (type.equals("requestDeviceLinkFlows")) {
+                        requestDeviceLinkFlows(trafficEvent);
+                    } else if (type.equals("requestSelectedIntentTraffic")) {
+                        requestSelectedIntentTraffic(trafficEvent);
+                    }
+                }
+            } catch (Exception e) {
+                log.warn("Unable to handle traffic request due to {}", e.getMessage());
+                log.debug("Boom!", e);
+            }
+        }
+    }
+
+    // Periodic update of the summary information
+    private class SummaryMonitor extends TimerTask {
+        @Override
+        public void run() {
+            try {
+                if (summaryEvent != null) {
+                    requestSummary(summaryEvent);
+                }
+            } catch (Exception e) {
+                log.warn("Unable to handle summary request due to {}", e.getMessage());
+                log.debug("Boom!", e);
+            }
+        }
+    }
+
+    // Accumulates events to drive methodic update of the summary pane.
+    private class InternalEventAccummulator extends AbstractAccumulator<Event> {
+        protected InternalEventAccummulator() {
+            super(new Timer("topo-summary"), MAX_EVENTS, MAX_BATCH_MS, MAX_IDLE_MS);
+        }
+
+        @Override
+        public void processItems(List<Event> items) {
+            try {
+                if (summaryEvent != null) {
+                    sendMessage(summmaryMessage(0));
+                }
+            } catch (Exception e) {
+                log.warn("Unable to handle summary request due to {}", e.getMessage());
+                log.debug("Boom!", e);
+            }
+        }
+    }
+}
+
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
new file mode 100644
index 0000000..6ca1e21
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
@@ -0,0 +1,96 @@
+/*
+ * 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.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+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.UiView;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.collect.ImmutableList.of;
+import static java.util.stream.Collectors.toSet;
+
+/**
+ * Manages the user interface extensions.
+ */
+@Component(immediate = true)
+@Service
+public class UiExtensionManager implements UiExtensionService {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    // List of all extensions
+    private final List<UiExtension> extensions = Lists.newArrayList();
+
+    // Map of views to extensions
+    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 = new UiExtension(coreViews, getClass().getClassLoader());
+
+    @Activate
+    public void activate() {
+        register(core);
+        log.info("Started");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        unregister(core);
+        log.info("Stopped");
+    }
+
+    @Override
+    public synchronized void register(UiExtension extension) {
+        if (!extensions.contains(extension)) {
+            extensions.add(extension);
+            for (UiView view : extension.views()) {
+                views.put(view.id(), extension);
+            }
+        }
+    }
+
+    @Override
+    public synchronized void unregister(UiExtension extension) {
+        extensions.remove(extension);
+        extension.views().stream().map(UiView::id).collect(toSet()).forEach(views::remove);
+    }
+
+    @Override
+    public synchronized List<UiExtension> getExtensions() {
+        return ImmutableList.copyOf(extensions);
+    }
+
+    @Override
+    public synchronized UiExtension getViewExtension(String viewId) {
+        return views.get(viewId);
+    }
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/package-info.java b/web/gui/src/main/java/org/onosproject/ui/impl/package-info.java
new file mode 100644
index 0000000..cb9ae2f
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2014,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.
+ */
+
+/**
+ * Set of resources providing data for the ONOS GUI.
+ */
+package org.onosproject.ui.impl;