Adding server-side user preferences.

More work still needs to get done to allow client to process
server-pushed preferences updates.

Change-Id: I6e80e3f3677285cb19cfa3b6240c1b13aac56622
diff --git a/web/gui/pom.xml b/web/gui/pom.xml
index efa8520..17a7742 100644
--- a/web/gui/pom.xml
+++ b/web/gui/pom.xml
@@ -54,6 +54,11 @@
         </dependency>
         <dependency>
             <groupId>org.onosproject</groupId>
+            <artifactId>onos-core-serializers</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.onosproject</groupId>
             <artifactId>onos-incubator-api</artifactId>
         </dependency>
         <dependency>
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
index ac1a8bb..dd345b4 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/MainIndexResource.java
@@ -15,17 +15,22 @@
  */
 package org.onosproject.ui.impl;
 
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableList.Builder;
 import org.onlab.osgi.ServiceNotFoundException;
 import org.onosproject.rest.AbstractInjectionResource;
 import org.onosproject.ui.UiExtensionService;
+import org.onosproject.ui.UiPreferencesService;
 
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.SequenceInputStream;
@@ -42,12 +47,21 @@
     private static final String INDEX = "index.html";
     private static final String NOT_READY = "not-ready.html";
 
+    private static final String INJECT_USER_START = "<!-- {INJECTED-USER-START} -->";
+    private static final String INJECT_USER_END = "<!-- {INJECTED-USER-END} -->";
+
     private static final String INJECT_CSS_START = "<!-- {INJECTED-STYLESHEETS-START} -->";
     private static final String INJECT_CSS_END = "<!-- {INJECTED-STYLESHEETS-END} -->";
 
     private static final String INJECT_JS_START = "<!-- {INJECTED-JAVASCRIPT-START} -->";
     private static final String INJECT_JS_END = "<!-- {INJECTED-JAVASCRIPT-END} -->";
 
+    private static final byte[] SCRIPT_START = "\n<script>\n".getBytes();
+    private static final byte[] SCRIPT_END = "\n</script>\n\n".getBytes();
+
+    @Context
+    private SecurityContext ctx;
+
     @GET
     @Produces(MediaType.TEXT_HTML)
     public Response getMainIndex() throws IOException {
@@ -62,14 +76,25 @@
         InputStream indexTemplate = classLoader.getResourceAsStream(INDEX);
         String index = new String(toByteArray(indexTemplate));
 
-        int p1s = split(index, 0, INJECT_JS_START) - INJECT_JS_START.length();
+        int p0s = split(index,   0, INJECT_USER_START) - INJECT_USER_START.length();
+        int p0e = split(index, p0s, INJECT_USER_END);
+        int p1s = split(index, p0e, INJECT_JS_START) - INJECT_JS_START.length();
         int p1e = split(index, p1s, INJECT_JS_END);
         int p2s = split(index, p1e, INJECT_CSS_START) - INJECT_CSS_START.length();
         int p2e = split(index, p2s, INJECT_CSS_END);
         int p3s = split(index, p2e, null);
 
+        // FIXME: use global opaque auth token to allow secure failover
+        String userName = ctx.getUserPrincipal().getName();
+        String auth = "var onosAuth='" + userName + "';\n";
+
         StreamEnumeration streams =
-                new StreamEnumeration(of(stream(index, 0, p1s),
+                new StreamEnumeration(of(stream(index, 0, p0s),
+                                         new ByteArrayInputStream(SCRIPT_START),
+                                         stream(auth, 0, auth.length()),
+                                         userPreferences(userName),
+                                         new ByteArrayInputStream(SCRIPT_END),
+                                         stream(index, p0e, p1s),
                                          includeJs(service),
                                          stream(index, p1e, p2s),
                                          includeCss(service),
@@ -78,6 +103,15 @@
         return Response.ok(new SequenceInputStream(streams)).build();
     }
 
+    // Produces an input stream including user preferences.
+    private InputStream userPreferences(String userName) {
+        UiPreferencesService service = get(UiPreferencesService.class);
+        ObjectNode prefs = mapper().createObjectNode();
+        service.getPreferences(userName).forEach(prefs::set);
+        String string = "var userPrefs = " + prefs.toString() + ";";
+        return new ByteArrayInputStream(string.getBytes());
+    }
+
     // Produces an input stream including JS injections from all extensions.
     private InputStream includeJs(UiExtensionService service) {
         Builder<InputStream> builder = ImmutableList.builder();
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
index 3bf6c27..55ff611 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiExtensionManager.java
@@ -16,7 +16,19 @@
 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.BooleanNode;
+import com.fasterxml.jackson.databind.node.DoubleNode;
+import com.fasterxml.jackson.databind.node.IntNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.LongNode;
+import com.fasterxml.jackson.databind.node.NullNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.ShortNode;
+import com.fasterxml.jackson.databind.node.TextNode;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -26,40 +38,49 @@
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
+import org.onlab.util.KryoNamespace;
 import org.onosproject.mastership.MastershipService;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.EventuallyConsistentMap;
+import org.onosproject.store.service.EventuallyConsistentMapEvent;
+import org.onosproject.store.service.EventuallyConsistentMapListener;
+import org.onosproject.store.service.StorageService;
+import org.onosproject.store.service.WallClockTimestamp;
 import org.onosproject.ui.UiExtension;
 import org.onosproject.ui.UiExtensionService;
 import org.onosproject.ui.UiMessageHandlerFactory;
+import org.onosproject.ui.UiPreferencesService;
 import org.onosproject.ui.UiTopoOverlayFactory;
 import org.onosproject.ui.UiView;
 import org.onosproject.ui.UiViewHidden;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 import static com.google.common.collect.ImmutableList.of;
 import static java.util.stream.Collectors.toSet;
-import static org.onosproject.ui.UiView.Category.NETWORK;
-import static org.onosproject.ui.UiView.Category.PLATFORM;
-
 import static org.onosproject.security.AppGuard.checkPermission;
 import static org.onosproject.security.AppPermission.Type.UI_READ;
 import static org.onosproject.security.AppPermission.Type.UI_WRITE;
+import static org.onosproject.ui.UiView.Category.NETWORK;
+import static org.onosproject.ui.UiView.Category.PLATFORM;
 
 /**
  * Manages the user interface extensions.
  */
 @Component(immediate = true)
 @Service
-public class UiExtensionManager implements UiExtensionService, SpriteService {
+public class UiExtensionManager implements UiExtensionService, UiPreferencesService, SpriteService {
 
     private static final ClassLoader CL = UiExtensionManager.class.getClassLoader();
     private static final String CORE = "core";
     private static final String GUI_ADDED = "guiAdded";
     private static final String GUI_REMOVED = "guiRemoved";
+    private static final String UPDATE_PREFS = "updatePrefs";
 
     private final Logger log = LoggerFactory.getLogger(getClass());
 
@@ -72,10 +93,19 @@
     // Core views & core extension
     private final UiExtension core = createCoreExtension();
 
-
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected MastershipService mastershipService;
 
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected StorageService storageService;
+
+    // User preferences
+    private EventuallyConsistentMap<String, ObjectNode> prefs;
+    private final EventuallyConsistentMapListener<String, ObjectNode> prefsListener =
+            new InternalPrefsListener();
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
     // Creates core UI extension
     private UiExtension createCoreExtension() {
         List<UiView> coreViews = of(
@@ -98,6 +128,7 @@
 
         UiMessageHandlerFactory messageHandlerFactory =
                 () -> ImmutableList.of(
+                        new UserPreferencesMessageHandler(),
                         new TopologyViewMessageHandler(),
                         new DeviceViewMessageHandler(),
                         new LinkViewMessageHandler(),
@@ -128,12 +159,28 @@
 
     @Activate
     public void activate() {
+        KryoNamespace.Builder kryoBuilder = new KryoNamespace.Builder()
+                .register(KryoNamespaces.API)
+                .register(ObjectNode.class, ArrayNode.class,
+                          JsonNodeFactory.class, LinkedHashMap.class,
+                          TextNode.class, BooleanNode.class,
+                          LongNode.class, DoubleNode.class, ShortNode.class,
+                          IntNode.class, NullNode.class);
+
+        prefs = storageService.<String, ObjectNode>eventuallyConsistentMapBuilder()
+                .withName("onos-user-preferences")
+                .withSerializer(kryoBuilder)
+                .withTimestampProvider((k, v) -> new WallClockTimestamp())
+                .withPersistence()
+                .build();
+        prefs.addListener(prefsListener);
         register(core);
         log.info("Started");
     }
 
     @Deactivate
     public void deactivate() {
+        prefs.removeListener(prefsListener);
         UiWebSocketServlet.closeAll();
         unregister(core);
         log.info("Stopped");
@@ -171,6 +218,27 @@
         return views.get(viewId);
     }
 
+    @Override
+    public Set<String> getUserNames() {
+        ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+        prefs.keySet().forEach(k -> builder.add(userName(k)));
+        return builder.build();
+    }
+
+    @Override
+    public Map<String, ObjectNode> getPreferences(String userName) {
+        ImmutableMap.Builder<String, ObjectNode> builder = ImmutableMap.builder();
+        prefs.entrySet().stream()
+                .filter(e -> e.getKey().startsWith(userName + "/"))
+                .forEach(e -> builder.put(keyName(e.getKey()), e.getValue()));
+        return builder.build();
+    }
+
+    @Override
+    public void setPreference(String userName, String preference, ObjectNode value) {
+        prefs.put(key(userName, preference), value);
+    }
+
     // =====================================================================
     // Provisional tracking of sprite definitions
 
@@ -192,4 +260,33 @@
         return sprites.get(name);
     }
 
+    private String key(String userName, String keyName) {
+        return userName + "/" + keyName;
+    }
+
+    private String userName(String key) {
+        return key.split("/")[0];
+    }
+
+    private String keyName(String key) {
+        return key.split("/")[1];
+    }
+
+    // Auxiliary listener to preference map events.
+    private class InternalPrefsListener
+            implements EventuallyConsistentMapListener<String, ObjectNode> {
+        @Override
+        public void event(EventuallyConsistentMapEvent<String, ObjectNode> event) {
+            String userName = userName(event.key());
+            if (event.type() == EventuallyConsistentMapEvent.Type.PUT) {
+                UiWebSocketServlet.sendToUser(userName, UPDATE_PREFS, jsonPrefs());
+            }
+        }
+
+        private ObjectNode jsonPrefs() {
+            ObjectNode json = mapper.createObjectNode();
+            prefs.entrySet().forEach(e -> json.set(keyName(e.getKey()), e.getValue()));
+            return json;
+        }
+    }
 }
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
index d2d6705..efe30843 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocket.java
@@ -54,6 +54,7 @@
 
     private Connection connection;
     private FrameConnection control;
+    private String userName;
 
     private final ObjectMapper mapper = new ObjectMapper();
 
@@ -66,9 +67,16 @@
      * Creates a new web-socket for serving data to GUI.
      *
      * @param directory service directory
+     * @param userName user name of the logged-in user
      */
-    public UiWebSocket(ServiceDirectory directory) {
+    public UiWebSocket(ServiceDirectory directory, String userName) {
         this.directory = directory;
+        this.userName = userName;
+    }
+
+    @Override
+    public String userName() {
+        return userName;
     }
 
     /**
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java
index 564b07f..a9f349f 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UiWebSocketServlet.java
@@ -70,7 +70,10 @@
         if (isStopped) {
             return null;
         }
-        UiWebSocket socket = new UiWebSocket(directory);
+
+        // FIXME: Replace this with globally shared opaque token to allow secure failover
+        String userName = request.getUserPrincipal().getName();
+        UiWebSocket socket = new UiWebSocket(directory, userName);
         synchronized (sockets) {
             sockets.add(socket);
         }
@@ -89,6 +92,20 @@
         }
     }
 
+    /**
+     * Sends the specified message to all the GUI clients of the specified user.
+     *
+     * @param userName user name
+     * @param type     message type
+     * @param payload  message payload
+     */
+    static void sendToUser(String userName, String type, ObjectNode payload) {
+        if (instance != null) {
+            instance.sockets.stream().filter(ws -> userName.equals(ws.userName()))
+                    .forEach(ws -> ws.sendMessage(type, 0, payload));
+        }
+    }
+
     // Task for pruning web-sockets that are idle.
     private class Pruner extends TimerTask {
         @Override
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/UserPreferencesMessageHandler.java b/web/gui/src/main/java/org/onosproject/ui/impl/UserPreferencesMessageHandler.java
new file mode 100644
index 0000000..cc6fa59
--- /dev/null
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/UserPreferencesMessageHandler.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2016 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;
+import com.google.common.collect.ImmutableSet;
+import org.onosproject.ui.RequestHandler;
+import org.onosproject.ui.UiMessageHandler;
+import org.onosproject.ui.UiPreferencesService;
+
+import java.util.Collection;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+/**
+ * Message handler for intercepting user preferences messages.
+ */
+class UserPreferencesMessageHandler extends UiMessageHandler {
+
+    private static final String UPDATE_PREFS_REQ = "updatePrefReq";
+    private static final String KEY = "key";
+    private static final String VALUE = "value";
+
+    @Override
+    protected Collection<RequestHandler> createRequestHandlers() {
+        return ImmutableSet.of(new UpdatePreferencesRequest());
+    }
+
+    private final class UpdatePreferencesRequest extends RequestHandler {
+        private UpdatePreferencesRequest() {
+            super(UPDATE_PREFS_REQ);
+        }
+
+        @Override
+        public void process(long sid, ObjectNode payload) {
+            if (!isNullOrEmpty(connection().userName())) {
+                UiPreferencesService service = get(UiPreferencesService.class);
+                service.setPreference(connection().userName(),
+                                      payload.get(KEY).asText(),
+                                      (ObjectNode) payload.get(VALUE));
+            }
+        }
+    }
+}
diff --git a/web/gui/src/main/webapp/app/fw/layer/loading.js b/web/gui/src/main/webapp/app/fw/layer/loading.js
index 86dd56d..a9e141eb 100644
--- a/web/gui/src/main/webapp/app/fw/layer/loading.js
+++ b/web/gui/src/main/webapp/app/fw/layer/loading.js
@@ -122,9 +122,9 @@
 
     angular.module('onosLayer')
         .factory('LoadingService',
-        ['$log', '$timeout', 'ThemeService', 'FnService',
+        ['$log', '$timeout', 'ThemeService', 'FnService', 'WebSocketService',
 
-            function (_$log_, _$timeout_, _ts_, _fs_) {
+            function (_$log_, _$timeout_, _ts_, _fs_, wss) {
             $log = _$log_;
             $timeout = _$timeout_;
             ts = _ts_;
@@ -132,11 +132,13 @@
 
             preloadImages();
 
-            return {
+            var self = {
                 start: start,
                 stop: stop,
                 waiting: waiting
             };
+            wss._setLoadingDelegate(self);
+            return self;
         }]);
 
 }());
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/app/fw/layer/veil.js b/web/gui/src/main/webapp/app/fw/layer/veil.js
index fc0530a..fddfe7f 100644
--- a/web/gui/src/main/webapp/app/fw/layer/veil.js
+++ b/web/gui/src/main/webapp/app/fw/layer/veil.js
@@ -79,21 +79,23 @@
 
     angular.module('onosLayer')
     .factory('VeilService',
-        ['$log', '$route', 'FnService', 'KeyService', 'GlyphService',
+        ['$log', '$route', 'FnService', 'KeyService', 'GlyphService', 'WebSocketService',
 
-        function (_$log_, _$route_, _fs_, _ks_, _gs_) {
+        function (_$log_, _$route_, _fs_, _ks_, _gs_, wss) {
             $log = _$log_;
             $route = _$route_;
             fs = _fs_;
             ks = _ks_;
             gs = _gs_;
 
-            return {
+            var self = {
                 init: init,
                 show: show,
                 hide: hide,
                 lostServer: lostServer
             };
+            wss._setVeilDelegate(self);
+            return self;
     }]);
 
 }());
diff --git a/web/gui/src/main/webapp/app/fw/remote/websocket.js b/web/gui/src/main/webapp/app/fw/remote/websocket.js
index e39a7f9..92d2d2e 100644
--- a/web/gui/src/main/webapp/app/fw/remote/websocket.js
+++ b/web/gui/src/main/webapp/app/fw/remote/websocket.js
@@ -59,7 +59,7 @@
 
     function handleOpen() {
         $log.info('Web socket open - ', url);
-        vs.hide();
+        vs && vs.hide();
 
         if (fs.debugOn('txrx')) {
             $log.debug('Sending ' + pendingEvents.length + ' pending event(s)...');
@@ -105,14 +105,14 @@
         var gsucc;
 
         $log.info('Web socket closed');
-        ls.stop();
+        ls && ls.stop();
         wsUp = false;
 
         if (gsucc = findGuiSuccessor()) {
             createWebSocket(webSockOpts, gsucc);
         } else {
             // If no controllers left to contact, show the Veil...
-            vs.show([
+            vs && vs.show([
                 'Oops!',
                 'Web-socket connection to server closed...',
                 'Try refreshing the page.'
@@ -296,22 +296,29 @@
         }
     }
 
+    // Binds the veil service as a delegate
+    function setVeilDelegate(vd) {
+        vs = vd;
+    }
+
+    // Binds the loading service as a delegate
+    function setLoadingDelegate(ld) {
+        ls = ld;
+    }
+
 
     // ============================
     // ===== Definition of module
     angular.module('onosRemote')
     .factory('WebSocketService',
         ['$log', '$location', 'FnService', 'UrlFnService', 'WSock',
-            'VeilService', 'LoadingService',
 
-        function (_$log_, _$loc_, _fs_, _ufs_, _wsock_, _vs_, _ls_) {
+        function (_$log_, _$loc_, _fs_, _ufs_, _wsock_) {
             $log = _$log_;
             $loc = _$loc_;
             fs = _fs_;
             ufs = _ufs_;
             wsock = _wsock_;
-            vs = _vs_;
-            ls = _ls_;
 
             bindHandlers(builtinHandlers);
 
@@ -324,7 +331,10 @@
                 addOpenListener: addOpenListener,
                 removeOpenListener: removeOpenListener,
                 sendEvent: sendEvent,
-                isConnected: function () { return wsUp; }
+                isConnected: function () { return wsUp; },
+
+                _setVeilDelegate: setVeilDelegate,
+                _setLoadingDelegate: setLoadingDelegate
             };
         }
     ]);
diff --git a/web/gui/src/main/webapp/app/fw/util/prefs.js b/web/gui/src/main/webapp/app/fw/util/prefs.js
index 02a2390..ad3dc04 100644
--- a/web/gui/src/main/webapp/app/fw/util/prefs.js
+++ b/web/gui/src/main/webapp/app/fw/util/prefs.js
@@ -21,54 +21,14 @@
     'use strict';
 
     // injected refs
-    var $log, $cookies, fs;
+    var $log, fs, wss;
 
     // internal state
     var cache = {};
 
-    // NOTE: in Angular 1.3.5, $cookies is just a simple object, and
-    //       cookie values are just strings. From the 1.3.5 docs:
-    //
-    //       "Only a simple Object is exposed and by adding or removing
-    //        properties to/from this object, new cookies are created/deleted
-    //        at the end of current $eval. The object's properties can only
-    //        be strings."
-    //
-    //       We may want to upgrade the version of Angular sometime soon
-    //        since later version support objects as cookie values.
-
-    // NOTE: prefs represented as simple name/value pairs
-    //       => a temporary restriction while we are encoding into cookies
-    /*
-        {
-          foo: 1,
-          bar: 0,
-          goo: 2
-        }
-
-        stored as "foo:1,bar:0,goo:2"
-     */
-
-    // reads cookie with given name and returns an object rep of its value
-    // or null if no such cookie is set
-    function getPrefs(name) {
-        var cook = $cookies[name],
-            bits,
-            obj = {};
-
-        if (cook) {
-            bits = cook.split(',');
-            bits.forEach(function (value) {
-                var x = value.split(':');
-                obj[x[0]] = x[1];
-            });
-
-            // update the cache
-            cache[name] = obj;
-            return obj;
-        }
-        // perhaps we have a cached copy..
-        return cache[name];
+    // returns the preference by the specified name
+    function getPrefs(name, defaults) {
+        return cache[name] || defaults;
     }
 
     // converts string values to numbers for selected (or all) keys
@@ -89,34 +49,28 @@
     }
 
     function setPrefs(name, obj) {
-        var bits = [],
-            str;
-
-        angular.forEach(obj, function (value, key) {
-            bits.push(key + ':' + value);
-        });
-        str = bits.join(',');
-
-        // keep a cached copy of the object
+        // keep a cached copy of the object and send an update to server
         cache[name] = obj;
-
-        // The angular way of doing this...
-        // $cookies[name] = str;
-        //  ...but it appears that this gets delayed, and doesn't 'stick' ??
-
-        // FORCE cookie to be set by writing directly to document.cookie...
-        document.cookie = name + '=' + encodeURIComponent(str);
-        if (fs.debugOn('prefs')) {
-            $log.debug('<<>> Wrote cookie <'+name+'>:', str);
-        }
+        wss.sendEvent('updatePrefReq', { key: name, value: obj });
+    }
+    
+    function updatePrefs(data) {
+        $log.info('User properties updated');
+        cache[data.key] = data.value;
     }
 
     angular.module('onosUtil')
-    .factory('PrefsService', ['$log', '$cookies', 'FnService',
-        function (_$log_, _$cookies_, _fs_) {
+    .factory('PrefsService', ['$log', 'FnService', 'WebSocketService',
+        function (_$log_, _fs_, _wss_) {
             $log = _$log_;
-            $cookies = _$cookies_;
             fs = _fs_;
+            wss = _wss_;
+
+            cache = userPrefs;
+
+            wss.bindHandlers({
+                updatePrefs: updatePrefs
+            });
 
             return {
                 getPrefs: getPrefs,
diff --git a/web/gui/src/main/webapp/app/fw/util/theme.js b/web/gui/src/main/webapp/app/fw/util/theme.js
index 0e0ee64..4441402 100644
--- a/web/gui/src/main/webapp/app/fw/util/theme.js
+++ b/web/gui/src/main/webapp/app/fw/util/theme.js
@@ -20,7 +20,7 @@
 (function () {
     'use strict';
 
-    var $log, fs;
+    var $log, fs, ps;
 
     var themes = ['light', 'dark'],
         themeStr = themes.join(' '),
@@ -29,7 +29,7 @@
         nextListenerId = 1;
 
     function init() {
-        thidx = 0;
+        thidx = ps.getPrefs('theme', { idx: 0 }).idx;
         updateBodyClass();
     }
 
@@ -37,10 +37,11 @@
         return themes[thidx];
     }
 
-    function setTheme(t) {
+    function setTheme(t, force) {
         var idx = themes.indexOf(t);
-        if (idx > -1 && idx !== thidx) {
+        if (force || idx > -1 && idx !== thidx) {
             thidx = idx;
+            ps.setPrefs('theme', { idx: thidx });
             updateBodyClass();
             themeEvent('set');
         }
@@ -49,6 +50,7 @@
     function toggleTheme() {
         var i = thidx + 1;
         thidx = (i===themes.length) ? 0 : i;
+        ps.setPrefs('theme', { idx: thidx });
         updateBodyClass();
         themeEvent('toggle');
         return getTheme();
@@ -97,11 +99,11 @@
     }
 
     angular.module('onosUtil')
-        .factory('ThemeService', ['$log', 'FnService',
-        function (_$log_, _fs_) {
+        .factory('ThemeService', ['$log', 'FnService', 'PrefsService',
+        function (_$log_, _fs_, _ps_) {
             $log = _$log_;
             fs = _fs_;
-            thidx = 0;
+            ps = _ps_;
 
             return {
                 init: init,
diff --git a/web/gui/src/main/webapp/app/fw/widget/toolbar.js b/web/gui/src/main/webapp/app/fw/widget/toolbar.js
index 050afd0..3b9e5ed 100644
--- a/web/gui/src/main/webapp/app/fw/widget/toolbar.js
+++ b/web/gui/src/main/webapp/app/fw/widget/toolbar.js
@@ -219,6 +219,10 @@
             }
         }
 
+        function isVisible() {
+            return panel.isVisible();
+        }
+
         return {
             addButton: addButton,
             addToggle: addToggle,
@@ -228,7 +232,8 @@
 
             show: show,
             hide: hide,
-            toggle: toggle
+            toggle: toggle,
+            isVisible: isVisible
         };
     }
 
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index 9b532f6..069fa42 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -30,7 +30,7 @@
 
     // references to injected services
     var $scope, $log, $cookies, fs, ks, zs, gs, ms, sus, flash, wss, ps, th,
-        tds, tes, tfs, tps, tis, tss, tls, tts, tos, fltr, ttbs, tspr, ttip, tov;
+        tds, t3s, tes, tfs, tps, tis, tss, tls, tts, tos, fltr, ttbs, tspr, ttip, tov;
 
     // DOM elements
     var ovtopo, svg, defs, zoomLayer, mapG, spriteG, forceG, noDevsLayer;
@@ -369,16 +369,14 @@
 
     function setUpMap($loc) {
         var qp = $loc.search(),
-            pr = ps.getPrefs('topo_mapid'),
-            mi1 = qp.mapid,
-            mi2 = pr && pr.id,
-            mapId = mi1 || mi2 || 'usa',
-            ms1 = qp.mapscale,
-            ms2 = pr && pr.scale,
-            mapScale = ms1 || ms2 || 1,
-            t1 = qp.tint,
-            t2 = pr && pr.tint,
-            tint = t1 || t2 || 'off',
+            pr = ps.getPrefs('topo_mapid', {
+                id: qp.mapid || 'usa',
+                scale: qp.mapscale || 1,
+                tint: qp.tint || 'off'
+            }),
+            mapId = pr.id,
+            mapScale = pr.scale,
+            tint = pr.tint,
             promise,
             cfilter;
 
@@ -441,8 +439,8 @@
 
     function setUpSprites($loc, tspr) {
         var s1 = $loc.search().sprites,
-            s2 = ps.getPrefs('topo_sprites'),
-            sprId = s1 || (s2 && s2.id);
+            s2 = ps.getPrefs('topo_sprites', { id: s1 }),
+            sprId = s2.id;
 
         spriteG = zoomLayer.append ('g').attr('id', 'topo-sprites');
         if (sprId) {
@@ -463,7 +461,7 @@
 
     function restoreConfigFromPrefs() {
         // NOTE: toolbar will have set this for us..
-        prefsState = ps.asNumbers(ps.getPrefs('topo_prefs'));
+        prefsState = ps.asNumbers(ps.getPrefs('topo_prefs', ttbs.defaultPrefs));
 
         $log.debug('TOPO- Prefs State:', prefsState);
 
@@ -476,6 +474,7 @@
         togglePorts(prefsState.porthl);
         toggleMap(prefsState.bg);
         toggleSprites(prefsState.spr);
+        t3s.setDevLabIndex(prefsState.dlbls);
         flash.enable(true);
     }
 
@@ -484,7 +483,7 @@
     //  have opened the websocket to the server; hence this extra function
     // invoked after tes.start()
     function restoreSummaryFromPrefs() {
-        prefsState = ps.asNumbers(ps.getPrefs('topo_prefs'));
+        prefsState = ps.asNumbers(ps.getPrefs('topo_prefs', ttbs.defaultPrefs));
         $log.debug('TOPO- Prefs SUMMARY State:', prefsState.summary);
 
         flash.enable(false);
@@ -506,7 +505,7 @@
             '$cookies', 'FnService', 'MastService', 'KeyService', 'ZoomService',
             'GlyphService', 'MapService', 'SvgUtilService', 'FlashService',
             'WebSocketService', 'PrefsService', 'ThemeService',
-            'TopoDialogService',
+            'TopoDialogService', 'TopoD3Service',
             'TopoEventService', 'TopoForceService', 'TopoPanelService',
             'TopoInstService', 'TopoSelectService', 'TopoLinkService',
             'TopoTrafficService', 'TopoObliqueService', 'TopoFilterService',
@@ -515,7 +514,7 @@
 
         function (_$scope_, _$log_, $loc, $timeout, _$cookies_, _fs_, mast, _ks_,
                   _zs_, _gs_, _ms_, _sus_, _flash_, _wss_, _ps_, _th_,
-                  _tds_, _tes_,
+                  _tds_, _t3s_, _tes_,
                   _tfs_, _tps_, _tis_, _tss_, _tls_, _tts_, _tos_, _fltr_,
                   _ttbs_, _tspr_, _ttip_, _tov_) {
             var params = $loc.search(),
@@ -545,6 +544,7 @@
             ps = _ps_;
             th = _th_;
             tds = _tds_;
+            t3s = _t3s_;
             tes = _tes_;
             tfs = _tfs_;
             // TODO: consider funnelling actions through TopoForceService...
@@ -601,7 +601,7 @@
             setUpNoDevs();
             setUpMap($loc).then(
                 function (proj) {
-                    var z = ps.getPrefs('topo_zoom') || {tx:0, ty:0, sc:1};
+                    var z = ps.getPrefs('topo_zoom', { tx:0, ty:0, sc:1 });
                     zoomer.panZoom([z.tx, z.ty], z.sc);
                     $log.debug('** Zoom restored:', z);
 
diff --git a/web/gui/src/main/webapp/app/view/topo/topoD3.js b/web/gui/src/main/webapp/app/view/topo/topoD3.js
index a14ccbe..ef704c7 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoD3.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoD3.js
@@ -23,7 +23,7 @@
     'use strict';
 
     // injected refs
-    var $log, fs, sus, is, ts;
+    var $log, fs, sus, is, ts, ps, ttbs;
 
     // api to topoForce
     var api;
@@ -159,7 +159,7 @@
     // ====
 
     function incDevLabIndex() {
-        deviceLabelIndex = (deviceLabelIndex+1) % 3;
+        setDevLabIndex(deviceLabelIndex+1);
         switch(deviceLabelIndex) {
             case 0: return 'Hide device labels';
             case 1: return 'Show friendly device labels';
@@ -167,6 +167,13 @@
         }
     }
 
+    function setDevLabIndex(mode) {
+        deviceLabelIndex = mode % 3;
+        var p = ps.getPrefs('topo_prefs', ttbs.defaultPrefs);
+        p.dlbls = deviceLabelIndex;
+        ps.setPrefs('topo_prefs', p);
+    }
+
     // Returns the newly computed bounding box of the rectangle
     function adjustRectToFitText(n) {
         var text = n.select('text'),
@@ -599,13 +606,16 @@
     angular.module('ovTopo')
     .factory('TopoD3Service',
         ['$log', 'FnService', 'SvgUtilService', 'IconService', 'ThemeService',
+            'PrefsService', 'TopoToolbarService',
 
-        function (_$log_, _fs_, _sus_, _is_, _ts_) {
+        function (_$log_, _fs_, _sus_, _is_, _ts_, _ps_, _ttbs_) {
             $log = _$log_;
             fs = _fs_;
             sus = _sus_;
             is = _is_;
             ts = _ts_;
+            ps = _ps_;
+            ttbs = _ttbs_;
 
             icfg = is.iconConfig();
 
@@ -620,6 +630,7 @@
                 destroyD3: destroyD3,
 
                 incDevLabIndex: incDevLabIndex,
+                setDevLabIndex: setDevLabIndex,
                 adjustRectToFitText: adjustRectToFitText,
                 hostLabel: hostLabel,
                 deviceLabel: deviceLabel,
diff --git a/web/gui/src/main/webapp/app/view/topo/topoPanel.js b/web/gui/src/main/webapp/app/view/topo/topoPanel.js
index 34e85f3..e1cef91 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoPanel.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoPanel.js
@@ -529,6 +529,7 @@
 
                 showSummary: showSummary,
                 toggleSummary: toggleSummary,
+                hideSummary: hideSummaryPanel,
 
                 toggleUseDetailsFlag: toggleUseDetailsFlag,
                 displaySingle: displaySingle,
@@ -538,8 +539,6 @@
                 displaySomething: displaySomething,
                 addAction: addAction,
 
-                hideSummaryPanel: hideSummaryPanel,
-
                 detailVisible: function () { return detail.panel().isVisible(); },
                 summaryVisible: function () { return summary.panel().isVisible(); }
             };
diff --git a/web/gui/src/main/webapp/app/view/topo/topoToolbar.js b/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
index deb49a5..535cd17 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoToolbar.js
@@ -70,11 +70,12 @@
 
     // initial toggle state: default settings and tag to key mapping
     var defaultPrefsState = {
-            summary: 1,
             insts: 1,
+            summary: 1,
             detail: 1,
             hosts: 0,
             offdev: 1,
+            dlbls: 0,
             porthl: 1,
             bg: 0,
             spr: 0,
@@ -104,7 +105,7 @@
     }
 
     function setInitToggleState() {
-        cachedState = ps.asNumbers(ps.getPrefs(cooktag));
+        cachedState = ps.asNumbers(ps.getPrefs(cooktag, defaultPrefsState));
         $log.debug('TOOLBAR---- read prefs state:', cachedState);
 
         if (!cachedState) {
@@ -264,6 +265,9 @@
 
     function toggleToolbar() {
         toolbar.toggle();
+        var prefs = ps.getPrefs(cooktag, defaultPrefsState);
+        prefs.toolbar = !prefs.toolbar;
+        ps.setPrefs('topo_prefs', prefs);
     }
 
     function setDefaultOverlay() {
@@ -298,6 +302,7 @@
                 keyListener: keyListener,
                 toggleToolbar: toggleToolbar,
                 setDefaultOverlay: setDefaultOverlay,
+                defaultPrefs: defaultPrefsState,
                 fnkey: fnkey
             };
         }]);
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index d014515..1a645ad 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -38,6 +38,9 @@
     <script src="tp/Chart.min.js"></script>
     <script src="tp/angular-chart.min.js"></script>
 
+    <!-- {INJECTED-USER-START} -->
+    <!-- {INJECTED-USER-END} -->
+
     <!-- ONOS UI Framework included here -->
     <!-- TODO: use a single catenated-minified file here -->
     <script src="onos.js"></script>