CORD Subscriber GUI - XosFunction mementos now stored inside each SubscriberUser to capture the parameter state per user.

Change-Id: I678249f63a68172db66a5d3faa0b1747c670bf6e
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/CordModelCache.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/CordModelCache.java
index e47e68f..c71477f 100644
--- a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/CordModelCache.java
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/CordModelCache.java
@@ -26,6 +26,9 @@
 import org.onosproject.cord.gui.model.JsonFactory;
 import org.onosproject.cord.gui.model.SubscriberUser;
 import org.onosproject.cord.gui.model.UserFactory;
+import org.onosproject.cord.gui.model.XosFunction;
+import org.onosproject.cord.gui.model.XosFunctionDescriptor;
+import org.onosproject.cord.gui.model.XosFunctionFactory;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -62,12 +65,21 @@
      * Used to initialize users for the demo. These are currently fake.
      */
     public void initUsers() {
-        users.add(new SubscriberUser(1, "Mom's MacBook", MAC_1));
-        users.add(new SubscriberUser(2, "Dad's iPad", MAC_2));
-        users.add(new SubscriberUser(3, "Dick's laptop", MAC_3));
-        users.add(new SubscriberUser(4, "Jane's laptop", MAC_4));
+        users.add(createUser(1, "Mom's MacBook", MAC_1));
+        users.add(createUser(2, "Dad's iPad", MAC_2));
+        users.add(createUser(3, "Dick's laptop", MAC_3));
+        users.add(createUser(4, "Jane's laptop", MAC_4));
     }
 
+    private SubscriberUser createUser(int uid, String name, String mac) {
+        SubscriberUser user = new SubscriberUser(uid, name, mac);
+        for (XosFunction f: currentBundle.functions()) {
+            user.setMemento(f.descriptor(), f.createMemento());
+        }
+        return user;
+    }
+
+
     /**
      * Returns the currently selected bundle.
      *
@@ -84,10 +96,20 @@
      * @throws IllegalArgumentException if bundle ID is unknown
      */
     public void setCurrentBundle(String bundleId) {
-        BundleDescriptor bdesc = BundleFactory.bundleFromId(bundleId);
-        currentBundle = new Bundle(bdesc);
+        BundleDescriptor bd = BundleFactory.bundleFromId(bundleId);
+        currentBundle = new Bundle(bd);
+        // update the user mementos
+        for (SubscriberUser user: users) {
+            user.clearMementos();
+            for (XosFunction f: currentBundle.functions()) {
+                user.setMemento(f.descriptor(), f.createMemento());
+            }
+        }
+
+        // TODO: tell XOS which functions are enabled / disabled
     }
 
+
     /**
      * Returns the list of current users for this subscriber account.
      *
@@ -97,6 +119,25 @@
         return ImmutableList.copyOf(users);
     }
 
+    /**
+     * Applies a function parameter change for a user.
+     *
+     * @param userId user identifier
+     * @param funcId function identifier
+     * @param param function parameter to change
+     * @param value new value for function parameter
+     */
+    public void applyPerUserParam(String userId, String funcId,
+                                  String param, String value) {
+        // FIXME: this is not right yet...
+        int uid = Integer.parseInt(userId);
+        XosFunctionDescriptor xfd =
+                XosFunctionDescriptor.valueOf(funcId.toUpperCase());
+        XosFunctionFactory.apply(xfd, uid, param, value);
+    }
+
+    // =============
+
     private ArrayNode userJsonArray() {
         ArrayNode userList = arrayNode();
         for (SubscriberUser user: users) {
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/CordWebResource.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/CordWebResource.java
index fa4bba1..0a27290 100644
--- a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/CordWebResource.java
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/CordWebResource.java
@@ -55,9 +55,19 @@
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     @Path("bundle/{id}")
-    @Deprecated
     public Response bundle(@PathParam("id") String bundleId) {
         CordModelCache.INSTANCE.setCurrentBundle(bundleId);
         return bundle();
     }
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("users/{id}/apply/{func}/{param}/{value}")
+    public Response bundle(@PathParam("id") String userId,
+                           @PathParam("func") String funcId,
+                           @PathParam("param") String param,
+                           @PathParam("value") String value) {
+        CordModelCache.INSTANCE.applyPerUserParam(userId, funcId, param, value);
+        return users();
+    }
 }
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/Bundle.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/Bundle.java
index 02181bc..199c8d3 100644
--- a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/Bundle.java
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/Bundle.java
@@ -17,11 +17,17 @@
 
 package org.onosproject.cord.gui.model;
 
+import com.google.common.collect.ImmutableSet;
+
+import java.util.HashSet;
+import java.util.Set;
+
 /**
  * Encapsulates a bundle, including current state.
  */
 public class Bundle {
     private final BundleDescriptor bundleDescriptor;
+    private final Set<XosFunction> functions;
 
     /**
      * Constructs a new bundle instance.
@@ -30,6 +36,7 @@
      */
     public Bundle(BundleDescriptor bundleDescriptor) {
         this.bundleDescriptor = bundleDescriptor;
+        this.functions = initFunctions();
     }
 
     /**
@@ -41,4 +48,39 @@
         return bundleDescriptor;
     }
 
+    /**
+     * Returns the set of function instances for this bundle.
+     *
+     * @return the functions
+     */
+    public Set<XosFunction> functions() {
+        return ImmutableSet.copyOf(functions);
+    }
+
+    /**
+     * Creates an initial set of function instances.
+     *
+     * @return initial function instances
+     */
+    private Set<XosFunction> initFunctions() {
+        Set<XosFunction> funcs = new HashSet<XosFunction>();
+        for (XosFunctionDescriptor xfd: bundleDescriptor.functions()) {
+            funcs.add(createFunction(xfd));
+        }
+        return funcs;
+    }
+
+    private XosFunction createFunction(XosFunctionDescriptor xfd) {
+        XosFunction func;
+        switch (xfd) {
+            case URL_FILTER:
+                func = new UrlFilterFunction(xfd);
+                break;
+
+            default:
+                func = new DefaultXosFunction(xfd);
+                break;
+        }
+        return func;
+    }
 }
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/DefaultXosFunction.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/DefaultXosFunction.java
new file mode 100644
index 0000000..f4a25e7
--- /dev/null
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/DefaultXosFunction.java
@@ -0,0 +1,58 @@
+/*
+ * 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.cord.gui.model;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Default XOS function implementation, that does not have any parameters
+ * to tweak.
+ */
+public class DefaultXosFunction implements XosFunction {
+
+    protected static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private final XosFunctionDescriptor xfd;
+
+    public DefaultXosFunction(XosFunctionDescriptor xfd) {
+        this.xfd = xfd;
+    }
+
+    public XosFunctionDescriptor descriptor() {
+        return xfd;
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * This default implementation throws an exception.
+     *
+     * @param user user to apply the change to
+     * @param param parameter name
+     * @param value new parameter value
+     * @throws UnsupportedOperationException if invoked
+     */
+    public void applyParam(SubscriberUser user, String param, String value) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Memento createMemento() {
+        return null;
+    }
+
+}
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/JsonFactory.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/JsonFactory.java
index fd5cdb5..63f8522 100644
--- a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/JsonFactory.java
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/JsonFactory.java
@@ -26,7 +26,7 @@
  */
 public abstract class JsonFactory {
 
-    private static final ObjectMapper mapper = new ObjectMapper();
+    private static final ObjectMapper MAPPER = new ObjectMapper();
 
     protected static final String ID = "id";
     protected static final String NAME = "name";
@@ -38,7 +38,7 @@
      * @return empty object node
      */
     protected static ObjectNode objectNode() {
-        return mapper.createObjectNode();
+        return MAPPER.createObjectNode();
     }
 
     /**
@@ -47,6 +47,6 @@
      * @return empty array node
      */
     protected static ArrayNode arrayNode() {
-        return mapper.createArrayNode();
+        return MAPPER.createArrayNode();
     }
 }
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/SubscriberUser.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/SubscriberUser.java
index 151df57..88dd3d8 100644
--- a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/SubscriberUser.java
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/SubscriberUser.java
@@ -17,6 +17,9 @@
 
 package org.onosproject.cord.gui.model;
 
+import java.util.HashMap;
+import java.util.Map;
+
 /**
  * Designates a user of a subscriber's account.
  */
@@ -25,6 +28,9 @@
     private final String name;
     private final String mac;
 
+    private final Map<XosFunctionDescriptor, XosFunction.Memento> mementos =
+            new HashMap<XosFunctionDescriptor, XosFunction.Memento>();
+
     /**
      * Constructs a subscriber user from the given parameters.
      *
@@ -64,4 +70,33 @@
     public String mac() {
         return mac;
     }
+
+    /**
+     * Stores a memento for the given XOS function.
+     *
+     * @param f XOS function
+     * @param m memento
+     */
+    public void setMemento(XosFunctionDescriptor f, XosFunction.Memento m) {
+        if (m != null) {
+            mementos.put(f, m);
+        }
+    }
+
+    /**
+     * Returns the memento stored on this user, for the given XOS function.
+     *
+     * @param f XOS function
+     * @return memento
+     */
+    public XosFunction.Memento getMemento(XosFunctionDescriptor f) {
+        return mementos.get(f);
+    }
+
+    /**
+     * Clears the memento map.
+     */
+    public void clearMementos() {
+        mementos.clear();
+    }
 }
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/UrlFilterFunction.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/UrlFilterFunction.java
new file mode 100644
index 0000000..a4787a1
--- /dev/null
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/UrlFilterFunction.java
@@ -0,0 +1,61 @@
+/*
+ * 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.cord.gui.model;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Specialization of XosFunction for URL filtering.
+ */
+public class UrlFilterFunction extends DefaultXosFunction {
+
+    private static final String LEVEL = "level";
+
+    /**
+     * Denotes the URL filtering levels available.
+     */
+    public enum Level { PG, PG_13, R }
+
+    /**
+     * The default URL filtering level
+     */
+    public static final Level DEFAULT_LEVEL = Level.PG;
+
+    public UrlFilterFunction(XosFunctionDescriptor xfd) {
+        super(xfd);
+    }
+
+    @Override
+    public Memento createMemento() {
+        return new UrlFilterMemento();
+    }
+
+    class UrlFilterMemento implements Memento {
+        private Level level = DEFAULT_LEVEL;
+
+        public ObjectNode toObjectNode() {
+            ObjectNode node = MAPPER.createObjectNode();
+            node.put(LEVEL, level.name());
+            return node;
+        }
+
+        public void setLevel(Level level) {
+            this.level = level;
+        }
+    }
+}
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/UserFactory.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/UserFactory.java
index e143366..f377ad0 100644
--- a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/UserFactory.java
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/UserFactory.java
@@ -41,7 +41,7 @@
                 .put(ID, user.id())
                 .put(NAME, user.name())
                 .put(MAC, user.mac());
-        // TODO: add profile data
+        root.set(PROFILE, XosFunctionFactory.profileForUser(user));
         return root;
     }
 
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/XosFunction.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/XosFunction.java
index 1721685..3c02a72 100644
--- a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/XosFunction.java
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/XosFunction.java
@@ -28,16 +28,37 @@
     /**
      * Returns the descriptor for this function.
      *
-     * @return function identifier
+     * @return function descriptor
      */
     XosFunctionDescriptor descriptor();
 
     /**
-     * Returns the current state of this function, encapsulated
-     * as a JSON node.
+     * Applies a parameter change for the given user.
      *
-     * @return parameters for the function
+     * @param user user to apply change to
+     * @param param parameter name
+     * @param value new parameter value
      */
-    ObjectNode params();
+    void applyParam(SubscriberUser user, String param, String value);
+
+    /**
+     * Create an initialized memento.
+     * If the function maintains no state per user, return null.
+     *
+     * @return a new memento
+     */
+    Memento createMemento();
+
+    /**
+     * Internal state memento.
+     */
+    interface Memento {
+        /**
+         * Returns a JSON representation of this memento.
+         *
+         * @return memento state as object node
+         */
+        ObjectNode toObjectNode();
+    }
 }
 
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/XosFunctionFactory.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/XosFunctionFactory.java
index 69c5bb5..93ed831 100644
--- a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/XosFunctionFactory.java
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/XosFunctionFactory.java
@@ -1,30 +1,24 @@
 package org.onosproject.cord.gui.model;
 
-import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 
 import java.util.HashMap;
 import java.util.Map;
 
+import static org.onosproject.cord.gui.model.XosFunctionDescriptor.URL_FILTER;
+
 /**
  * Utility factory for operating on XOS functions.
  */
 public class XosFunctionFactory extends JsonFactory {
 
     private static final String PARAMS = "params";
-
     private static final String LEVEL = "level";
     private static final String LEVELS = "levels";
 
-
-    // URL Filtering Levels...
-    private static final String PG = "PG";
-    private static final String PG13 = "PG-13";
-    private static final String R = "R";
-
-    private static final String[] FILTER_LEVELS = { PG, PG13, R };
-    private static final String DEFAULT_FILTER_LEVEL = PG;
+    private static final UrlFilterFunction.Level DEFAULT_FILTER_LEVEL =
+        UrlFilterFunction.Level.PG;
 
 
     // no instantiation
@@ -45,38 +39,76 @@
         return root;
     }
 
-    private static JsonNode paramsForXfd(XosFunctionDescriptor xfd) {
-        ParamStructFactory psf = PARAM_MAP.get(xfd);
+    private static ObjectNode paramsForXfd(XosFunctionDescriptor xfd) {
+        ParamsFactory psf = PARAM_MAP.get(xfd);
         if (psf == null) {
-            psf = DEF_PARAMS;
+            psf = DEF_PARAMS_FACTORY;
         }
         return psf.params();
     }
 
-    // ==== handling different parameter structures...
-    private static final Map<XosFunctionDescriptor, ParamStructFactory>
-        PARAM_MAP = new HashMap<XosFunctionDescriptor, ParamStructFactory>();
 
-    private static final ParamStructFactory DEF_PARAMS = new ParamStructFactory();
+    // ==== handling different parameter structures...
+    private static final Map<XosFunctionDescriptor, ParamsFactory>
+        PARAM_MAP = new HashMap<XosFunctionDescriptor, ParamsFactory>();
+
+    private static final ParamsFactory DEF_PARAMS_FACTORY = new ParamsFactory();
     static {
-        PARAM_MAP.put(XosFunctionDescriptor.URL_FILTER, new UrlFilterParams());
+        PARAM_MAP.put(URL_FILTER, new UrlFilterParamsFactory());
     }
 
+    /**
+     * Applies a parameter change for the given function, in the context of
+     * the specified user.
+     *
+     * @param xfd function context
+     * @param userId user identifier
+     * @param param parameter name
+     * @param value value to apply
+     */
+    public static void apply(XosFunctionDescriptor xfd, int userId,
+                             String param, String value) {
+        // TODO:
+    }
+
+    /**
+     * Creates an object node representation of the profile for the
+     * specified user.
+     *
+     * @param user the user
+     * @return object node profile
+     */
+    public static ObjectNode profileForUser(SubscriberUser user) {
+        ObjectNode root = objectNode();
+        for (XosFunctionDescriptor xfd: XosFunctionDescriptor.values()) {
+            XosFunction.Memento mem = user.getMemento(xfd);
+            if (mem != null) {
+                root.set(xfd.id(), mem.toObjectNode());
+            }
+        }
+        return root;
+    }
+
+
+    // ===================================================================
+    // === factories for creating parameter structures, both default
+    //     and from a memento...
+
     // private parameter structure creator
-    static class ParamStructFactory {
+    static class ParamsFactory {
         ObjectNode params() {
             return objectNode();
         }
     }
 
-    static class UrlFilterParams extends ParamStructFactory {
+    static class UrlFilterParamsFactory extends ParamsFactory {
         @Override
         ObjectNode params() {
             ObjectNode result = objectNode();
-            result.put(LEVEL, DEFAULT_FILTER_LEVEL);
+            result.put(LEVEL, DEFAULT_FILTER_LEVEL.name());
             ArrayNode levels = arrayNode();
-            for (String lvl: FILTER_LEVELS) {
-                levels.add(lvl);
+            for (UrlFilterFunction.Level lvl: UrlFilterFunction.Level.values()) {
+                levels.add(lvl.name());
             }
             result.set(LEVELS, levels);
             return result;
diff --git a/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/users-1.json b/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/users-1.json
index e7fb9c2..f33625c 100644
--- a/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/users-1.json
+++ b/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/users-1.json
@@ -3,7 +3,7 @@
     {
       "id": 1,
       "name": "Mom's MacBook",
-      "mac": "01:02:03:04:05:06",
+      "mac": "010203040506",
       "profile": {
         "url_filter": {
           "level": "R"
@@ -13,7 +13,7 @@
     {
       "id": 2,
       "name": "Dad's iPad",
-      "mac": "01:02:03:04:05:77",
+      "mac": "010203040507",
       "profile": {
         "url_filter": {
           "level": "R"
@@ -23,17 +23,17 @@
     {
       "id": 3,
       "name": "Dick's laptop",
-      "mac": "01:02:03:04:05:88",
+      "mac": "010203040508",
       "profile": {
         "url_filter": {
-          "level": "PG-13"
+          "level": "PG_13"
         }
       }
     },
     {
       "id": 4,
       "name": "Jane's laptop",
-      "mac": "01:02:03:04:05:99",
+      "mac": "010203040509",
       "profile": {
         "url_filter": {
           "level": "PG"
diff --git a/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/CoreModelCacheTest.java b/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/CoreModelCacheTest.java
index bccd099..ede8641 100644
--- a/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/CoreModelCacheTest.java
+++ b/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/CoreModelCacheTest.java
@@ -51,6 +51,7 @@
     @Test
     public void basicBundleJson() {
         String json = BundleFactory.toJson(cache.getCurrentBundle());
+        System.out.println(json);
         assertTrue("bad basic json", sameJson(BASIC_BUNDLE_JSON, json));
     }
 
@@ -75,6 +76,21 @@
         assertEquals("wrong # users", 4, users.size());
     }
 
+    @Test
+    public void usersBasicJson() {
+        String json = cache.jsonUsers();
+        System.out.println(json);
+        assertTrue("bad users basic json", sameJson(USERS_BASIC, json));
+    }
+
+    @Test
+    public void usersFamilyJson() {
+        cache.setCurrentBundle("family");
+        String json = cache.jsonUsers();
+        System.out.println(json);
+        assertTrue("bad users family json", sameJson(USERS_FAMILY, json));
+    }
+
     // =============
 
     private boolean sameJson(String s1, String s2) {
@@ -91,61 +107,157 @@
     private static final ObjectMapper MAPPER = new ObjectMapper();
 
     private static final String BASIC_BUNDLE_JSON = "{\n" +
-            "  \"bundle\": {\n" +
-            "    \"id\": \"basic\",\n" +
-            "    \"name\": \"Basic Bundle\",\n" +
-            "    \"functions\": [\n" +
-            "      {\n" +
-            "        \"id\": \"internet\",\n" +
-            "        \"name\": \"Internet\",\n" +
-            "        \"desc\": \"Basic internet connectivity.\",\n" +
-            "        \"params\": {}\n" +
-            "      },\n" +
-            "      {\n" +
-            "        \"id\": \"firewall\",\n" +
-            "        \"name\": \"Firewall\",\n" +
-            "        \"desc\": \"Normal firewall protection.\",\n" +
-            "        \"params\": {}\n" +
-            "      }\n" +
+            "    \"bundle\": {\n" +
+            "        \"id\": \"basic\",\n" +
+            "        \"name\": \"Basic Bundle\",\n" +
+            "        \"desc\": \"Provides basic internet and firewall functions.\",\n" +
+            "        \"functions\": [\n" +
+            "            {\n" +
+            "                \"id\": \"internet\",\n" +
+            "                \"name\": \"Internet\",\n" +
+            "                \"desc\": \"Basic internet connectivity.\",\n" +
+            "                \"params\": {}\n" +
+            "            },\n" +
+            "            {\n" +
+            "                \"id\": \"firewall\",\n" +
+            "                \"name\": \"Firewall\",\n" +
+            "                \"desc\": \"Normal firewall protection.\",\n" +
+            "                \"params\": {}\n" +
+            "            }\n" +
+            "        ]\n" +
+            "    },\n" +
+            "    \"bundles\": [\n" +
+            "        {\n" +
+            "            \"id\": \"basic\",\n" +
+            "            \"name\": \"Basic Bundle\",\n" +
+            "            \"desc\": \"Provides basic internet and firewall functions.\"\n" +
+            "        },\n" +
+            "        {\n" +
+            "            \"id\": \"family\",\n" +
+            "            \"name\": \"Family Bundle\",\n" +
+            "            \"desc\": \"Provides internet, firewall and parental control functions.\"\n" +
+            "        }\n" +
             "    ]\n" +
-            "  },\n" +
-            "  \"bundles\": [\n" +
-            "    { \"id\": \"basic\", \"name\": \"Basic Bundle\" },\n" +
-            "    { \"id\": \"family\", \"name\": \"Family Bundle\" }\n" +
-            "  ]\n" +
             "}\n";
 
     private static final String FAMILY_BUNDLE_JSON = "{\n" +
-            "  \"bundle\": {\n" +
-            "    \"id\": \"family\",\n" +
-            "    \"name\": \"Family Bundle\",\n" +
-            "    \"functions\": [\n" +
-            "      {\n" +
-            "        \"id\": \"internet\",\n" +
-            "        \"name\": \"Internet\",\n" +
-            "        \"desc\": \"Basic internet connectivity.\",\n" +
-            "        \"params\": {}\n" +
-            "      },\n" +
-            "      {\n" +
-            "        \"id\": \"firewall\",\n" +
-            "        \"name\": \"Firewall\",\n" +
-            "        \"desc\": \"Normal firewall protection.\",\n" +
-            "        \"params\": {}\n" +
-            "      },\n" +
-            "      {\n" +
-            "        \"id\": \"url_filter\",\n" +
-            "        \"name\": \"Parental Control\",\n" +
-            "        \"desc\": \"Variable levels of URL filtering.\",\n" +
-            "        \"params\": {\n" +
-            "          \"level\": \"PG\",\n" +
-            "          \"levels\": [ \"PG\", \"PG-13\", \"R\" ]\n" +
+            "    \"bundle\": {\n" +
+            "        \"id\": \"family\",\n" +
+            "        \"name\": \"Family Bundle\",\n" +
+            "        \"desc\": \"Provides internet, firewall and parental control functions.\",\n" +
+            "        \"functions\": [\n" +
+            "            {\n" +
+            "                \"id\": \"internet\",\n" +
+            "                \"name\": \"Internet\",\n" +
+            "                \"desc\": \"Basic internet connectivity.\",\n" +
+            "                \"params\": {}\n" +
+            "            },\n" +
+            "            {\n" +
+            "                \"id\": \"firewall\",\n" +
+            "                \"name\": \"Firewall\",\n" +
+            "                \"desc\": \"Normal firewall protection.\",\n" +
+            "                \"params\": {}\n" +
+            "            },\n" +
+            "            {\n" +
+            "                \"id\": \"url_filter\",\n" +
+            "                \"name\": \"Parental Control\",\n" +
+            "                \"desc\": \"Variable levels of URL filtering.\",\n" +
+            "                \"params\": {\n" +
+            "                    \"level\": \"PG\",\n" +
+            "                    \"levels\": [\n" +
+            "                        \"PG\",\n" +
+            "                        \"PG_13\",\n" +
+            "                        \"R\"\n" +
+            "                    ]\n" +
+            "                }\n" +
+            "            }\n" +
+            "        ]\n" +
+            "    },\n" +
+            "    \"bundles\": [\n" +
+            "        {\n" +
+            "            \"id\": \"basic\",\n" +
+            "            \"name\": \"Basic Bundle\",\n" +
+            "            \"desc\": \"Provides basic internet and firewall functions.\"\n" +
+            "        },\n" +
+            "        {\n" +
+            "            \"id\": \"family\",\n" +
+            "            \"name\": \"Family Bundle\",\n" +
+            "            \"desc\": \"Provides internet, firewall and parental control functions.\"\n" +
+            "        }\n" +
+            "    ]\n" +
+            "}\n";
+
+    private static final String USERS_BASIC = "{\n" +
+            "  \"users\": [\n" +
+            "    {\n" +
+            "      \"id\": 1,\n" +
+            "      \"name\": \"Mom's MacBook\",\n" +
+            "      \"mac\": \"010203040506\",\n" +
+            "      \"profile\": { }\n" +
+            "    },\n" +
+            "    {\n" +
+            "      \"id\": 2,\n" +
+            "      \"name\": \"Dad's iPad\",\n" +
+            "      \"mac\": \"010203040507\",\n" +
+            "      \"profile\": { }\n" +
+            "    },\n" +
+            "    {\n" +
+            "      \"id\": 3,\n" +
+            "      \"name\": \"Dick's laptop\",\n" +
+            "      \"mac\": \"010203040508\",\n" +
+            "      \"profile\": { }\n" +
+            "    },\n" +
+            "    {\n" +
+            "      \"id\": 4,\n" +
+            "      \"name\": \"Jane's laptop\",\n" +
+            "      \"mac\": \"010203040509\",\n" +
+            "      \"profile\": { }\n" +
+            "    }\n" +
+            "  ]\n" +
+            "}\n";
+
+    private static final String USERS_FAMILY = "{\n" +
+            "  \"users\": [\n" +
+            "    {\n" +
+            "      \"id\": 1,\n" +
+            "      \"name\": \"Mom's MacBook\",\n" +
+            "      \"mac\": \"010203040506\",\n" +
+            "      \"profile\": {\n" +
+            "        \"url_filter\": {\n" +
+            "          \"level\": \"PG\"\n" +
             "        }\n" +
             "      }\n" +
-            "    ]\n" +
-            "  },\n" +
-            "  \"bundles\": [\n" +
-            "    { \"id\": \"basic\", \"name\": \"Basic Bundle\" },\n" +
-            "    { \"id\": \"family\", \"name\": \"Family Bundle\" }\n" +
+            "    },\n" +
+            "    {\n" +
+            "      \"id\": 2,\n" +
+            "      \"name\": \"Dad's iPad\",\n" +
+            "      \"mac\": \"010203040507\",\n" +
+            "      \"profile\": {\n" +
+            "        \"url_filter\": {\n" +
+            "          \"level\": \"PG\"\n" +
+            "        }\n" +
+            "      }\n" +
+            "    },\n" +
+            "    {\n" +
+            "      \"id\": 3,\n" +
+            "      \"name\": \"Dick's laptop\",\n" +
+            "      \"mac\": \"010203040508\",\n" +
+            "      \"profile\": {\n" +
+            "        \"url_filter\": {\n" +
+            "          \"level\": \"PG\"\n" +
+            "        }\n" +
+            "      }\n" +
+            "    },\n" +
+            "    {\n" +
+            "      \"id\": 4,\n" +
+            "      \"name\": \"Jane's laptop\",\n" +
+            "      \"mac\": \"010203040509\",\n" +
+            "      \"profile\": {\n" +
+            "        \"url_filter\": {\n" +
+            "          \"level\": \"PG\"\n" +
+            "        }\n" +
+            "      }\n" +
+            "    }\n" +
             "  ]\n" +
             "}\n";
 }