CORD Subscriber GUI -- More bundle wrangling.

Change-Id: I2fafdb281712d7747399d61611c3d4bb663a39b5
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 847527f..f09ecfe 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
@@ -17,9 +17,75 @@
 
 package org.onosproject.cord.gui;
 
+import com.google.common.collect.ImmutableList;
+import org.onosproject.cord.gui.model.Bundle;
+import org.onosproject.cord.gui.model.BundleDescriptor;
+import org.onosproject.cord.gui.model.BundleFactory;
+import org.onosproject.cord.gui.model.SubscriberUser;
+
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * In memory cache of the model of the subscriber's account.
  */
 public class CordModelCache {
 
+    // faked for the demo
+    private static final int SUBSCRIBER_ID = 92;
+    private static final String MAC_1 = "010203040506";
+    private static final String MAC_2 = "010203040507";
+    private static final String MAC_3 = "010203040508";
+    private static final String MAC_4 = "010203040509";
+
+    private Bundle currentBundle;
+    private final List<SubscriberUser> users;
+
+    /**
+     * Constructs a model cache, initializing it with basic bundle.
+     */
+    public CordModelCache() {
+        currentBundle = new Bundle(BundleFactory.BASIC_BUNDLE);
+        users = new ArrayList<SubscriberUser>();
+        initUsers();
+    }
+
+    /**
+     * 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));
+    }
+
+    /**
+     * Returns the currently selected bundle.
+     *
+     * @return current bundle
+     */
+    public Bundle getCurrentBundle() {
+        return currentBundle;
+    }
+
+    /**
+     * Sets a new bundle.
+     *
+     * @param bundleId bundle identifier
+     * @throws IllegalArgumentException if bundle ID is unknown
+     */
+    public void setCurrentBundle(String bundleId) {
+        BundleDescriptor bdesc = BundleFactory.bundleFromId(bundleId);
+        currentBundle = new Bundle(bdesc);
+    }
+
+    /**
+     * Returns the list of current users for this subscriber account.
+     *
+     * @return the list of users
+     */
+    public List<SubscriberUser> getUsers() {
+        return ImmutableList.copyOf(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
new file mode 100644
index 0000000..02181bc
--- /dev/null
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/Bundle.java
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+/**
+ * Encapsulates a bundle, including current state.
+ */
+public class Bundle {
+    private final BundleDescriptor bundleDescriptor;
+
+    /**
+     * Constructs a new bundle instance.
+     *
+     * @param bundleDescriptor the descriptor
+     */
+    public Bundle(BundleDescriptor bundleDescriptor) {
+        this.bundleDescriptor = bundleDescriptor;
+    }
+
+    /**
+     * Returns the bundle descriptor.
+     *
+     * @return the descriptor
+     */
+    public BundleDescriptor descriptor() {
+        return bundleDescriptor;
+    }
+
+}
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/BundleFactory.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/BundleFactory.java
index 7f0bad2..0b3c90e 100644
--- a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/BundleFactory.java
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/BundleFactory.java
@@ -17,14 +17,20 @@
 
 package org.onosproject.cord.gui.model;
 
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.collect.ImmutableList;
 
 import java.util.List;
 
 /**
- * Utility factory for creating bundles and functions etc.
+ * Utility factory for creating and/or operating on bundles.
  */
-public class BundleFactory {
+public class BundleFactory extends JsonFactory {
+
+    private static final String BUNDLE = "bundle";
+    private static final String BUNDLES = "bundles";
+    private static final String FUNCTIONS = "functions";
 
     private static final String BASIC_ID = "basic";
     private static final String BASIC_DISPLAY_NAME = "Basic Bundle";
@@ -35,23 +41,83 @@
     // no instantiation
     private BundleFactory() {}
 
-    private static final BundleDescriptor BASIC =
+    /**
+     * Designates the BASIC bundle.
+     */
+    public static final BundleDescriptor BASIC_BUNDLE =
             new DefaultBundleDescriptor(BASIC_ID, BASIC_DISPLAY_NAME,
                                         XosFunctionDescriptor.INTERNET,
                                         XosFunctionDescriptor.FIREWALL);
 
-    private static final BundleDescriptor FAMILY =
+    /**
+     * Designates the FAMILY bundle.
+     */
+    public static final BundleDescriptor FAMILY_BUNDLE =
             new DefaultBundleDescriptor(FAMILY_ID, FAMILY_DISPLAY_NAME,
                                         XosFunctionDescriptor.INTERNET,
                                         XosFunctionDescriptor.FIREWALL,
                                         XosFunctionDescriptor.URL_FILTER);
 
+    // all bundles, in the order they should be listed in the GUI
+    private static final List<BundleDescriptor> ALL_BUNDLES = ImmutableList.of(
+            BASIC_BUNDLE,
+            FAMILY_BUNDLE
+    );
+
     /**
      * Returns the list of available bundles.
      *
      * @return available bundles
      */
     public static List<BundleDescriptor> availableBundles() {
-        return ImmutableList.of(BASIC, FAMILY);
+        return ALL_BUNDLES;
+    }
+
+    /**
+     * Returns the bundle descriptor for the given identifier.
+     *
+     * @param bundleId bundle identifier
+     * @return bundle descriptor
+     * @throws IllegalArgumentException if bundle ID is unknown
+     */
+    public static BundleDescriptor bundleFromId(String bundleId) {
+        for (BundleDescriptor bd : ALL_BUNDLES) {
+            if (bd.id().equals(bundleId)) {
+                return bd;
+            }
+        }
+        throw new IllegalArgumentException("unknown bundle: " + bundleId);
+    }
+
+    /**
+     * Returns a JSON string representation of the given bundle.
+     *
+     * @param bundle the bundle
+     * @return JSON string
+     */
+    public static String toJson(Bundle bundle) {
+        ObjectNode root = objectNode();
+
+        ObjectNode bnode = objectNode()
+                .put(ID, bundle.descriptor().id())
+                .put(NAME, bundle.descriptor().displayName());
+
+        ArrayNode funcs = arrayNode();
+        for (XosFunctionDescriptor xfd: bundle.descriptor().functions()) {
+            funcs.add(XosFunctionFactory.toObjectNode(xfd));
+        }
+        bnode.set(FUNCTIONS, funcs);
+        root.set(BUNDLE, bnode);
+
+        ArrayNode bundles = arrayNode();
+        for (BundleDescriptor bd: BundleFactory.availableBundles()) {
+            ObjectNode bdnode = objectNode()
+                    .put(ID, bd.id())
+                    .put(NAME, bd.displayName());
+            bundles.add(bdnode);
+        }
+        root.set(BUNDLES, bundles);
+        return root.toString();
+
     }
 }
diff --git a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/DefaultBundleDescriptor.java b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/DefaultBundleDescriptor.java
index 45a2bda..509e179 100644
--- a/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/DefaultBundleDescriptor.java
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/DefaultBundleDescriptor.java
@@ -57,4 +57,27 @@
     public Set<XosFunctionDescriptor> functions() {
         return functions;
     }
+
+    @Override
+    public String toString() {
+        return "{BundleDescriptor: " + displayName + "}";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        DefaultBundleDescriptor that = (DefaultBundleDescriptor) o;
+        return id.equals(that.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return id.hashCode();
+    }
 }
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
new file mode 100644
index 0000000..5592b51
--- /dev/null
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/JsonFactory.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Base class for factories that convert objects to JSON.
+ */
+public abstract class JsonFactory {
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    protected static final String ID = "id";
+    protected static final String NAME = "name";
+
+    /**
+     * Returns a freshly minted object node.
+     *
+     * @return empty object node
+     */
+    protected static ObjectNode objectNode() {
+        return mapper.createObjectNode();
+    }
+
+    /**
+     * Returns a freshly minted array node.
+     *
+     * @return empty array node
+     */
+    protected static ArrayNode arrayNode() {
+        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
new file mode 100644
index 0000000..151df57
--- /dev/null
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/SubscriberUser.java
@@ -0,0 +1,67 @@
+/*
+ * 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;
+
+/**
+ * Designates a user of a subscriber's account.
+ */
+public class SubscriberUser {
+    private final int id;
+    private final String name;
+    private final String mac;
+
+    /**
+     * Constructs a subscriber user from the given parameters.
+     *
+     * @param id internal identifier
+     * @param name display name
+     * @param mac MAC address of the associated device
+     */
+    public SubscriberUser(int id, String name, String mac) {
+        this.id = id;
+        this.name = name;
+        this.mac = mac;
+    }
+
+    /**
+     * Returns the internal identifier.
+     *
+     * @return the identifier
+     */
+    public int id() {
+        return id;
+    }
+
+    /**
+     * Returns the display name.
+     *
+     * @return display name
+     */
+    public String name() {
+        return name;
+    }
+
+    /**
+     * Returns the MAC address of the associated device.
+     *
+     * @return MAC address
+     */
+    public String mac() {
+        return mac;
+    }
+}
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
new file mode 100644
index 0000000..e143366
--- /dev/null
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/UserFactory.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+/**
+ * Utility functions on users.
+ */
+public class UserFactory extends JsonFactory {
+
+    private static final String MAC = "mac";
+    private static final String PROFILE = "profile";
+
+    // no instantiation
+    private UserFactory() {}
+
+    /**
+     * Returns an object node representation of the given user.
+     *
+     * @param user the user
+     * @return object node
+     */
+    public static ObjectNode toObjectNode(SubscriberUser user) {
+        ObjectNode root = objectNode()
+                .put(ID, user.id())
+                .put(NAME, user.name())
+                .put(MAC, user.mac());
+        // TODO: add profile data
+        return root;
+    }
+
+}
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
new file mode 100644
index 0000000..ea9ec35
--- /dev/null
+++ b/apps/demo/cord-gui/src/main/java/org/onosproject/cord/gui/model/XosFunctionFactory.java
@@ -0,0 +1,86 @@
+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;
+
+/**
+ * Utility factory for operating on XOS functions.
+ */
+public class XosFunctionFactory extends JsonFactory {
+
+    private static final String DESC = "desc";
+    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;
+
+
+    // no instantiation
+    private XosFunctionFactory() {}
+
+    /**
+     * Produces the JSON representation of the given XOS function descriptor.
+     *
+     * @param xfd function descriptor
+     * @return JSON encoding
+     */
+    public static ObjectNode toObjectNode(XosFunctionDescriptor xfd) {
+        ObjectNode root = objectNode()
+                .put(ID, xfd.id())
+                .put(NAME, xfd.displayName())
+                .put(DESC, xfd.description());
+        root.set(PARAMS, paramsForXfd(xfd));
+        return root;
+    }
+
+    private static JsonNode paramsForXfd(XosFunctionDescriptor xfd) {
+        ParamStructFactory psf = PARAM_MAP.get(xfd);
+        if (psf == null) {
+            psf = DEF_PARAMS;
+        }
+        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();
+    static {
+        PARAM_MAP.put(XosFunctionDescriptor.URL_FILTER, new UrlFilterParams());
+    }
+
+    // private parameter structure creator
+    static class ParamStructFactory {
+        ObjectNode params() {
+            return objectNode();
+        }
+    }
+
+    static class UrlFilterParams extends ParamStructFactory {
+        @Override
+        ObjectNode params() {
+            ObjectNode result = objectNode();
+            result.put(LEVEL, DEFAULT_FILTER_LEVEL);
+            ArrayNode levels = arrayNode();
+            for (String lvl: FILTER_LEVELS) {
+                levels.add(lvl);
+            }
+            result.set(LEVELS, levels);
+            return result;
+        }
+    }
+}
diff --git a/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/bundle-0.json b/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/bundle-0.json
index 50caefd..904ad61 100644
--- a/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/bundle-0.json
+++ b/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/bundle-0.json
@@ -6,13 +6,13 @@
       {
         "id": "internet",
         "name": "Internet",
-        "desc": "Basic internet connectivity",
+        "desc": "Basic internet connectivity.",
         "params": {}
       },
       {
         "id": "firewall",
         "name": "Firewall",
-        "desc": "Normal firewall protection",
+        "desc": "Normal firewall protection.",
         "params": {}
       }
     ]
diff --git a/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/bundle-1.json b/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/bundle-1.json
index addd7b8..035f23f 100644
--- a/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/bundle-1.json
+++ b/apps/demo/cord-gui/src/main/resources/org/onosproject/cord/gui/local/bundle-1.json
@@ -6,19 +6,19 @@
       {
         "id": "internet",
         "name": "Internet",
-        "desc": "Basic internet connectivity",
+        "desc": "Basic internet connectivity.",
         "params": {}
       },
       {
         "id": "firewall",
         "name": "Firewall",
-        "desc": "Normal firewall protection",
+        "desc": "Normal firewall protection.",
         "params": {}
       },
       {
         "id": "url_filter",
         "name": "Parental Control",
-        "desc": "Variable levels of URL filtering",
+        "desc": "Variable levels of URL filtering.",
         "params": {
           "level": "PG",
           "levels": [ "PG", "PG-13", "R" ]
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
new file mode 100644
index 0000000..bccd099
--- /dev/null
+++ b/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/CoreModelCacheTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+import org.onosproject.cord.gui.model.BundleFactory;
+import org.onosproject.cord.gui.model.SubscriberUser;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit tests for {@link CordModelCache}.
+ */
+public class CoreModelCacheTest {
+
+    private CordModelCache cache;
+
+    @Before
+    public void setUp() {
+        cache = new CordModelCache();
+    }
+
+    @Test
+    public void basic() {
+        assertEquals("wrong bundle", BundleFactory.BASIC_BUNDLE,
+                     cache.getCurrentBundle().descriptor());
+    }
+
+    @Test
+    public void basicBundleJson() {
+        String json = BundleFactory.toJson(cache.getCurrentBundle());
+        assertTrue("bad basic json", sameJson(BASIC_BUNDLE_JSON, json));
+    }
+
+    @Test
+    public void chooseFamilyBundle() {
+        cache.setCurrentBundle("family");
+        assertEquals("wrong bundle", BundleFactory.FAMILY_BUNDLE,
+                     cache.getCurrentBundle().descriptor());
+    }
+
+    @Test
+    public void familyBundleJson() {
+        cache.setCurrentBundle("family");
+        String json = BundleFactory.toJson(cache.getCurrentBundle());
+        System.out.println(json);
+        assertTrue("bad family json", sameJson(FAMILY_BUNDLE_JSON, json));
+    }
+
+    @Test
+    public void checkUsers() {
+        List<SubscriberUser> users = cache.getUsers();
+        assertEquals("wrong # users", 4, users.size());
+    }
+
+    // =============
+
+    private boolean sameJson(String s1, String s2) {
+        try {
+            JsonNode tree1 = MAPPER.readTree(s1);
+            JsonNode tree2 = MAPPER.readTree(s2);
+            return tree1.equals(tree2);
+        } catch (IOException e) {
+            System.out.println("Exception: " + e);
+        }
+        return false;
+    }
+
+    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" +
+            "    ]\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" +
+            "        }\n" +
+            "      }\n" +
+            "    ]\n" +
+            "  },\n" +
+            "  \"bundles\": [\n" +
+            "    { \"id\": \"basic\", \"name\": \"Basic Bundle\" },\n" +
+            "    { \"id\": \"family\", \"name\": \"Family Bundle\" }\n" +
+            "  ]\n" +
+            "}\n";
+}
diff --git a/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/model/BundleFactoryTest.java b/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/model/BundleFactoryTest.java
index 999ee97..a4d662e 100644
--- a/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/model/BundleFactoryTest.java
+++ b/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/model/BundleFactoryTest.java
@@ -22,7 +22,7 @@
 import java.util.Set;
 
 import static org.junit.Assert.*;
-import static org.onosproject.cord.gui.model.BundleFactory.availableBundles;
+import static org.onosproject.cord.gui.model.BundleFactory.*;
 import static org.onosproject.cord.gui.model.XosFunctionDescriptor.*;
 
 /**
@@ -33,11 +33,13 @@
     @Test
     public void bundleCount() {
         assertEquals("wrong count", 2, availableBundles().size());
+        assertTrue("missing basic", availableBundles().contains(BASIC_BUNDLE));
+        assertTrue("missing family", availableBundles().contains(FAMILY_BUNDLE));
     }
 
     @Test
     public void basicBundle() {
-        BundleDescriptor bundle = availableBundles().get(0);
+        BundleDescriptor bundle = BundleFactory.BASIC_BUNDLE;
         assertEquals("wrong id", "basic", bundle.id());
         assertEquals("wrong id", "Basic Bundle", bundle.displayName());
         Set<XosFunctionDescriptor> funcs = bundle.functions();
@@ -48,7 +50,7 @@
 
     @Test
     public void familyBundle() {
-        BundleDescriptor bundle = availableBundles().get(1);
+        BundleDescriptor bundle = BundleFactory.FAMILY_BUNDLE;
         assertEquals("wrong id", "family", bundle.id());
         assertEquals("wrong id", "Family Bundle", bundle.displayName());
         Set<XosFunctionDescriptor> funcs = bundle.functions();
@@ -57,5 +59,19 @@
         assertTrue("missing url-f", funcs.contains(URL_FILTER));
     }
 
+    @Test
+    public void bundleFromIdBasic() {
+        assertEquals("wrong bundle", BASIC_BUNDLE, bundleFromId("basic"));
+    }
+
+    @Test
+    public void bundleFromIdFamily() {
+        assertEquals("wrong bundle", FAMILY_BUNDLE, bundleFromId("family"));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void bundleFromIdUnknown() {
+        bundleFromId("unknown");
+    }
 }
 
diff --git a/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/model/XosFunctionDescriptorTest.java b/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/model/XosFunctionDescriptorTest.java
index c08d019..fd8cb8b 100644
--- a/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/model/XosFunctionDescriptorTest.java
+++ b/apps/demo/cord-gui/src/test/org/onosproject/cord/gui/model/XosFunctionDescriptorTest.java
@@ -49,7 +49,7 @@
 
     @Test
     public void urlFiltering() {
-        assertEquals("wrong id", "url_filtering", URL_FILTER.id());
+        assertEquals("wrong id", "url_filter", URL_FILTER.id());
         assertEquals("wrong display", "Parental Control", URL_FILTER.displayName());
         assertTrue("wrong desc", URL_FILTER.description().startsWith("Variable"));
     }