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/gui/AbstractTableRow.java b/web/gui/src/main/java/org/onosproject/ui/impl/AbstractTableRow.java
similarity index 97%
rename from web/gui/src/main/java/org/onosproject/gui/AbstractTableRow.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/AbstractTableRow.java
index 7be7172..66db400 100644
--- a/web/gui/src/main/java/org/onosproject/gui/AbstractTableRow.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/AbstractTableRow.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.gui;
+package org.onosproject.ui.impl;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
diff --git a/web/gui/src/main/java/org/onosproject/gui/DeviceGuiResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceGuiResource.java
similarity index 98%
rename from web/gui/src/main/java/org/onosproject/gui/DeviceGuiResource.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/DeviceGuiResource.java
index 403b8c6..d409ebf 100644
--- a/web/gui/src/main/java/org/onosproject/gui/DeviceGuiResource.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceGuiResource.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onosproject.gui;
+package org.onosproject.ui.impl;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
diff --git a/web/gui/src/main/java/org/onosproject/gui/DeviceTableRow.java b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceTableRow.java
similarity index 98%
rename from web/gui/src/main/java/org/onosproject/gui/DeviceTableRow.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/DeviceTableRow.java
index 423657a..7e67e67 100644
--- a/web/gui/src/main/java/org/onosproject/gui/DeviceTableRow.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/DeviceTableRow.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.gui;
+package org.onosproject.ui.impl;
 
 import org.onosproject.net.Device;
 import org.onosproject.net.device.DeviceService;
diff --git a/web/gui/src/main/java/org/onosproject/gui/GuiWebSocketServlet.java b/web/gui/src/main/java/org/onosproject/ui/impl/GuiWebSocketServlet.java
similarity index 98%
rename from web/gui/src/main/java/org/onosproject/gui/GuiWebSocketServlet.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/GuiWebSocketServlet.java
index 8d0eabe..5a660e0 100644
--- a/web/gui/src/main/java/org/onosproject/gui/GuiWebSocketServlet.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/GuiWebSocketServlet.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onosproject.gui;
+package org.onosproject.ui.impl;
 
 import org.eclipse.jetty.websocket.WebSocket;
 import org.eclipse.jetty.websocket.WebSocketServlet;
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/gui/RowComparator.java b/web/gui/src/main/java/org/onosproject/ui/impl/RowComparator.java
similarity index 98%
rename from web/gui/src/main/java/org/onosproject/gui/RowComparator.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/RowComparator.java
index 7c79fc2..b588f48 100644
--- a/web/gui/src/main/java/org/onosproject/gui/RowComparator.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/RowComparator.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.gui;
+package org.onosproject.ui.impl;
 
 import java.util.Comparator;
 
diff --git a/web/gui/src/main/java/org/onosproject/gui/TableRow.java b/web/gui/src/main/java/org/onosproject/ui/impl/TableRow.java
similarity index 96%
rename from web/gui/src/main/java/org/onosproject/gui/TableRow.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/TableRow.java
index 67c8449..81a14ba 100644
--- a/web/gui/src/main/java/org/onosproject/gui/TableRow.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TableRow.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package org.onosproject.gui;
+package org.onosproject.ui.impl;
 
 
 import com.fasterxml.jackson.databind.node.ObjectNode;
diff --git a/web/gui/src/main/java/org/onosproject/gui/TopologyResource.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyResource.java
similarity index 98%
rename from web/gui/src/main/java/org/onosproject/gui/TopologyResource.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/TopologyResource.java
index 980be31..95f9a07 100644
--- a/web/gui/src/main/java/org/onosproject/gui/TopologyResource.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyResource.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onosproject.gui;
+package org.onosproject.ui.impl;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
diff --git a/web/gui/src/main/java/org/onosproject/gui/TopologyViewIntentFilter.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewIntentFilter.java
similarity index 99%
rename from web/gui/src/main/java/org/onosproject/gui/TopologyViewIntentFilter.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewIntentFilter.java
index 0549604..f8b1d6c 100644
--- a/web/gui/src/main/java/org/onosproject/gui/TopologyViewIntentFilter.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewIntentFilter.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onosproject.gui;
+package org.onosproject.ui.impl;
 
 import org.onosproject.net.ConnectPoint;
 import org.onosproject.net.Device;
diff --git a/web/gui/src/main/java/org/onosproject/gui/TopologyViewMessages.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessages.java
similarity index 99%
rename from web/gui/src/main/java/org/onosproject/gui/TopologyViewMessages.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessages.java
index 746c37e..dd891cd 100644
--- a/web/gui/src/main/java/org/onosproject/gui/TopologyViewMessages.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewMessages.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onosproject.gui;
+package org.onosproject.ui.impl;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
diff --git a/web/gui/src/main/java/org/onosproject/gui/TopologyViewWebSocket.java b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewWebSocket.java
similarity index 99%
rename from web/gui/src/main/java/org/onosproject/gui/TopologyViewWebSocket.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewWebSocket.java
index 44ad026..e259cb5 100644
--- a/web/gui/src/main/java/org/onosproject/gui/TopologyViewWebSocket.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TopologyViewWebSocket.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.onosproject.gui;
+package org.onosproject.ui.impl;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
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/gui/package-info.java b/web/gui/src/main/java/org/onosproject/ui/impl/package-info.java
similarity index 95%
rename from web/gui/src/main/java/org/onosproject/gui/package-info.java
rename to web/gui/src/main/java/org/onosproject/ui/impl/package-info.java
index 7a91308..cb9ae2f 100644
--- a/web/gui/src/main/java/org/onosproject/gui/package-info.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/package-info.java
@@ -17,4 +17,4 @@
 /**
  * Set of resources providing data for the ONOS GUI.
  */
-package org.onosproject.gui;
+package org.onosproject.ui.impl;