Merge "Unit tests for LinkKey and Criteria classes."
diff --git a/apps/calendar/pom.xml b/apps/calendar/pom.xml
index dd73c99..396a4d0 100644
--- a/apps/calendar/pom.xml
+++ b/apps/calendar/pom.xml
@@ -62,6 +62,32 @@
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.core</artifactId>
         </dependency>
+
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onlab-thirdparty</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onlab-misc</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onlab-junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.onlab.onos</groupId>
+      <artifactId>onos-cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.karaf.shell</groupId>
+      <artifactId>org.apache.karaf.shell.console</artifactId>
+    </dependency>
     </dependencies>
 
     <build>
@@ -77,6 +103,7 @@
                             ${project.groupId}.${project.artifactId}
                         </Bundle-SymbolicName>
                         <Import-Package>
+                            org.slf4j,
                             org.osgi.framework,
                             javax.ws.rs,javax.ws.rs.core,
                             com.sun.jersey.api.core,
diff --git a/apps/calendar/src/main/java/org/onlab/onos/calendar/BandwidthCalendarResource.java b/apps/calendar/src/main/java/org/onlab/onos/calendar/BandwidthCalendarResource.java
index cd8cc5c..cfa1a63 100644
--- a/apps/calendar/src/main/java/org/onlab/onos/calendar/BandwidthCalendarResource.java
+++ b/apps/calendar/src/main/java/org/onlab/onos/calendar/BandwidthCalendarResource.java
@@ -16,43 +16,47 @@
 package org.onlab.onos.calendar;
 
 import java.net.URI;
-
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.core.Response;
-
-import org.onlab.onos.core.ApplicationId;
-import org.onlab.onos.core.CoreService;
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.intent.IntentService;
+import org.onlab.rest.BaseResource;
+import javax.ws.rs.POST;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.core.Response;
+import org.onlab.onos.core.ApplicationId;
+import org.onlab.onos.core.CoreService;
 import org.onlab.onos.net.flow.DefaultTrafficSelector;
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
-import org.onlab.onos.net.intent.Intent;
-import org.onlab.onos.net.intent.IntentService;
-import org.onlab.onos.net.intent.PointToPointIntentWithBandwidthConstraint;
-import org.onlab.onos.net.resource.BandwidthResourceRequest;
+import org.onlab.onos.net.intent.PointToPointIntent;
 import org.onlab.packet.Ethernet;
-import org.onlab.rest.BaseResource;
-
 import static org.onlab.onos.net.PortNumber.portNumber;
 import static org.onlab.onos.net.flow.DefaultTrafficTreatment.builder;
 
+import static org.slf4j.LoggerFactory.getLogger;
+import org.slf4j.Logger;
+
 /**
  * Web resource for triggering calendared intents.
  */
-@Path("intent")
+@javax.ws.rs.Path("intent")
 public class BandwidthCalendarResource extends BaseResource {
 
+    private static final Logger log = getLogger(BandwidthCalendarResource.class);
+
+    @javax.ws.rs.Path("/{src}/{dst}/{srcPort}/{dstPort}/{bandwidth}")
     @POST
-    @Path("{src}/{dst}/{srcPort}/{dstPort}/{bandwidth}")
     public Response createIntent(@PathParam("src") String src,
                                  @PathParam("dst") String dst,
                                  @PathParam("srcPort") String srcPort,
                                  @PathParam("dstPort") String dstPort,
                                  @PathParam("bandwidth") String bandwidth) {
-        // TODO: implement calls to intent framework
+
+        log.info("Receiving Create Intent request...");
+        log.info("Path Constraints: Src = {} SrcPort = {} Dest = {} DestPort = {} BW = {}",
+                src, srcPort, dst, dstPort, bandwidth);
+
         IntentService service = get(IntentService.class);
 
         ConnectPoint srcPoint = new ConnectPoint(deviceId(src), portNumber(srcPort));
@@ -61,13 +65,38 @@
         TrafficSelector selector = buildTrafficSelector();
         TrafficTreatment treatment = builder().build();
 
-        Intent intent = new PointToPointIntentWithBandwidthConstraint(
-                appId(), selector, treatment,
-                srcPoint, dstPoint, new BandwidthResourceRequest(Double.parseDouble(bandwidth)));
-        service.submit(intent);
+        PointToPointIntent intentP2P =
+                        new PointToPointIntent(appId(), selector, treatment,
+                                               srcPoint, dstPoint);
+        service.submit(intentP2P);
+        log.info("Submitted Calendar App intent: src = " + src + "dest = " + dst
+                + "srcPort = " + srcPort + "destPort" + dstPort + "intentID = " + intentP2P.id().toString());
+        String reply =  intentP2P.id().toString() + "\n";
 
-        return Response.ok("Yo! We got src=" + srcPoint + "; dst=" + dstPoint +
-                                   "; bw=" + bandwidth + "; intent service " + service).build();
+        return Response.ok(reply).build();
+    }
+
+    @javax.ws.rs.Path("/cancellation/{intentId}")
+    @DELETE
+    public Response withdrawIntent(@PathParam("intentId") String intentId) {
+
+        log.info("Receiving Teardown request...");
+        log.info("Withdraw intentId = {} ", intentId);
+
+        String reply =  "ok\n";
+        return Response.ok(reply).build();
+    }
+
+    @javax.ws.rs.Path("/modification/{intentId}/{bandwidth}")
+    @POST
+    public Response modifyBandwidth(@PathParam("intentId") String intentId,
+                                 @PathParam("bandwidth") String bandwidth) {
+
+        log.info("Receiving Modify request...");
+        log.info("Modify bw for intentId = {} with new bandwidth = {}", intentId, bandwidth);
+
+        String reply =  "ok\n";
+        return Response.ok(reply).build();
     }
 
     private TrafficSelector buildTrafficSelector() {
@@ -86,5 +115,4 @@
     protected ApplicationId appId() {
         return get(CoreService.class).registerApplication("org.onlab.onos.calendar");
     }
-
 }
diff --git a/apps/foo/src/main/java/org/onlab/onos/foo/IOLoopTestClient.java b/apps/foo/src/main/java/org/onlab/onos/foo/IOLoopTestClient.java
index dd72128..7ea0d26 100644
--- a/apps/foo/src/main/java/org/onlab/onos/foo/IOLoopTestClient.java
+++ b/apps/foo/src/main/java/org/onlab/onos/foo/IOLoopTestClient.java
@@ -85,7 +85,7 @@
         System.exit(0);
     }
 
-    /**
+    /*
      * Starts a standalone IO loop test client.
      *
      * @param args command-line arguments
diff --git a/apps/foo/src/main/java/org/onlab/onos/foo/IOLoopTestServer.java b/apps/foo/src/main/java/org/onlab/onos/foo/IOLoopTestServer.java
index a4aed30..7fe743c 100644
--- a/apps/foo/src/main/java/org/onlab/onos/foo/IOLoopTestServer.java
+++ b/apps/foo/src/main/java/org/onlab/onos/foo/IOLoopTestServer.java
@@ -85,7 +85,7 @@
         System.exit(0);
     }
 
-    /**
+    /*
      * Starts a standalone IO loop test server.
      *
      * @param args command-line arguments
diff --git a/apps/foo/src/main/java/org/onlab/onos/foo/TestMessage.java b/apps/foo/src/main/java/org/onlab/onos/foo/TestMessage.java
index 20dfee0..fcfbb07 100644
--- a/apps/foo/src/main/java/org/onlab/onos/foo/TestMessage.java
+++ b/apps/foo/src/main/java/org/onlab/onos/foo/TestMessage.java
@@ -32,6 +32,7 @@
     /**
      * Creates a new message with the specified data.
      *
+     * @param length        message length
      * @param requestorTime requester time
      * @param responderTime responder time
      * @param padding       message padding
diff --git a/apps/oecfg/pom.xml b/apps/oecfg/pom.xml
index 7a84412..d0cd081 100644
--- a/apps/oecfg/pom.xml
+++ b/apps/oecfg/pom.xml
@@ -49,6 +49,7 @@
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-shade-plugin</artifactId>
+                <version>2.3</version>
                 <executions>
                     <execution>
                         <phase>package</phase>
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/HostToInterfaceAdaptor.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/HostToInterfaceAdaptor.java
index f9d6951..604d12d 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/HostToInterfaceAdaptor.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/HostToInterfaceAdaptor.java
@@ -53,11 +53,13 @@
     public Interface getInterface(ConnectPoint connectPoint) {
         checkNotNull(connectPoint);
 
-        PortAddresses portAddresses =
+        Set<PortAddresses> portAddresses =
                 hostService.getAddressBindingsForPort(connectPoint);
 
-        if (!portAddresses.ipAddresses().isEmpty()) {
-            return new Interface(portAddresses);
+        for (PortAddresses addresses : portAddresses) {
+            if (addresses.connectPoint().equals(connectPoint)) {
+                return new Interface(addresses);
+            }
         }
 
         return null;
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
index e4eafb5..f1a14e7 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/Router.java
@@ -143,7 +143,7 @@
     }
 
     /**
-     * Starts the Router.
+     * Starts the router.
      */
     public void start() {
         bgpUpdatesExecutor.execute(new Runnable() {
@@ -161,6 +161,14 @@
         });
     }
 
+    /**
+     * Shuts the router down.
+     */
+    public void shutdown() {
+        bgpUpdatesExecutor.shutdownNow();
+        bgpIntentsSynchronizerExecutor.shutdownNow();
+    }
+
     //@Override TODO hook this up to something
     public void leaderChanged(boolean isLeader) {
         log.debug("Leader changed: {}", isLeader);
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
index 0f6e38a..4abefa7 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/SdnIp.java
@@ -90,6 +90,9 @@
 
     @Deactivate
     protected void deactivate() {
+        bgpSessionManager.shutDown();
+        router.shutdown();
+
         log.info("Stopped");
     }
 
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/config/SdnIpConfigReader.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/config/SdnIpConfigReader.java
index cde65c6..2fcd1fe 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/config/SdnIpConfigReader.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/config/SdnIpConfigReader.java
@@ -16,6 +16,7 @@
 package org.onlab.onos.sdnip.config;
 
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.util.Collections;
 import java.util.Map;
@@ -40,12 +41,8 @@
 
     private static final String DEFAULT_CONFIG_FILE = "config/sdnip.json";
     private String configFileName = DEFAULT_CONFIG_FILE;
-    //private Map<String, Interface> interfaces;
-    // We call the BGP routers in our SDN network the BGP speakers, and call
-    // the BGP routers outside our SDN network the BGP peers.
-    private Map<String, BgpSpeaker> bgpSpeakers;
-    private Map<IpAddress, BgpPeer> bgpPeers;
-    //private InvertedRadixTree<Interface> interfaceRoutes;
+    private Map<String, BgpSpeaker> bgpSpeakers = new ConcurrentHashMap<>();
+    private Map<IpAddress, BgpPeer> bgpPeers = new ConcurrentHashMap<>();
 
     /**
      * Reads the info contained in the configuration file.
@@ -58,78 +55,25 @@
 
         try {
             Configuration config = mapper.readValue(gatewaysFile, Configuration.class);
-            /*interfaces = new ConcurrentHashMap<>();
-            for (Interface intf : config.getInterfaces()) {
-                interfaces.put(intf.getName(), intf);
-            }*/
-            bgpSpeakers = new ConcurrentHashMap<>();
             for (BgpSpeaker speaker : config.getBgpSpeakers()) {
                 bgpSpeakers.put(speaker.name(), speaker);
             }
-            bgpPeers = new ConcurrentHashMap<>();
             for (BgpPeer peer : config.getPeers()) {
                 bgpPeers.put(peer.ipAddress(), peer);
             }
+        } catch (FileNotFoundException e) {
+            log.warn("Configuration file not found: {}", configFileName);
         } catch (IOException e) {
             log.error("Error reading JSON file", e);
-            //throw new ConfigurationRuntimeException("Error in JSON file", e);
         }
-
-        // Populate the interface InvertedRadixTree
-        /*for (Interface intf : interfaces.values()) {
-            Ip4Prefix prefix = intf.getIp4Prefix();
-            String binaryString = RouteEntry.createBinaryString(prefix);
-            interfaceRoutes.put(binaryString, intf);
-        }*/
     }
 
-    /*
-     * To find the Interface which has longest matchable IP prefix (sub-network
-     *  prefix) to next hop IP address.
-     *
-     * @param address the IP address of next hop router
-     * @return the Interface which has longest matchable IP prefix
-     */
-    /*private Interface longestInterfacePrefixMatch(IpAddress address) {
-        Ip4Prefix prefixToSearchFor =
-            new Ip4Prefix(address, (short) Ip4Address.BIT_LENGTH);
-        String binaryString = RouteEntry.createBinaryString(prefixToSearchFor);
-
-        Iterator<Interface> it =
-            interfaceRoutes.getValuesForKeysPrefixing(binaryString).iterator();
-        Interface intf = null;
-        // Find the last prefix, which will be the longest prefix
-        while (it.hasNext()) {
-            intf = it.next();
-        }
-
-        return intf;
-    }*/
-
-    /*@Override
-    public Interface getOutgoingInterface(IpAddress dstIpAddress) {
-        return longestInterfacePrefixMatch(dstIpAddress);
-    }*/
-
     public void init() {
-        //interfaceRoutes = new ConcurrentInvertedRadixTree<>(
-                //new DefaultByteArrayNodeFactory());
-
-        // Reading config values
-        /*String configFilenameParameter = context.getConfigParams(this).get("configfile");
-        if (configFilenameParameter != null) {
-            currentConfigFilename = configFilenameParameter;
-        }*/
         log.debug("Config file set to {}", configFileName);
 
         readConfiguration(configFileName);
     }
 
-    /*@Override
-    public Map<String, Interface> getInterfaces() {
-        return Collections.unmodifiableMap(interfaces);
-    }*/
-
     @Override
     public Map<String, BgpSpeaker> getBgpSpeakers() {
         return Collections.unmodifiableMap(bgpSpeakers);
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/HostToInterfaceAdaptorTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/HostToInterfaceAdaptorTest.java
index 2a65616..9e31389 100644
--- a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/HostToInterfaceAdaptorTest.java
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/HostToInterfaceAdaptorTest.java
@@ -23,6 +23,7 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
+import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 
@@ -63,10 +64,6 @@
     private static final ConnectPoint NON_EXISTENT_CP = new ConnectPoint(
             DeviceId.deviceId("doesnotexist"), PortNumber.portNumber(1));
 
-    private static final PortAddresses DEFAULT_PA = new PortAddresses(
-            NON_EXISTENT_CP, null, null);
-
-
     @Before
     public void setUp() throws Exception {
         hostService = createMock(HostService.class);
@@ -123,7 +120,8 @@
             MacAddress mac) {
         PortAddresses pa = new PortAddresses(cp, ipAddresses, mac);
         portAddresses.add(pa);
-        expect(hostService.getAddressBindingsForPort(cp)).andReturn(pa).anyTimes();
+        expect(hostService.getAddressBindingsForPort(cp)).andReturn(
+                Collections.singleton(pa)).anyTimes();
 
         Interface intf = new Interface(cp, ipAddresses, mac);
         interfaces.put(cp, intf);
@@ -158,7 +156,7 @@
         // Try and get an interface for a connect point with no addresses
         reset(hostService);
         expect(hostService.getAddressBindingsForPort(NON_EXISTENT_CP))
-                .andReturn(DEFAULT_PA).anyTimes();
+                .andReturn(Collections.<PortAddresses>emptySet()).anyTimes();
         replay(hostService);
 
         assertNull(adaptor.getInterface(NON_EXISTENT_CP));
diff --git a/cli/src/main/java/org/onlab/onos/cli/AbstractShellCommand.java b/cli/src/main/java/org/onlab/onos/cli/AbstractShellCommand.java
index 35dc2a2..122245a 100644
--- a/cli/src/main/java/org/onlab/onos/cli/AbstractShellCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/AbstractShellCommand.java
@@ -92,6 +92,7 @@
     /**
      * Produces a JSON object from the specified key/value annotations.
      *
+     * @param mapper ObjectMapper to use while converting to JSON
      * @param annotations key/value annotations
      * @return JSON object
      */
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/AddPointToPointIntentWithBandwidthConstraintCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/AddPointToPointIntentWithBandwidthConstraintCommand.java
index 1207f1a..1590ae7 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/AddPointToPointIntentWithBandwidthConstraintCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/AddPointToPointIntentWithBandwidthConstraintCommand.java
@@ -27,8 +27,7 @@
 import org.onlab.onos.net.flow.TrafficTreatment;
 import org.onlab.onos.net.intent.Intent;
 import org.onlab.onos.net.intent.IntentService;
-import org.onlab.onos.net.intent.PointToPointIntentWithBandwidthConstraint;
-import org.onlab.onos.net.resource.BandwidthResourceRequest;
+import org.onlab.onos.net.intent.PointToPointIntent;
 
 import static org.onlab.onos.net.DeviceId.deviceId;
 import static org.onlab.onos.net.PortNumber.portNumber;
@@ -73,9 +72,10 @@
         TrafficSelector selector = buildTrafficSelector();
         TrafficTreatment treatment = builder().build();
 
-        Intent intent = new PointToPointIntentWithBandwidthConstraint(
+        // FIXME: add bandwitdh constraint
+        Intent intent = new PointToPointIntent(
                 appId(), selector, treatment,
-                ingress, egress, new BandwidthResourceRequest(bandwidth));
+                ingress, egress);
         service.submit(intent);
     }
 
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java
index b61a646..d8ec3c7 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/ConnectivityIntentCommand.java
@@ -44,6 +44,7 @@
     /**
      * Constructs a traffic selector based on the command line arguments
      * presented to the command.
+     * @return traffic selector
      */
     protected TrafficSelector buildTrafficSelector() {
         TrafficSelector.Builder selectorBuilder = DefaultTrafficSelector.builder();
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/FlowsListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/FlowsListCommand.java
index ab79d03..0bc7c61 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/FlowsListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/FlowsListCommand.java
@@ -141,7 +141,8 @@
     /**
      * Returns the list of devices sorted using the device ID URIs.
      *
-     * @param service device service
+     * @param deviceService device service
+     * @param service flow rule service
      * @return sorted device list
      */
     protected Map<Device, List<FlowEntry>> getSortedFlows(DeviceService deviceService,
@@ -175,7 +176,8 @@
      * Prints flows.
      *
      * @param d     the device
-     * @param flows the set of flows for that device.
+     * @param flows the set of flows for that device
+     * @param coreService core system service
      */
     protected void printFlows(Device d, List<FlowEntry> flows,
                               CoreService coreService) {
diff --git a/cli/src/main/java/org/onlab/onos/cli/net/IntentsListCommand.java b/cli/src/main/java/org/onlab/onos/cli/net/IntentsListCommand.java
index f5c65e5..e18cc5e 100644
--- a/cli/src/main/java/org/onlab/onos/cli/net/IntentsListCommand.java
+++ b/cli/src/main/java/org/onlab/onos/cli/net/IntentsListCommand.java
@@ -78,6 +78,9 @@
             if (!ci.treatment().instructions().isEmpty()) {
                 print("    treatment=%s", ci.treatment().instructions());
             }
+            if (ci.constraints() != null && !ci.constraints().isEmpty()) {
+                print("    constraints=%s", ci.constraints());
+            }
         }
 
         if (intent instanceof PointToPointIntent) {
diff --git a/core/api/src/main/java/org/onlab/onos/cluster/DefaultControllerNode.java b/core/api/src/main/java/org/onlab/onos/cluster/DefaultControllerNode.java
index 3aee1a6..4ed972f 100644
--- a/core/api/src/main/java/org/onlab/onos/cluster/DefaultControllerNode.java
+++ b/core/api/src/main/java/org/onlab/onos/cluster/DefaultControllerNode.java
@@ -54,6 +54,7 @@
      *
      * @param id instance identifier
      * @param ip instance IP address
+     * @param tcpPort TCP port
      */
     public DefaultControllerNode(NodeId id, IpAddress ip, int tcpPort) {
         this.id = id;
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultDevice.java b/core/api/src/main/java/org/onlab/onos/net/DefaultDevice.java
index f6ba352..1207427 100644
--- a/core/api/src/main/java/org/onlab/onos/net/DefaultDevice.java
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultDevice.java
@@ -54,6 +54,7 @@
      * @param hwVersion    device HW version
      * @param swVersion    device SW version
      * @param serialNumber device serial number
+     * @param chassisId    chasis id
      * @param annotations optional key/value annotations
      */
     public DefaultDevice(ProviderId providerId, DeviceId id, Type type,
diff --git a/core/api/src/main/java/org/onlab/onos/net/DeviceId.java b/core/api/src/main/java/org/onlab/onos/net/DeviceId.java
index 5dcc076..e1080f2 100644
--- a/core/api/src/main/java/org/onlab/onos/net/DeviceId.java
+++ b/core/api/src/main/java/org/onlab/onos/net/DeviceId.java
@@ -48,6 +48,7 @@
      * Creates a device id using the supplied URI.
      *
      * @param uri device URI
+     * @return DeviceId
      */
     public static DeviceId deviceId(URI uri) {
         return new DeviceId(uri);
@@ -57,6 +58,7 @@
      * Creates a device id using the supplied URI string.
      *
      * @param string device URI string
+     * @return DeviceId
      */
     public static DeviceId deviceId(String string) {
         return deviceId(URI.create(string));
diff --git a/core/api/src/main/java/org/onlab/onos/net/LinkKey.java b/core/api/src/main/java/org/onlab/onos/net/LinkKey.java
index 48df67a..c4e461d 100644
--- a/core/api/src/main/java/org/onlab/onos/net/LinkKey.java
+++ b/core/api/src/main/java/org/onlab/onos/net/LinkKey.java
@@ -84,7 +84,7 @@
 
     @Override
     public int hashCode() {
-        return Objects.hash(src(), dst);
+        return Objects.hash(src, dst);
     }
 
     @Override
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DefaultDeviceDescription.java b/core/api/src/main/java/org/onlab/onos/net/device/DefaultDeviceDescription.java
index 1bbdaf7..e3aa2ed 100644
--- a/core/api/src/main/java/org/onlab/onos/net/device/DefaultDeviceDescription.java
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DefaultDeviceDescription.java
@@ -47,6 +47,7 @@
      * @param hwVersion    device HW version
      * @param swVersion    device SW version
      * @param serialNumber device serial number
+     * @param chassis      chasis id
      * @param annotations  optional key/value annotations map
      */
     public DefaultDeviceDescription(URI uri, Type type, String manufacturer,
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceProviderService.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceProviderService.java
index 6f71495..d156b28 100644
--- a/core/api/src/main/java/org/onlab/onos/net/device/DeviceProviderService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceProviderService.java
@@ -32,6 +32,7 @@
     /**
      * Signals the core that a device has connected or has been detected somehow.
      *
+     * @param deviceId device identifier
      * @param deviceDescription information about network device
      */
     void deviceConnected(DeviceId deviceId, DeviceDescription deviceDescription);
@@ -65,7 +66,7 @@
      *
      * @param deviceId identity of the device
      * @param requested mastership role that was requested by the node
-     * @param replied mastership role the switch accepted
+     * @param response mastership role the switch accepted
      */
     void receivedRoleReply(DeviceId deviceId, MastershipRole requested, MastershipRole response);
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DeviceStore.java b/core/api/src/main/java/org/onlab/onos/net/device/DeviceStore.java
index f3f124a..bc0b5c1 100644
--- a/core/api/src/main/java/org/onlab/onos/net/device/DeviceStore.java
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DeviceStore.java
@@ -125,6 +125,7 @@
      * Administratively removes the specified device from the store.
      *
      * @param deviceId device to be removed
+     * @return null if no such device, or was forwarded to remove master
      */
     DeviceEvent removeDevice(DeviceId deviceId);
 }
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficSelector.java b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficSelector.java
index 960eb02..413473f 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficSelector.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficSelector.java
@@ -99,6 +99,7 @@
      * Returns a new traffic selector builder primed to produce entities
      * patterned after the supplied selector.
      *
+     * @param selector base selector
      * @return traffic selector builder
      */
     public static TrafficSelector.Builder builder(TrafficSelector selector) {
@@ -188,7 +189,7 @@
         }
 
         @Override
-        public Builder matchOpticalSignalType(Byte signalType) {
+        public Builder matchOpticalSignalType(Short signalType) {
             return add(Criteria.matchOpticalSignalType(signalType));
 
         }
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficTreatment.java b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficTreatment.java
index c0b3986..a63ae13 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficTreatment.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/DefaultTrafficTreatment.java
@@ -63,6 +63,7 @@
      * Returns a new traffic treatment builder primed to produce entities
      * patterned after the supplied treatment.
      *
+     * @param treatment base treatment
      * @return traffic treatment builder
      */
     public static TrafficTreatment.Builder builder(TrafficTreatment treatment) {
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchEvent.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchEvent.java
index d5c762d..1dbf8bd 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchEvent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchEvent.java
@@ -27,11 +27,14 @@
      */
     public enum Type {
 
+        // Request has been forwarded to MASTER Node
         /**
          * Signifies that a batch operation has been initiated.
          */
         BATCH_OPERATION_REQUESTED,
 
+        // MASTER Node has pushed the batch down to the Device
+        // (e.g., Received barrier reply)
         /**
          * Signifies that a batch operation has completed.
          */
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchRequest.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchRequest.java
index 4a2bcf9..f75c663 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchRequest.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleBatchRequest.java
@@ -25,29 +25,29 @@
 public class FlowRuleBatchRequest {
 
     private final int batchId;
-    private final List<FlowEntry> toAdd;
-    private final List<FlowEntry> toRemove;
+    private final List<FlowRule> toAdd;
+    private final List<FlowRule> toRemove;
 
-    public FlowRuleBatchRequest(int batchId, List<? extends FlowEntry> toAdd, List<? extends FlowEntry> toRemove) {
+    public FlowRuleBatchRequest(int batchId, List<? extends FlowRule> toAdd, List<? extends FlowRule> toRemove) {
         this.batchId = batchId;
         this.toAdd = Collections.unmodifiableList(toAdd);
         this.toRemove = Collections.unmodifiableList(toRemove);
     }
 
-    public List<FlowEntry> toAdd() {
+    public List<FlowRule> toAdd() {
         return toAdd;
     }
 
-    public List<FlowEntry> toRemove() {
+    public List<FlowRule> toRemove() {
         return toRemove;
     }
 
     public FlowRuleBatchOperation asBatchOperation() {
         List<FlowRuleBatchEntry> entries = Lists.newArrayList();
-        for (FlowEntry e : toAdd) {
+        for (FlowRule e : toAdd) {
             entries.add(new FlowRuleBatchEntry(FlowRuleOperation.ADD, e));
         }
-        for (FlowEntry e : toRemove) {
+        for (FlowRule e : toRemove) {
             entries.add(new FlowRuleBatchEntry(FlowRuleOperation.REMOVE, e));
         }
         return new FlowRuleBatchOperation(entries);
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProvider.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProvider.java
index 05ecd66..de2c7fd 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProvider.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProvider.java
@@ -48,6 +48,7 @@
     /**
      * Removes rules by their id.
      * @param id the id to remove
+     * @param flowRules one or more flow rules
      */
     void removeRulesById(ApplicationId id, FlowRule... flowRules);
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderService.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderService.java
index 2f2dd5e..4c0d98d 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleProviderService.java
@@ -35,6 +35,7 @@
      * Pushes the collection of flow entries currently applied on the given
      * device.
      *
+     * @param deviceId device identifier
      * @param flowEntries collection of flow rules
      */
     void pushFlowMetrics(DeviceId deviceId, Iterable<FlowEntry> flowEntries);
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleService.java b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleService.java
index 789b6bf..f40a0f1 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/FlowRuleService.java
@@ -86,6 +86,7 @@
     /**
      * Applies a batch operation of FlowRules.
      *
+     * @param batch batch operation to apply
      * @return future indicating the state of the batch operation
      */
     Future<CompletedBatchOperation> applyBatch(FlowRuleBatchOperation batch);
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/TrafficSelector.java b/core/api/src/main/java/org/onlab/onos/net/flow/TrafficSelector.java
index aaf0ece..e0c8c78 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/TrafficSelector.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/TrafficSelector.java
@@ -137,17 +137,17 @@
 
         /**
          * Matches an optical signal ID or lambda.
-         * @param lambda
+         * @param lambda lamda
          * @return a selection builder
          */
         public Builder matchLambda(Short lambda);
 
         /**
          * Matches an optical Signal Type.
-         * @param signalType
+         * @param signalType signalType
          * @return a selection builder
          */
-        public Builder matchOpticalSignalType(Byte signalType);
+        public Builder matchOpticalSignalType(Short signalType);
 
         /**
          * Builds an immutable traffic selector.
diff --git a/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criteria.java b/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criteria.java
index aba5680..bac1bab 100644
--- a/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criteria.java
+++ b/core/api/src/main/java/org/onlab/onos/net/flow/criteria/Criteria.java
@@ -131,7 +131,7 @@
     /**
      * Creates a match on TCP source port field using the specified value.
      *
-     * @param tcpPort
+     * @param tcpPort TCP source port
      * @return match criterion
      */
     public static Criterion matchTcpSrc(Short tcpPort) {
@@ -141,7 +141,7 @@
     /**
      * Creates a match on TCP destination port field using the specified value.
      *
-     * @param tcpPort
+     * @param tcpPort TCP destination port
      * @return match criterion
      */
     public static Criterion matchTcpDst(Short tcpPort) {
@@ -151,7 +151,7 @@
     /**
      * Creates a match on lambda field using the specified value.
      *
-     * @param lambda
+     * @param lambda lamda to match on
      * @return match criterion
      */
     public static Criterion matchLambda(Short lambda) {
@@ -161,11 +161,11 @@
     /**
      * Creates a match on lambda field using the specified value.
      *
-     * @param lambda
+     * @param sigType signame type
      * @return match criterion
      */
-    public static Criterion matchOpticalSignalType(Byte lambda) {
-        return new OpticalSignalTypeCriterion(lambda, Type.OCH_SIGTYPE);
+    public static Criterion matchOpticalSignalType(Short sigType) {
+        return new OpticalSignalTypeCriterion(sigType, Type.OCH_SIGTYPE);
     }
 
 
@@ -587,10 +587,10 @@
 
     public static final class OpticalSignalTypeCriterion implements Criterion {
 
-        private final byte signalType;
+        private final Short signalType;
         private final Type type;
 
-        public OpticalSignalTypeCriterion(byte signalType, Type type) {
+        public OpticalSignalTypeCriterion(Short signalType, Type type) {
             this.signalType = signalType;
             this.type = type;
         }
@@ -600,7 +600,7 @@
             return this.type;
         }
 
-        public Byte signalType() {
+        public Short signalType() {
             return this.signalType;
         }
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostAdminService.java b/core/api/src/main/java/org/onlab/onos/net/host/HostAdminService.java
index f421fd8..c51a847 100644
--- a/core/api/src/main/java/org/onlab/onos/net/host/HostAdminService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostAdminService.java
@@ -34,11 +34,7 @@
      * Binds IP and MAC addresses to the given connection point.
      * <p>
      * The addresses are added to the set of addresses already bound to the
-     * connection point. If any of the fields in addresses is null, no change
-     * is made to the corresponding addresses in the store.
-     * {@link #unbindAddressesFromPort(PortAddresses)} must be use to unbind
-     * addresses that have previously been bound.
-     * </p>
+     * connection point.
      *
      * @param addresses address object containing addresses to add and the port
      * to add them to
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostService.java b/core/api/src/main/java/org/onlab/onos/net/host/HostService.java
index aa31459..7f7be50 100644
--- a/core/api/src/main/java/org/onlab/onos/net/host/HostService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostService.java
@@ -135,7 +135,7 @@
      * @param connectPoint the connection point to retrieve address bindings for
      * @return addresses bound to the port
      */
-    PortAddresses getAddressBindingsForPort(ConnectPoint connectPoint);
+    Set<PortAddresses> getAddressBindingsForPort(ConnectPoint connectPoint);
 
     /**
      * Adds the specified host listener.
diff --git a/core/api/src/main/java/org/onlab/onos/net/host/HostStore.java b/core/api/src/main/java/org/onlab/onos/net/host/HostStore.java
index a6bf96d..0316dcf 100644
--- a/core/api/src/main/java/org/onlab/onos/net/host/HostStore.java
+++ b/core/api/src/main/java/org/onlab/onos/net/host/HostStore.java
@@ -15,6 +15,8 @@
  */
 package org.onlab.onos.net.host;
 
+import java.util.Set;
+
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.Host;
@@ -25,8 +27,6 @@
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
 
-import java.util.Set;
-
 /**
  * Manages inventory of end-station hosts; not intended for direct use.
  */
@@ -153,5 +153,5 @@
      *                     for
      * @return address information for the connection point
      */
-    PortAddresses getAddressBindingsForPort(ConnectPoint connectPoint);
+    Set<PortAddresses> getAddressBindingsForPort(ConnectPoint connectPoint);
 }
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java
index 65d48d5..2269aa0 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java
@@ -23,6 +23,7 @@
 import org.onlab.onos.net.flow.TrafficTreatment;
 
 import java.util.Collection;
+import java.util.List;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
@@ -40,10 +41,14 @@
 
     private final TrafficSelector selector;
     private final TrafficTreatment treatment;
+    private final List<Constraint> constraints;
 
     /**
      * Creates a connectivity intent that matches on the specified selector
      * and applies the specified treatment.
+     * <p>
+     * Path will be chosen without any constraints.
+     * </p>
      *
      * @param id        intent identifier
      * @param appId     application identifier
@@ -56,9 +61,33 @@
                                  Collection<NetworkResource> resources,
                                  TrafficSelector selector,
                                  TrafficTreatment treatment) {
+        this(id, appId, resources, selector, treatment, null);
+    }
+
+    /**
+     * Creates a connectivity intent that matches on the specified selector
+     * and applies the specified treatment.
+     * <p>
+     * Path will be optimized based on the first constraint if one is given.
+     * </p>
+     *
+     * @param id          intent identifier
+     * @param appId       application identifier
+     * @param resources   required network resources (optional)
+     * @param selector    traffic selector
+     * @param treatment   treatment
+     * @param constraints optional prioritized list of constraints
+     * @throws NullPointerException if the selector or treatement is null
+     */
+    protected ConnectivityIntent(IntentId id, ApplicationId appId,
+                                 Collection<NetworkResource> resources,
+                                 TrafficSelector selector,
+                                 TrafficTreatment treatment,
+                                 List<Constraint> constraints) {
         super(id, appId, resources);
         this.selector = checkNotNull(selector);
         this.treatment = checkNotNull(treatment);
+        this.constraints = constraints;
     }
 
     /**
@@ -68,6 +97,7 @@
         super();
         this.selector = null;
         this.treatment = null;
+        this.constraints = null;
     }
 
     /**
@@ -89,13 +119,22 @@
     }
 
     /**
+     * Returns the set of connectivity constraints.
+     *
+     * @return list of intent constraints
+     */
+    public List<Constraint> constraints() {
+        return constraints;
+    }
+
+    /**
      * Produces a collection of network resources from the given links.
      *
      * @param links collection of links
      * @return collection of link resources
      */
     protected static Collection<NetworkResource> resources(Collection<Link> links) {
-        return ImmutableSet.<NetworkResource>copyOf(links);
+        return ImmutableSet.copyOf(links);
     }
 
 }
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/Constraint.java b/core/api/src/main/java/org/onlab/onos/net/intent/Constraint.java
new file mode 100644
index 0000000..9004aa6
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/Constraint.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2014 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.onlab.onos.net.intent;
+
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.Path;
+import org.onlab.onos.net.resource.LinkResourceService;
+
+/**
+ * Representation of a connectivity constraint capable of evaluating a link
+ * and determining the cost of traversing that link in the context of this
+ * constraint.
+ */
+public interface Constraint {
+
+    // TODO: Consider separating cost vs viability.
+
+    /**
+     * Evaluates the specified link and provides the cost for its traversal.
+     *
+     * @param link            link to be evaluated
+     * @param resourceService resource service for validating availability of
+     *                        link resources
+     * @return cost of link traversal
+     */
+    double cost(Link link, LinkResourceService resourceService);
+
+    /**
+     * Validates that the specified path satisfies the constraint.
+     *
+     * @param path            path to be validated
+     * @param resourceService resource service for validating availability of
+     *                        link resources
+     * @return cost of link traversal
+     */
+    boolean validate(Path path, LinkResourceService resourceService);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
index 376a122..3fad93d 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/HostToHostIntent.java
@@ -21,6 +21,8 @@
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
 
+import java.util.List;
+
 import static com.google.common.base.Preconditions.checkNotNull;
 
 /**
@@ -44,11 +46,30 @@
     public HostToHostIntent(ApplicationId appId, HostId one, HostId two,
                             TrafficSelector selector,
                             TrafficTreatment treatment) {
+        this(appId, one, two, selector, treatment, null);
+    }
+
+    /**
+     * Creates a new host-to-host intent with the supplied host pair.
+     *
+     * @param appId       application identifier
+     * @param one         first host
+     * @param two         second host
+     * @param selector    action
+     * @param treatment   ingress port
+     * @param constraints optional prioritized list of path selection constraints
+     * @throws NullPointerException if {@code one} or {@code two} is null.
+     */
+    public HostToHostIntent(ApplicationId appId, HostId one, HostId two,
+                            TrafficSelector selector,
+                            TrafficTreatment treatment,
+                            List<Constraint> constraints) {
         super(id(HostToHostIntent.class, min(one, two), max(one, two),
-                 selector, treatment),
-              appId, null, selector, treatment);
+                 selector, treatment, constraints),
+              appId, null, selector, treatment, constraints);
         this.one = checkNotNull(one);
         this.two = checkNotNull(two);
+
     }
 
     private static HostId min(HostId one, HostId two) {
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java b/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java
index 885b851..657a3e4 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java
@@ -91,6 +91,7 @@
      * Produces an intent identifier backed by hash-like fingerprint for the
      * specified class of intent and its constituent fields.
      *
+     * @param intentClass Class of the intent
      * @param fields intent fields
      * @return intent identifier
      */
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java
index 8c75397..c4c2752 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java
@@ -51,6 +51,15 @@
         this.fingerprint = fingerprint;
     }
 
+    /**
+     * Returns the backing fingerprint.
+     *
+     * @return the fingerprint
+     */
+    public long fingerprint() {
+        return fingerprint;
+    }
+
     @Override
     public int hashCode() {
         return (int) (fingerprint ^ (fingerprint >>> 32));
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java
index 7566210..93d8a40 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java
@@ -27,6 +27,7 @@
      * Installs the specified intent to the environment.
      *
      * @param intent intent to be installed
+     * @return FlowRule operations to install
      * @throws IntentException if issues are encountered while installing the intent
      */
     List<FlowRuleBatchOperation> install(T intent);
@@ -35,6 +36,7 @@
      * Uninstalls the specified intent from the environment.
      *
      * @param intent intent to be uninstalled
+     * @return FlowRule operations to uninstall
      * @throws IntentException if issues are encountered while uninstalling the intent
      */
     List<FlowRuleBatchOperation> uninstall(T intent);
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java
index 9cffb11..090af63 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java
@@ -59,6 +59,7 @@
      * affected at later time.
      * </p>
      * @param operations batch of intent operations
+     * @return Future to get execution result
      */
     Future<IntentOperations> execute(IntentOperations operations);
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java
index 0789db0..9189bae 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java
@@ -20,7 +20,6 @@
 import org.onlab.onos.net.Path;
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
-import org.onlab.onos.net.resource.LinkResourceRequest;
 
 /**
  * Abstraction of explicitly path specified connectivity intent.
@@ -28,7 +27,6 @@
 public class PathIntent extends ConnectivityIntent {
 
     private final Path path;
-    private final LinkResourceRequest[] resourceRequests;
 
     /**
      * Creates a new point-to-point intent with the supplied ingress/egress
@@ -41,11 +39,10 @@
      * @throws NullPointerException {@code path} is null
      */
     public PathIntent(ApplicationId appId, TrafficSelector selector,
-                      TrafficTreatment treatment, Path path, LinkResourceRequest[] resourceRequests) {
+                      TrafficTreatment treatment, Path path) {
         super(id(PathIntent.class, selector, treatment, path), appId,
               resources(path.links()), selector, treatment);
         this.path = path;
-        this.resourceRequests = resourceRequests;
     }
 
     /**
@@ -54,7 +51,6 @@
     protected PathIntent() {
         super();
         this.path = null;
-        this.resourceRequests = new LinkResourceRequest[0];
     }
 
     /**
@@ -71,9 +67,6 @@
         return true;
     }
 
-    public LinkResourceRequest[] resourceRequests() {
-        return resourceRequests;
-    }
 
     @Override
     public String toString() {
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java
index 61a1a56..e480ee2 100644
--- a/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java
@@ -16,10 +16,15 @@
 package org.onlab.onos.net.intent;
 
 import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableList;
 import org.onlab.onos.core.ApplicationId;
 import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.Link;
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.intent.constraint.LinkTypeConstraint;
+
+import java.util.List;
 
 import static com.google.common.base.Preconditions.checkNotNull;
 
@@ -33,7 +38,7 @@
 
     /**
      * Creates a new point-to-point intent with the supplied ingress/egress
-     * ports.
+     * ports and with built-in link type constraint to avoid optical links.
      *
      * @param appId        application identifier
      * @param selector     traffic selector
@@ -46,8 +51,30 @@
                               TrafficTreatment treatment,
                               ConnectPoint ingressPoint,
                               ConnectPoint egressPoint) {
-        super(id(PointToPointIntent.class, selector, treatment, ingressPoint, egressPoint),
-              appId, null, selector, treatment);
+        this(appId, selector, treatment, ingressPoint, egressPoint,
+             ImmutableList.of(new LinkTypeConstraint(false, Link.Type.OPTICAL)));
+    }
+
+    /**
+     * Creates a new point-to-point intent with the supplied ingress/egress
+     * ports and constraints.
+     *
+     * @param appId        application identifier
+     * @param selector     traffic selector
+     * @param treatment    treatment
+     * @param ingressPoint ingress port
+     * @param egressPoint  egress port
+     * @param constraints  optional list of constraints
+     * @throws NullPointerException if {@code ingressPoint} or {@code egressPoints} is null.
+     */
+    public PointToPointIntent(ApplicationId appId, TrafficSelector selector,
+                              TrafficTreatment treatment,
+                              ConnectPoint ingressPoint,
+                              ConnectPoint egressPoint,
+                              List<Constraint> constraints) {
+        super(id(PointToPointIntent.class, selector, treatment,
+                 ingressPoint, egressPoint, constraints),
+              appId, null, selector, treatment, constraints);
         this.ingressPoint = checkNotNull(ingressPoint);
         this.egressPoint = checkNotNull(egressPoint);
     }
@@ -89,6 +116,7 @@
                 .add("treatment", treatment())
                 .add("ingress", ingressPoint)
                 .add("egress", egressPoint)
+                .add("constraints", constraints())
                 .toString();
     }
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntentWithBandwidthConstraint.java b/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntentWithBandwidthConstraint.java
deleted file mode 100644
index d5c4d57..0000000
--- a/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntentWithBandwidthConstraint.java
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.onlab.onos.net.intent;
-
-import org.onlab.onos.core.ApplicationId;
-import org.onlab.onos.net.ConnectPoint;
-import org.onlab.onos.net.flow.TrafficSelector;
-import org.onlab.onos.net.flow.TrafficTreatment;
-import org.onlab.onos.net.resource.BandwidthResourceRequest;
-
-import com.google.common.base.MoreObjects;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-
-/**
- * Abstraction of point-to-point connectivity.
- */
-public class PointToPointIntentWithBandwidthConstraint extends ConnectivityIntent {
-
-    private final ConnectPoint ingressPoint;
-    private final ConnectPoint egressPoint;
-    private final BandwidthResourceRequest bandwidthResourceRequest;
-
-    /**
-     * Creates a new point-to-point intent with the supplied ingress/egress
-     * ports.
-     *
-     * @param appId        application identifier
-     * @param selector     traffic selector
-     * @param treatment    treatment
-     * @param ingressPoint ingress port
-     * @param egressPoint  egress port
-     * @throws NullPointerException if {@code ingressPoint} or {@code egressPoints} is null.
-     */
-    public PointToPointIntentWithBandwidthConstraint(ApplicationId appId, TrafficSelector selector,
-                                                     TrafficTreatment treatment,
-                                                     ConnectPoint ingressPoint,
-                                                     ConnectPoint egressPoint,
-                                                     BandwidthResourceRequest bandwidthResourceRequest) {
-        super(id(PointToPointIntentWithBandwidthConstraint.class, selector,
-                 treatment, ingressPoint, egressPoint, bandwidthResourceRequest.bandwidth()),
-              appId, null, selector, treatment);
-        this.ingressPoint = checkNotNull(ingressPoint);
-        this.egressPoint = checkNotNull(egressPoint);
-        this.bandwidthResourceRequest = bandwidthResourceRequest;
-    }
-
-    /**
-     * Constructor for serializer.
-     */
-    protected PointToPointIntentWithBandwidthConstraint() {
-        super();
-        this.ingressPoint = null;
-        this.egressPoint = null;
-        bandwidthResourceRequest = new BandwidthResourceRequest(0.0);
-    }
-
-    /**
-     * Returns the port on which the ingress traffic should be connected to
-     * the egress.
-     *
-     * @return ingress port
-     */
-    public ConnectPoint ingressPoint() {
-        return ingressPoint;
-    }
-
-    /**
-     * Returns the port on which the traffic should egress.
-     *
-     * @return egress port
-     */
-    public ConnectPoint egressPoint() {
-        return egressPoint;
-    }
-
-    public BandwidthResourceRequest bandwidthRequest() {
-        return this.bandwidthResourceRequest;
-    }
-
-    @Override
-    public String toString() {
-        return MoreObjects.toStringHelper(getClass())
-                .add("id", id())
-                .add("appId", appId())
-                .add("selector", selector())
-                .add("treatment", treatment())
-                .add("ingress", ingressPoint)
-                .add("egress", egressPoint)
-                .add("bandwidth", bandwidthResourceRequest.bandwidth().toString())
-                .toString();
-    }
-
-}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/BandwidthConstraint.java b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/BandwidthConstraint.java
new file mode 100644
index 0000000..fc08214
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/BandwidthConstraint.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2014 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.onlab.onos.net.intent.constraint;
+
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.resource.Bandwidth;
+import org.onlab.onos.net.resource.BandwidthResourceRequest;
+import org.onlab.onos.net.resource.LinkResourceService;
+import org.onlab.onos.net.resource.ResourceRequest;
+import org.onlab.onos.net.resource.ResourceType;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Constraint that evaluates links based on available bandwidths.
+ */
+public class BandwidthConstraint extends BooleanConstraint {
+
+    private final Bandwidth bandwidth;
+
+    /**
+     * Creates a new bandwidth constraint.
+     *
+     * @param bandwidth required bandwidth
+     */
+    public BandwidthConstraint(Bandwidth bandwidth) {
+        this.bandwidth = checkNotNull(bandwidth, "Bandwidth cannot be null");
+    }
+
+    // Constructor for serialization
+    private BandwidthConstraint() {
+        this.bandwidth = null;
+    }
+
+    @Override
+    public boolean isValid(Link link, LinkResourceService resourceService) {
+        for (ResourceRequest request : resourceService.getAvailableResources(link)) {
+            if (request.type() == ResourceType.BANDWIDTH) {
+                BandwidthResourceRequest brr = (BandwidthResourceRequest) request;
+                if (brr.bandwidth().toDouble() >= bandwidth.toDouble()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns the bandwidth required by this constraint.
+     *
+     * @return required bandwidth
+     */
+    public Bandwidth bandwidth() {
+        return bandwidth;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(bandwidth);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        final BandwidthConstraint other = (BandwidthConstraint) obj;
+        return Objects.equals(this.bandwidth, other.bandwidth);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this).add("bandwidth", bandwidth).toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/BooleanConstraint.java b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/BooleanConstraint.java
new file mode 100644
index 0000000..97c5695
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/BooleanConstraint.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2014 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.onlab.onos.net.intent.constraint;
+
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.Path;
+import org.onlab.onos.net.intent.Constraint;
+import org.onlab.onos.net.resource.LinkResourceService;
+
+/**
+ * Abstract base class for various constraints that evaluate link viability
+ * in a yes/no fashion.
+ */
+public abstract class BooleanConstraint implements Constraint {
+
+    /**
+     * Returns true if the specified link satisfies the constraint.
+     *
+     * @param link            link to be validated
+     * @param resourceService resource service for checking available link resources
+     * @return true if link is viable
+     */
+    public abstract boolean isValid(Link link, LinkResourceService resourceService);
+
+    /**
+     * {@inheritDoc}
+     *
+     * Negative return value means the specified link does not satisfy this constraint.
+     *
+     * @param link {@inheritDoc}
+     * @param resourceService {@inheritDoc}
+     * @return {@inheritDoc}
+     */
+    @Override
+    public double cost(Link link, LinkResourceService resourceService) {
+        return isValid(link, resourceService) ? +1 : -1;
+    }
+
+    @Override
+    public boolean validate(Path path, LinkResourceService resourceService) {
+        for (Link link : path.links()) {
+            if (isValid(link, resourceService)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/LambdaConstraint.java b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/LambdaConstraint.java
new file mode 100644
index 0000000..ab88c92
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/LambdaConstraint.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2014 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.onlab.onos.net.intent.constraint;
+
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.resource.Lambda;
+import org.onlab.onos.net.resource.LinkResourceService;
+import org.onlab.onos.net.resource.ResourceRequest;
+import org.onlab.onos.net.resource.ResourceType;
+
+import java.util.Objects;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+/**
+ * Constraint that evaluates links based on available lambda.
+ */
+public class LambdaConstraint extends BooleanConstraint {
+
+    private final Lambda lambda;
+
+    /**
+     * Creates a new optical lambda constraint.
+     *
+     * @param lambda optional lambda to indicate a specific lambda
+     */
+    public LambdaConstraint(Lambda lambda) {
+        this.lambda = lambda;
+    }
+
+    // Constructor for serialization
+    private LambdaConstraint() {
+        this.lambda = null;
+    }
+
+    @Override
+    public boolean isValid(Link link, LinkResourceService resourceService) {
+        for (ResourceRequest request : resourceService.getAvailableResources(link)) {
+            if (request.type() == ResourceType.LAMBDA) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns the lambda required by this constraint.
+     *
+     * @return required lambda
+     */
+    public Lambda lambda() {
+        return lambda;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(lambda);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        final LambdaConstraint other = (LambdaConstraint) obj;
+        return Objects.equals(this.lambda, other.lambda);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this).add("lambda", lambda).toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/constraint/LinkTypeConstraint.java b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/LinkTypeConstraint.java
new file mode 100644
index 0000000..7d44b4d
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/LinkTypeConstraint.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2014 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.onlab.onos.net.intent.constraint;
+
+import com.google.common.collect.ImmutableSet;
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.resource.LinkResourceService;
+
+import java.util.Objects;
+import java.util.Set;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Constraint that evaluates links based on their type.
+ */
+public class LinkTypeConstraint extends BooleanConstraint {
+
+    private final Set<Link.Type> types;
+    private final boolean isInclusive;
+
+    /**
+     * Creates a new constraint for requesting connectivity using or avoiding
+     * the specified link types.
+     *
+     * @param inclusive indicates whether the given link types are to be
+     *                  permitted or avoided
+     * @param types     link types
+     */
+    public LinkTypeConstraint(boolean inclusive, Link.Type... types) {
+        checkNotNull(types, "Link types cannot be null");
+        checkArgument(types.length > 0, "There must be more than one type");
+        this.types = ImmutableSet.copyOf(types);
+        this.isInclusive = inclusive;
+    }
+
+    // Constructor for serialization
+    private LinkTypeConstraint() {
+        this.types = null;
+        this.isInclusive = false;
+    }
+
+    @Override
+    public boolean isValid(Link link, LinkResourceService resourceService) {
+        boolean contains = types.contains(link.type());
+        return isInclusive ? contains : !contains;
+    }
+
+    /**
+     * Returns the set of link types.
+     *
+     * @return set of link types
+     */
+    public Set<Link.Type> types() {
+        return types;
+    }
+
+    /**
+     * Indicates if the constraint is inclusive or exclusive.
+     *
+     * @return true if inclusive
+     */
+    public boolean isInclusive() {
+        return isInclusive;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(types, isInclusive);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        final LinkTypeConstraint other = (LinkTypeConstraint) obj;
+        return Objects.equals(this.types, other.types) && Objects.equals(this.isInclusive, other.isInclusive);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("inclusive", isInclusive)
+                .add("types", types)
+                .toString();
+    }
+}
diff --git a/web/gui/src/main/webapp/module-template.js b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/package-info.java
similarity index 67%
copy from web/gui/src/main/webapp/module-template.js
copy to core/api/src/main/java/org/onlab/onos/net/intent/constraint/package-info.java
index 3de7d79..762d6d1 100644
--- a/web/gui/src/main/webapp/module-template.js
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/constraint/package-info.java
@@ -14,23 +14,7 @@
  * limitations under the License.
  */
 
-/*
- Module template file.
-
- @author Simon Hunt
+/**
+ * Definitions of constraints used to refine intent specifications.
  */
-
-(function (onos) {
-    'use strict';
-
-    var api = onos.api;
-
-    // == define your functions here.....
-
-
-    // == register views here, with links to lifecycle callbacks
-
-//    api.addView('view-id', {/* callbacks */});
-
-
-}(ONOS));
+package org.onlab.onos.net.intent.constraint;
diff --git a/core/api/src/main/java/org/onlab/onos/net/resource/Bandwidth.java b/core/api/src/main/java/org/onlab/onos/net/resource/Bandwidth.java
index 9dddb86..fcb3cd8 100644
--- a/core/api/src/main/java/org/onlab/onos/net/resource/Bandwidth.java
+++ b/core/api/src/main/java/org/onlab/onos/net/resource/Bandwidth.java
@@ -33,6 +33,11 @@
         this.bandwidth = bandwidth;
     }
 
+    // Constructor for serialization
+    private Bandwidth() {
+        this.bandwidth = 0;
+    }
+
     /**
      * Creates a new instance with given bandwidth.
      *
diff --git a/core/api/src/main/java/org/onlab/onos/net/resource/DefaultLinkResourceRequest.java b/core/api/src/main/java/org/onlab/onos/net/resource/DefaultLinkResourceRequest.java
index 73b48df6..c3f5c28 100644
--- a/core/api/src/main/java/org/onlab/onos/net/resource/DefaultLinkResourceRequest.java
+++ b/core/api/src/main/java/org/onlab/onos/net/resource/DefaultLinkResourceRequest.java
@@ -20,9 +20,12 @@
 import java.util.Set;
 
 import org.onlab.onos.net.Link;
+import org.onlab.onos.net.intent.Constraint;
 import org.onlab.onos.net.intent.IntentId;
 
 import com.google.common.collect.ImmutableSet;
+import org.onlab.onos.net.intent.constraint.BandwidthConstraint;
+import org.onlab.onos.net.intent.constraint.LambdaConstraint;
 
 /**
  * Implementation of {@link LinkResourceRequest}.
@@ -125,6 +128,18 @@
             return this;
         }
 
+        @Override
+        public LinkResourceRequest.Builder addConstraint(Constraint constraint) {
+            if (constraint instanceof LambdaConstraint) {
+                return addLambdaRequest();
+            } else if (constraint instanceof BandwidthConstraint) {
+                BandwidthConstraint bw = (BandwidthConstraint) constraint;
+                return addBandwidthRequest(bw.bandwidth().toDouble());
+            }
+            return this;
+        }
+
+
         /**
          * Returns link resource request.
          *
diff --git a/core/api/src/main/java/org/onlab/onos/net/resource/Lambda.java b/core/api/src/main/java/org/onlab/onos/net/resource/Lambda.java
index e09d915..d179d64 100644
--- a/core/api/src/main/java/org/onlab/onos/net/resource/Lambda.java
+++ b/core/api/src/main/java/org/onlab/onos/net/resource/Lambda.java
@@ -33,6 +33,11 @@
         this.lambda = lambda;
     }
 
+    // Constructor for serialization
+    private Lambda() {
+        this.lambda = 0;
+    }
+
     /**
      * Creates a new instance with given lambda.
      *
diff --git a/core/api/src/main/java/org/onlab/onos/net/resource/LinkResourceRequest.java b/core/api/src/main/java/org/onlab/onos/net/resource/LinkResourceRequest.java
index a511778..23a0c30 100644
--- a/core/api/src/main/java/org/onlab/onos/net/resource/LinkResourceRequest.java
+++ b/core/api/src/main/java/org/onlab/onos/net/resource/LinkResourceRequest.java
@@ -19,6 +19,7 @@
 import java.util.Set;
 
 import org.onlab.onos.net.Link;
+import org.onlab.onos.net.intent.Constraint;
 import org.onlab.onos.net.intent.IntentId;
 
 /**
@@ -67,6 +68,14 @@
         public Builder addBandwidthRequest(double bandwidth);
 
         /**
+         * Adds the resources required for a constraint.
+         *
+         * @param constraint the constraint
+         * @return self
+         */
+        public Builder addConstraint(Constraint constraint);
+
+        /**
          * Returns link resource request.
          *
          * @return link resource request
diff --git a/core/api/src/main/java/org/onlab/onos/net/statistic/StatisticStore.java b/core/api/src/main/java/org/onlab/onos/net/statistic/StatisticStore.java
index 29eced7..96fcc11 100644
--- a/core/api/src/main/java/org/onlab/onos/net/statistic/StatisticStore.java
+++ b/core/api/src/main/java/org/onlab/onos/net/statistic/StatisticStore.java
@@ -34,7 +34,7 @@
 
     /**
      * Remove entries associated with this rule.
-     a @param rule {@link org.onlab.onos.net.flow.FlowRule}
+     * @param rule {@link org.onlab.onos.net.flow.FlowRule}
      */
     void removeFromStatistics(FlowRule rule);
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/PathService.java b/core/api/src/main/java/org/onlab/onos/net/topology/PathService.java
index c4623b6..1719285 100644
--- a/core/api/src/main/java/org/onlab/onos/net/topology/PathService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/PathService.java
@@ -43,6 +43,7 @@
      *
      * @param src source element
      * @param dst destination element
+     * @param weight edge-weight entity
      * @return set of all shortest paths between the two element
      */
     Set<Path> getPaths(ElementId src, ElementId dst, LinkWeight weight);
diff --git a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyService.java b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyService.java
index 31a718b..5d8bbdf 100644
--- a/core/api/src/main/java/org/onlab/onos/net/topology/TopologyService.java
+++ b/core/api/src/main/java/org/onlab/onos/net/topology/TopologyService.java
@@ -103,6 +103,7 @@
      * @param topology topology descriptor
      * @param src      source device
      * @param dst      destination device
+     * @param weight   edge-weight entity
      * @return set of all shortest paths between the two devices
      */
     Set<Path> getPaths(Topology topology, DeviceId src, DeviceId dst,
diff --git a/core/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterCommunicationService.java b/core/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterCommunicationService.java
index cd2daff..283f92d 100644
--- a/core/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterCommunicationService.java
+++ b/core/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterCommunicationService.java
@@ -33,6 +33,7 @@
      *
      * @param message  message to send
      * @return true if the message was sent successfully to all nodes; false otherwise.
+     * @throws IOException when I/O exception of some sort has occurred
      */
     boolean broadcast(ClusterMessage message) throws IOException;
 
@@ -42,6 +43,7 @@
      * @param message  message to send
      * @param toNodeId node identifier
      * @return true if the message was sent successfully; false otherwise.
+     * @throws IOException when I/O exception of some sort has occurred
      */
     boolean unicast(ClusterMessage message, NodeId toNodeId) throws IOException;
 
@@ -49,7 +51,9 @@
      * Multicast a message to a set of controller nodes.
      *
      * @param message  message to send
+     * @param nodeIds  recipient node identifiers
      * @return true if the message was sent successfully to all nodes in the group; false otherwise.
+     * @throws IOException when I/O exception of some sort has occurred
      */
     boolean multicast(ClusterMessage message, Set<NodeId> nodeIds) throws IOException;
 
@@ -58,7 +62,7 @@
      * @param message message to send
      * @param toNodeId recipient node identifier
      * @return reply future.
-     * @throws IOException
+     * @throws IOException when I/O exception of some sort has occurred
      */
     ListenableFuture<byte[]> sendAndReceive(ClusterMessage message, NodeId toNodeId) throws IOException;
 
diff --git a/core/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterMessage.java b/core/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterMessage.java
index 5c8550f..c820704 100644
--- a/core/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterMessage.java
+++ b/core/api/src/main/java/org/onlab/onos/store/cluster/messaging/ClusterMessage.java
@@ -32,7 +32,9 @@
     /**
      * Creates a cluster message.
      *
+     * @param sender  message sender
      * @param subject message subject
+     * @param payload message payload
      */
     public ClusterMessage(NodeId sender, MessageSubject subject, byte[] payload) {
         this.sender = sender;
@@ -71,7 +73,7 @@
      * Sends a response to the sender.
      *
      * @param data payload response.
-     * @throws IOException
+     * @throws IOException when I/O exception of some sort has occurred
      */
     public void respond(byte[] data) throws IOException {
         throw new IllegalStateException("One can only repond to message recived from others.");
diff --git a/core/api/src/test/java/org/onlab/onos/event/DefaultEventSinkRegistryTest.java b/core/api/src/test/java/org/onlab/onos/event/DefaultEventSinkRegistryTest.java
index 0bb1e8c..cde9314 100644
--- a/core/api/src/test/java/org/onlab/onos/event/DefaultEventSinkRegistryTest.java
+++ b/core/api/src/test/java/org/onlab/onos/event/DefaultEventSinkRegistryTest.java
@@ -28,11 +28,15 @@
     private DefaultEventSinkRegistry registry;
 
     private static class FooEvent extends TestEvent {
-        public FooEvent(String subject) { super(Type.FOO, subject); }
+        public FooEvent(String subject) {
+            super(Type.FOO, subject);
+        }
     }
 
     private static class BarEvent extends TestEvent {
-        public BarEvent(String subject) { super(Type.BAR, subject); }
+        public BarEvent(String subject) {
+            super(Type.BAR, subject);
+        }
     }
 
     private static class FooSink implements EventSink<FooEvent> {
diff --git a/core/api/src/test/java/org/onlab/onos/net/NetTestTools.java b/core/api/src/test/java/org/onlab/onos/net/NetTestTools.java
index 9bc0450..8d2a61b 100644
--- a/core/api/src/test/java/org/onlab/onos/net/NetTestTools.java
+++ b/core/api/src/test/java/org/onlab/onos/net/NetTestTools.java
@@ -23,6 +23,7 @@
 import java.util.HashSet;
 import java.util.List;
 
+import static org.junit.Assert.assertEquals;
 import static org.onlab.onos.net.DeviceId.deviceId;
 import static org.onlab.onos.net.HostId.hostId;
 import static org.onlab.onos.net.PortNumber.portNumber;
@@ -85,4 +86,23 @@
         return new DefaultPath(PID, links, ids.length);
     }
 
+
+    /**
+     * Verifies that Annotations created by merging {@code annotations} is
+     * equal to actual Annotations.
+     *
+     * @param actual Annotations to check
+     * @param annotations
+     */
+    public static void assertAnnotationsEquals(Annotations actual, SparseAnnotations... annotations) {
+        DefaultAnnotations expected = DefaultAnnotations.builder().build();
+        for (SparseAnnotations a : annotations) {
+            expected = DefaultAnnotations.merge(expected, a);
+        }
+        assertEquals(expected.keys(), actual.keys());
+        for (String key : expected.keys()) {
+            assertEquals(expected.value(key), actual.value(key));
+        }
+    }
+
 }
diff --git a/core/api/src/test/java/org/onlab/onos/net/host/HostServiceAdapter.java b/core/api/src/test/java/org/onlab/onos/net/host/HostServiceAdapter.java
index a0a0e36..03a8a43 100644
--- a/core/api/src/test/java/org/onlab/onos/net/host/HostServiceAdapter.java
+++ b/core/api/src/test/java/org/onlab/onos/net/host/HostServiceAdapter.java
@@ -95,7 +95,7 @@
     }
 
     @Override
-    public PortAddresses getAddressBindingsForPort(ConnectPoint connectPoint) {
+    public Set<PortAddresses> getAddressBindingsForPort(ConnectPoint connectPoint) {
         return null;
     }
 
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/PathIntentTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/PathIntentTest.java
index 02ca0f5..5fce240 100644
--- a/core/api/src/test/java/org/onlab/onos/net/intent/PathIntentTest.java
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/PathIntentTest.java
@@ -18,7 +18,6 @@
 import org.junit.Test;
 import org.onlab.onos.net.NetTestTools;
 import org.onlab.onos.net.Path;
-import org.onlab.onos.net.resource.LinkResourceRequest;
 
 import static org.junit.Assert.assertEquals;
 
@@ -40,11 +39,11 @@
 
     @Override
     protected PathIntent createOne() {
-        return new PathIntent(APPID, MATCH, NOP, PATH1, new LinkResourceRequest[0]);
+        return new PathIntent(APPID, MATCH, NOP, PATH1);
     }
 
     @Override
     protected PathIntent createAnother() {
-        return new PathIntent(APPID, MATCH, NOP, PATH2, new LinkResourceRequest[0]);
+        return new PathIntent(APPID, MATCH, NOP, PATH2);
     }
 }
diff --git a/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java b/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
index 4fe9022..bd7fb94 100644
--- a/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/flow/impl/FlowRuleManager.java
@@ -371,10 +371,11 @@
             final FlowRuleBatchRequest request = event.subject();
             switch (event.type()) {
             case BATCH_OPERATION_REQUESTED:
-                for (FlowEntry entry : request.toAdd()) {
+                // Request has been forwarded to MASTER Node, and was
+                for (FlowRule entry : request.toAdd()) {
                     eventDispatcher.post(new FlowRuleEvent(FlowRuleEvent.Type.RULE_ADD_REQUESTED, entry));
                 }
-                for (FlowEntry entry : request.toRemove()) {
+                for (FlowRule entry : request.toRemove()) {
                     eventDispatcher.post(new FlowRuleEvent(FlowRuleEvent.Type.RULE_REMOVE_REQUESTED, entry));
                 }
                 // FIXME: what about op.equals(FlowRuleOperation.MODIFY) ?
@@ -392,21 +393,15 @@
                                                                                   Futures.getUnchecked(result)));
                     }
                 }, futureListeners);
+                break;
 
-                break;
             case BATCH_OPERATION_COMPLETED:
-                Set<FlowRule> failedItems = event.result().failedItems();
-                for (FlowEntry entry : request.toAdd()) {
-                    if (!failedItems.contains(entry)) {
-                        eventDispatcher.post(new FlowRuleEvent(FlowRuleEvent.Type.RULE_ADDED, entry));
-                    }
-                }
-                for (FlowEntry entry : request.toRemove()) {
-                    if (!failedItems.contains(entry)) {
-                            eventDispatcher.post(new FlowRuleEvent(FlowRuleEvent.Type.RULE_REMOVED, entry));
-                    }
-                }
+                // MASTER Node has pushed the batch down to the Device
+
+                // Note: RULE_ADDED will be posted
+                // when Flow was actually confirmed by stats reply.
                 break;
+
             default:
                 break;
             }
diff --git a/core/net/src/main/java/org/onlab/onos/net/host/impl/HostManager.java b/core/net/src/main/java/org/onlab/onos/net/host/impl/HostManager.java
index 762bae0..5b5e5b7 100644
--- a/core/net/src/main/java/org/onlab/onos/net/host/impl/HostManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/host/impl/HostManager.java
@@ -207,7 +207,7 @@
     }
 
     @Override
-    public PortAddresses getAddressBindingsForPort(ConnectPoint connectPoint) {
+    public Set<PortAddresses> getAddressBindingsForPort(ConnectPoint connectPoint) {
         return store.getAddressBindingsForPort(connectPoint);
     }
 
diff --git a/core/net/src/main/java/org/onlab/onos/net/host/impl/HostMonitor.java b/core/net/src/main/java/org/onlab/onos/net/host/impl/HostMonitor.java
index a6af018..d7299e8 100644
--- a/core/net/src/main/java/org/onlab/onos/net/host/impl/HostMonitor.java
+++ b/core/net/src/main/java/org/onlab/onos/net/host/impl/HostMonitor.java
@@ -175,13 +175,15 @@
         for (Device device : deviceService.getDevices()) {
             for (Port port : deviceService.getPorts(device.id())) {
                 ConnectPoint cp = new ConnectPoint(device.id(), port.number());
-                PortAddresses portAddresses =
+                Set<PortAddresses> portAddressSet =
                     hostManager.getAddressBindingsForPort(cp);
 
-                for (InterfaceIpAddress ia : portAddresses.ipAddresses()) {
-                    if (ia.subnetAddress().contains(targetIp)) {
-                        sendProbe(device.id(), port, targetIp,
-                                  ia.ipAddress(), portAddresses.mac());
+                for (PortAddresses portAddresses : portAddressSet) {
+                    for (InterfaceIpAddress ia : portAddresses.ipAddresses()) {
+                        if (ia.subnetAddress().contains(targetIp)) {
+                            sendProbe(device.id(), port, targetIp,
+                                      ia.ipAddress(), portAddresses.mac());
+                        }
                     }
                 }
             }
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java
new file mode 100644
index 0000000..4cf1830
--- /dev/null
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/ConnectivityIntentCompiler.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2014 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.onlab.onos.net.intent.impl;
+
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onlab.onos.net.ElementId;
+import org.onlab.onos.net.Path;
+import org.onlab.onos.net.intent.ConnectivityIntent;
+import org.onlab.onos.net.intent.Constraint;
+import org.onlab.onos.net.intent.IntentCompiler;
+import org.onlab.onos.net.intent.IntentExtensionService;
+import org.onlab.onos.net.provider.ProviderId;
+import org.onlab.onos.net.resource.LinkResourceService;
+import org.onlab.onos.net.topology.LinkWeight;
+import org.onlab.onos.net.topology.PathService;
+import org.onlab.onos.net.topology.TopologyEdge;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Base class for compilers of various
+ * {@link org.onlab.onos.net.intent.ConnectivityIntent connectivity intents}.
+ */
+@Component(immediate = true)
+public abstract class ConnectivityIntentCompiler<T extends ConnectivityIntent>
+        implements IntentCompiler<T> {
+
+    private static final ProviderId PID = new ProviderId("core", "org.onlab.onos.core", true);
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected IntentExtensionService intentManager;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected PathService pathService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected LinkResourceService resourceService;
+
+    /**
+     * Returns an edge-weight capable of evaluating links on the basis of the
+     * specified constraints.
+     *
+     * @param constraints path constraints
+     * @return edge-weight function
+     */
+    protected LinkWeight weight(List<Constraint> constraints) {
+        return new ConstraintBasedLinkWeight(constraints);
+    }
+
+    /**
+     * Validates the specified path against the given constraints.
+     *
+     * @param path        path to be checked
+     * @param constraints path constraints
+     * @return true if the path passes all constraints
+     */
+    protected boolean checkPath(Path path, List<Constraint> constraints) {
+        for (Constraint constraint : constraints) {
+            if (!constraint.validate(path, resourceService)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Computes a path between two ConnectPoints.
+     *
+     * @param intent intent on which behalf path is being computed
+     * @param one    start of the path
+     * @param two    end of the path
+     * @return Path between the two
+     * @throws PathNotFoundException if a path cannot be found
+     */
+    protected Path getPath(ConnectivityIntent intent,
+                           ElementId one, ElementId two) {
+        Set<Path> paths = pathService.getPaths(one, two, weight(intent.constraints()));
+        if (paths.isEmpty()) {
+            throw new PathNotFoundException("No packet path from " + one + " to " + two);
+        }
+        // TODO: let's be more intelligent about this eventually
+        return paths.iterator().next();
+    }
+
+    /**
+     * Edge-weight capable of evaluating link cost using a set of constraints.
+     */
+    protected class ConstraintBasedLinkWeight implements LinkWeight {
+
+        private final List<Constraint> constraints;
+
+        /**
+         * Creates a new edge-weight function capable of evaluating links
+         * on the basis of the specified constraints.
+         *
+         * @param constraints path constraints
+         */
+        ConstraintBasedLinkWeight(List<Constraint> constraints) {
+            this.constraints = constraints;
+        }
+
+        @Override
+        public double weight(TopologyEdge edge) {
+            if (constraints == null) {
+                return 1.0;
+            }
+
+            // iterate over all constraints in order and return the weight of
+            // the first one with fast fail over the first failure
+            Iterator<Constraint> it = constraints.iterator();
+            double cost = it.next().cost(edge.link(), resourceService);
+            while (it.hasNext() && cost > 0) {
+                if (it.next().cost(edge.link(), resourceService) < 0) {
+                    return -1;
+                }
+            }
+            return cost;
+        }
+    }
+
+}
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/HostToHostIntentCompiler.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/HostToHostIntentCompiler.java
index 37cf84b..605d3c7 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/HostToHostIntentCompiler.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/HostToHostIntentCompiler.java
@@ -15,30 +15,21 @@
  */
 package org.onlab.onos.net.intent.impl;
 
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
-
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.onlab.onos.net.Host;
-import org.onlab.onos.net.HostId;
-import org.onlab.onos.net.Link;
 import org.onlab.onos.net.Path;
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.host.HostService;
 import org.onlab.onos.net.intent.HostToHostIntent;
 import org.onlab.onos.net.intent.Intent;
-import org.onlab.onos.net.intent.IntentCompiler;
-import org.onlab.onos.net.intent.IntentExtensionService;
 import org.onlab.onos.net.intent.PathIntent;
-import org.onlab.onos.net.topology.LinkWeight;
-import org.onlab.onos.net.resource.LinkResourceRequest;
-import org.onlab.onos.net.topology.PathService;
-import org.onlab.onos.net.topology.TopologyEdge;
+
+import java.util.Arrays;
+import java.util.List;
 
 import static org.onlab.onos.net.flow.DefaultTrafficSelector.builder;
 
@@ -47,13 +38,7 @@
  */
 @Component(immediate = true)
 public class HostToHostIntentCompiler
-        implements IntentCompiler<HostToHostIntent> {
-
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected IntentExtensionService intentManager;
-
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected PathService pathService;
+        extends ConnectivityIntentCompiler<HostToHostIntent> {
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected HostService hostService;
@@ -70,8 +55,8 @@
 
     @Override
     public List<Intent> compile(HostToHostIntent intent) {
-        Path pathOne = getPath(intent.one(), intent.two());
-        Path pathTwo = getPath(intent.two(), intent.one());
+        Path pathOne = getPath(intent, intent.one(), intent.two());
+        Path pathTwo = getPath(intent, intent.two(), intent.one());
 
         Host one = hostService.getHost(intent.one());
         Host two = hostService.getHost(intent.two());
@@ -85,22 +70,7 @@
                                     HostToHostIntent intent) {
         TrafficSelector selector = builder(intent.selector())
                 .matchEthSrc(src.mac()).matchEthDst(dst.mac()).build();
-        return new PathIntent(intent.appId(), selector, intent.treatment(),
-                              path, new LinkResourceRequest[0]);
+        return new PathIntent(intent.appId(), selector, intent.treatment(), path);
     }
 
-    private Path getPath(HostId one, HostId two) {
-        Set<Path> paths = pathService.getPaths(one, two, new LinkWeight() {
-            @Override
-            public double weight(TopologyEdge edge) {
-                return edge.link().type() == Link.Type.OPTICAL ? -1 : +1;
-            }
-        });
-
-        if (paths.isEmpty()) {
-            throw new PathNotFoundException("No path from host " + one + " to " + two);
-        }
-        // TODO: let's be more intelligent about this eventually
-        return paths.iterator().next();
-    }
 }
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/OpticalPathIntentInstaller.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/OpticalPathIntentInstaller.java
index 5faae4d..27b5387 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/OpticalPathIntentInstaller.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/OpticalPathIntentInstaller.java
@@ -79,6 +79,7 @@
     private ApplicationId appId;
 
     //final short WAVELENGTH = 80;
+    static final short SIGNAL_TYPE = (short) 1;
 
     @Activate
     public void activate() {
@@ -151,7 +152,9 @@
 
             prev = link.dst();
             selectorBuilder.matchInport(link.dst().port());
+            selectorBuilder.matchOpticalSignalType(SIGNAL_TYPE); //todo
             selectorBuilder.matchLambda((short) la.toInt());
+
         }
 
         // build the last T port rule
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/PathIntentInstaller.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/PathIntentInstaller.java
index c3b27bf..461f670 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/PathIntentInstaller.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/PathIntentInstaller.java
@@ -35,10 +35,13 @@
 import org.onlab.onos.net.flow.FlowRuleBatchOperation;
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.intent.Constraint;
 import org.onlab.onos.net.intent.IntentExtensionService;
 import org.onlab.onos.net.intent.IntentInstaller;
 import org.onlab.onos.net.intent.PathIntent;
+import org.onlab.onos.net.resource.DefaultLinkResourceRequest;
 import org.onlab.onos.net.resource.LinkResourceAllocations;
+import org.onlab.onos.net.resource.LinkResourceRequest;
 import org.onlab.onos.net.resource.LinkResourceService;
 import org.slf4j.Logger;
 
@@ -79,13 +82,7 @@
 
     @Override
     public List<FlowRuleBatchOperation> install(PathIntent intent) {
-        if (intent.resourceRequests().length > 0) {
-            LinkResourceAllocations allocations = allocateBandwidth(intent);
-            if (allocations == null) {
-                log.debug("Insufficient bandwidth available to install path intent {}", intent);
-                return null;
-            }
-        }
+        LinkResourceAllocations allocations = allocateResources(intent);
 
         TrafficSelector.Builder builder =
                 DefaultTrafficSelector.builder(intent.selector());
@@ -110,6 +107,10 @@
 
     @Override
     public List<FlowRuleBatchOperation> uninstall(PathIntent intent) {
+        LinkResourceAllocations allocatedResources = resourceService.getAllocations(intent.id());
+        if (allocatedResources != null) {
+            resourceService.releaseResources(allocatedResources);
+        }
         TrafficSelector.Builder builder =
                 DefaultTrafficSelector.builder(intent.selector());
         Iterator<Link> links = intent.path().links().iterator();
@@ -130,8 +131,23 @@
         return Lists.newArrayList(new FlowRuleBatchOperation(rules));
     }
 
-    private LinkResourceAllocations allocateBandwidth(PathIntent intent) {
-        return resourceService.requestResources(intent.resourceRequests()[0]);
+    /**
+     * Allocate resources required for an intent.
+     *
+     * @param intent intent to allocate resource for
+     * @return allocated resources if any are required, null otherwise
+     */
+    private LinkResourceAllocations allocateResources(PathIntent intent) {
+        if (intent.constraints() == null) {
+            return null;
+        }
+        LinkResourceRequest.Builder builder =
+                DefaultLinkResourceRequest.builder(intent.id(), intent.path().links());
+        for (Constraint constraint : intent.constraints()) {
+            builder.addConstraint(constraint);
+        }
+        LinkResourceRequest request = builder.build();
+        return request.resources().isEmpty() ? null : resourceService.requestResources(request);
     }
 
     // TODO refactor below this line... ----------------------------
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/PointToPointIntentCompiler.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/PointToPointIntentCompiler.java
index 893b7ea..c32c8ee 100644
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/PointToPointIntentCompiler.java
+++ b/core/net/src/main/java/org/onlab/onos/net/intent/impl/PointToPointIntentCompiler.java
@@ -15,31 +15,20 @@
  */
 package org.onlab.onos.net.intent.impl;
 
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
-
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
-import org.apache.felix.scr.annotations.Reference;
-import org.apache.felix.scr.annotations.ReferenceCardinality;
-import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.DefaultEdgeLink;
 import org.onlab.onos.net.DefaultPath;
 import org.onlab.onos.net.Link;
 import org.onlab.onos.net.Path;
 import org.onlab.onos.net.intent.Intent;
-import org.onlab.onos.net.intent.IntentCompiler;
-import org.onlab.onos.net.intent.IntentExtensionService;
 import org.onlab.onos.net.intent.PathIntent;
 import org.onlab.onos.net.intent.PointToPointIntent;
 import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.net.resource.LinkResourceRequest;
-import org.onlab.onos.net.topology.LinkWeight;
-import org.onlab.onos.net.topology.Topology;
-import org.onlab.onos.net.topology.TopologyEdge;
-import org.onlab.onos.net.topology.TopologyService;
+
+import java.util.ArrayList;
+import java.util.List;
 
 import static java.util.Arrays.asList;
 
@@ -48,14 +37,11 @@
  */
 @Component(immediate = true)
 public class PointToPointIntentCompiler
-        implements IntentCompiler<PointToPointIntent> {
+        extends ConnectivityIntentCompiler<PointToPointIntent> {
 
-    private static final ProviderId PID = new ProviderId("core", "org.onlab.onos.core", true);
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected IntentExtensionService intentManager;
-
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected TopologyService topologyService;
+    // TODO: use off-the-shell core provider ID
+    private static final ProviderId PID =
+            new ProviderId("core", "org.onlab.onos.core", true);
 
     @Activate
     public void activate() {
@@ -69,15 +55,16 @@
 
     @Override
     public List<Intent> compile(PointToPointIntent intent) {
-        Path path = getPath(intent.ingressPoint(), intent.egressPoint());
+        Path path = getPath(intent, intent.ingressPoint().deviceId(),
+                            intent.egressPoint().deviceId());
 
         List<Link> links = new ArrayList<>();
         links.add(DefaultEdgeLink.createEdgeLink(intent.ingressPoint(), true));
         links.addAll(path.links());
         links.add(DefaultEdgeLink.createEdgeLink(intent.egressPoint(), false));
 
-        return asList(createPathIntent(new DefaultPath(PID, links, path.cost() + 2,
-                path.annotations()), intent));
+        return asList(createPathIntent(new DefaultPath(PID, links, path.cost(),
+                                                       path.annotations()), intent));
     }
 
     /**
@@ -90,34 +77,7 @@
     private Intent createPathIntent(Path path,
                                     PointToPointIntent intent) {
         return new PathIntent(intent.appId(),
-                              intent.selector(), intent.treatment(), path,
-                              new LinkResourceRequest[0]);
+                              intent.selector(), intent.treatment(), path);
     }
 
-    /**
-     * Computes a path between two ConnectPoints.
-     *
-     * @param one start of the path
-     * @param two end of the path
-     * @return Path between the two
-     * @throws PathNotFoundException if a path cannot be found
-     */
-    private Path getPath(ConnectPoint one, ConnectPoint two) {
-        Topology topology = topologyService.currentTopology();
-        LinkWeight weight = new LinkWeight() {
-            @Override
-            public double weight(TopologyEdge edge) {
-                return edge.link().type() == Link.Type.OPTICAL ? -1 : +1;
-            }
-        };
-
-        Set<Path> paths = topologyService.getPaths(topology, one.deviceId(),
-                                                   two.deviceId(), weight);
-        if (paths.isEmpty()) {
-            throw new PathNotFoundException("No packet path from " + one + " to " + two);
-        }
-
-        // TODO: let's be more intelligent about this eventually
-        return paths.iterator().next();
-    }
 }
diff --git a/core/net/src/main/java/org/onlab/onos/net/intent/impl/PointToPointIntentWithBandwidthConstraintCompiler.java b/core/net/src/main/java/org/onlab/onos/net/intent/impl/PointToPointIntentWithBandwidthConstraintCompiler.java
deleted file mode 100644
index a192a9f..0000000
--- a/core/net/src/main/java/org/onlab/onos/net/intent/impl/PointToPointIntentWithBandwidthConstraintCompiler.java
+++ /dev/null
@@ -1,146 +0,0 @@
-package org.onlab.onos.net.intent.impl;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Set;
-
-import org.apache.felix.scr.annotations.Activate;
-import org.apache.felix.scr.annotations.Component;
-import org.apache.felix.scr.annotations.Deactivate;
-import org.apache.felix.scr.annotations.Reference;
-import org.apache.felix.scr.annotations.ReferenceCardinality;
-import org.onlab.onos.net.ConnectPoint;
-import org.onlab.onos.net.DefaultEdgeLink;
-import org.onlab.onos.net.DefaultPath;
-import org.onlab.onos.net.Link;
-import org.onlab.onos.net.Path;
-import org.onlab.onos.net.intent.Intent;
-import org.onlab.onos.net.intent.IntentCompiler;
-import org.onlab.onos.net.intent.IntentExtensionService;
-import org.onlab.onos.net.intent.PathIntent;
-import org.onlab.onos.net.intent.PointToPointIntent;
-import org.onlab.onos.net.intent.PointToPointIntentWithBandwidthConstraint;
-import org.onlab.onos.net.provider.ProviderId;
-import org.onlab.onos.net.resource.BandwidthResourceRequest;
-import org.onlab.onos.net.resource.DefaultLinkResourceRequest;
-import org.onlab.onos.net.resource.LinkResourceRequest;
-import org.onlab.onos.net.resource.LinkResourceService;
-import org.onlab.onos.net.resource.ResourceRequest;
-import org.onlab.onos.net.resource.ResourceType;
-import org.onlab.onos.net.topology.LinkWeight;
-import org.onlab.onos.net.topology.Topology;
-import org.onlab.onos.net.topology.TopologyEdge;
-import org.onlab.onos.net.topology.TopologyService;
-
-/**
- * A intent compiler for {@link org.onlab.onos.net.intent.HostToHostIntent}.
- */
-@Component(immediate = true)
-public class PointToPointIntentWithBandwidthConstraintCompiler
-        implements IntentCompiler<PointToPointIntentWithBandwidthConstraint> {
-
-    private static final ProviderId PID = new ProviderId("core", "org.onlab.onos.core", true);
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected IntentExtensionService intentManager;
-
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected TopologyService topologyService;
-
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected LinkResourceService resourceService;
-
-    @Activate
-    public void activate() {
-        intentManager.registerCompiler(PointToPointIntentWithBandwidthConstraint.class, this);
-    }
-
-    @Deactivate
-    public void deactivate() {
-        intentManager.unregisterCompiler(PointToPointIntent.class);
-    }
-
-    @Override
-    public List<Intent> compile(PointToPointIntentWithBandwidthConstraint intent) {
-        Path path = getPath(intent.ingressPoint(), intent.egressPoint(), intent.bandwidthRequest());
-
-        List<Link> links = new ArrayList<>();
-        links.add(DefaultEdgeLink.createEdgeLink(intent.ingressPoint(), true));
-        links.addAll(path.links());
-        links.add(DefaultEdgeLink.createEdgeLink(intent.egressPoint(), false));
-
-        return Arrays.asList(createPathIntent(new DefaultPath(PID, links, path.cost() + 2,
-                                                              path.annotations()),
-                                              intent));
-    }
-
-    /**
-     * Creates a path intent from the specified path and original
-     * connectivity intent.
-     *
-     * @param path   path to create an intent for
-     * @param intent original intent
-     */
-    private Intent createPathIntent(Path path,
-                                    PointToPointIntentWithBandwidthConstraint intent) {
-        LinkResourceRequest.Builder request = DefaultLinkResourceRequest.builder(intent.id(),
-                path.links())
-                // TODO - this seems awkward, maybe allow directly attaching a BandwidthRequest
-                .addBandwidthRequest(intent.bandwidthRequest().bandwidth().toDouble());
-        LinkResourceRequest bandwidthRequest  = request.build();
-        LinkResourceRequest[] bandwidthRequests = {bandwidthRequest};
-        return new PathIntent(intent.appId(),
-                              intent.selector(), intent.treatment(), path,
-                              bandwidthRequests);
-    }
-
-    /**
-     * Computes a path between two ConnectPoints.
-     *
-     * @param one start of the path
-     * @param two end of the path
-     * @return Path between the two
-     * @throws org.onlab.onos.net.intent.impl.PathNotFoundException if a path cannot be found
-     */
-    private Path getPath(ConnectPoint one, ConnectPoint two, final BandwidthResourceRequest bandwidthRequest) {
-        Topology topology = topologyService.currentTopology();
-        LinkWeight weight = new LinkWeight() {
-            @Override
-            public double weight(TopologyEdge edge) {
-                if (bandwidthRequest != null) {
-                    double allocatedBandwidth = 0.0;
-                    Iterable<ResourceRequest> availableResources = resourceService.getAvailableResources(edge.link());
-                    for (ResourceRequest availableResource : availableResources) {
-                        if (availableResource.type() == ResourceType.BANDWIDTH) {
-                            BandwidthResourceRequest bandwidthRequest = (BandwidthResourceRequest) availableResource;
-                            allocatedBandwidth += bandwidthRequest.bandwidth().toDouble();
-                        }
-                    }
-
-                    // TODO this needs to be discovered from switch/ports somehow
-                    double maxBandwidth = 1000;
-
-                    double availableBandwidth = maxBandwidth - allocatedBandwidth;
-                    if (availableBandwidth >= bandwidthRequest.bandwidth().toDouble()) {
-                        return 1;
-                    } else {
-                        return -1;
-                    }
-                } else {
-                    return 1;
-                }
-            }
-        };
-
-        Set<Path> paths = topologyService.getPaths(topology,
-                one.deviceId(),
-                two.deviceId(),
-                weight);
-
-        if (paths.isEmpty()) {
-            throw new PathNotFoundException("No packet path from " + one + " to " + two);
-        }
-        // TODO: let's be more intelligent about this eventually
-        return paths.iterator().next();
-    }
-}
diff --git a/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java b/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java
index 0aae62d..49528d0 100644
--- a/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java
+++ b/core/net/src/main/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManager.java
@@ -134,14 +134,16 @@
             IpAddress target =
                 IpAddress.valueOf(IpAddress.Version.INET,
                                   arp.getTargetProtocolAddress());
-            PortAddresses addresses =
+            Set<PortAddresses> addressSet =
                 hostService.getAddressBindingsForPort(inPort);
 
-            for (InterfaceIpAddress ia : addresses.ipAddresses()) {
-                if (ia.ipAddress().equals(target)) {
-                    Ethernet arpReply =
-                        buildArpReply(ia.ipAddress(), addresses.mac(), eth);
-                    sendTo(arpReply, inPort);
+            for (PortAddresses addresses : addressSet) {
+                for (InterfaceIpAddress ia : addresses.ipAddresses()) {
+                    if (ia.ipAddress().equals(target)) {
+                        Ethernet arpReply =
+                            buildArpReply(ia.ipAddress(), addresses.mac(), eth);
+                        sendTo(arpReply, inPort);
+                    }
                 }
             }
             return;
@@ -244,7 +246,7 @@
         // TODO: Is this sufficient to identify outside-facing ports: just
         // having IP addresses on a port?
         //
-        return !hostService.getAddressBindingsForPort(port).ipAddresses().isEmpty();
+        return !hostService.getAddressBindingsForPort(port).isEmpty();
     }
 
     @Override
diff --git a/core/net/src/test/java/org/onlab/onos/net/flow/impl/FlowRuleManagerTest.java b/core/net/src/test/java/org/onlab/onos/net/flow/impl/FlowRuleManagerTest.java
index b986d6d..f67d992 100644
--- a/core/net/src/test/java/org/onlab/onos/net/flow/impl/FlowRuleManagerTest.java
+++ b/core/net/src/test/java/org/onlab/onos/net/flow/impl/FlowRuleManagerTest.java
@@ -148,7 +148,7 @@
         int i = 0;
         System.err.println("events :" + listener.events);
         for (FlowRuleEvent e : listener.events) {
-            assertTrue("unexpected event", e.type().equals(events[i]));
+            assertEquals("unexpected event", events[i], e.type());
             i++;
         }
 
@@ -178,15 +178,13 @@
                        RULE_ADDED, RULE_ADDED);
 
         addFlowRule(1);
+        System.err.println("events :" + listener.events);
         assertEquals("should still be 2 rules", 2, flowCount());
 
         providerService.pushFlowMetrics(DID, ImmutableList.of(fe1));
         validateEvents(RULE_UPDATED);
     }
 
-
-    // TODO: If preserving iteration order is a requirement, redo FlowRuleStore.
-    //backing store is sensitive to the order of additions/removals
     private boolean validateState(Map<FlowRule, FlowEntryState> expected) {
         Map<FlowRule, FlowEntryState> expectedToCheck = new HashMap<>(expected);
         Iterable<FlowEntry> rules = service.getFlowEntries(DID);
@@ -539,17 +537,17 @@
 
             @Override
             public boolean cancel(boolean mayInterruptIfRunning) {
-                return true;
+                return false;
             }
 
             @Override
             public boolean isCancelled() {
-                return true;
+                return false;
             }
 
             @Override
             public boolean isDone() {
-                return false;
+                return true;
             }
 
             @Override
@@ -562,12 +560,14 @@
             public CompletedBatchOperation get(long timeout, TimeUnit unit)
                     throws InterruptedException,
                     ExecutionException, TimeoutException {
-                return null;
+                return new CompletedBatchOperation(true, Collections.<FlowRule>emptySet());
             }
 
             @Override
             public void addListener(Runnable task, Executor executor) {
-                // TODO: add stuff.
+                if (isDone()) {
+                    executor.execute(task);
+                }
             }
         }
 
diff --git a/core/net/src/test/java/org/onlab/onos/net/host/impl/HostManagerTest.java b/core/net/src/test/java/org/onlab/onos/net/host/impl/HostManagerTest.java
index cbc9cf1..6a058ab 100644
--- a/core/net/src/test/java/org/onlab/onos/net/host/impl/HostManagerTest.java
+++ b/core/net/src/test/java/org/onlab/onos/net/host/impl/HostManagerTest.java
@@ -234,10 +234,10 @@
             new PortAddresses(CP1, Sets.newHashSet(IA1, IA2), MAC1);
 
         mgr.bindAddressesToPort(add1);
-        PortAddresses storedAddresses = mgr.getAddressBindingsForPort(CP1);
+        Set<PortAddresses> storedAddresses = mgr.getAddressBindingsForPort(CP1);
 
-        assertTrue(add1.ipAddresses().equals(storedAddresses.ipAddresses()));
-        assertTrue(add1.mac().equals(storedAddresses.mac()));
+        assertEquals(1, storedAddresses.size());
+        assertTrue(storedAddresses.contains(add1));
 
         // Add some more addresses and check that they're added correctly
         PortAddresses add2 =
@@ -246,18 +246,19 @@
         mgr.bindAddressesToPort(add2);
         storedAddresses = mgr.getAddressBindingsForPort(CP1);
 
-        assertTrue(storedAddresses.ipAddresses().equals(
-                Sets.newHashSet(IA1, IA2, IA3)));
-        assertTrue(storedAddresses.mac().equals(MAC1));
+        assertEquals(2, storedAddresses.size());
+        assertTrue(storedAddresses.contains(add1));
+        assertTrue(storedAddresses.contains(add2));
 
         PortAddresses add3 = new PortAddresses(CP1, null, MAC2);
 
         mgr.bindAddressesToPort(add3);
         storedAddresses = mgr.getAddressBindingsForPort(CP1);
 
-        assertTrue(storedAddresses.ipAddresses().equals(
-                Sets.newHashSet(IA1, IA2, IA3)));
-        assertTrue(storedAddresses.mac().equals(MAC2));
+        assertEquals(3, storedAddresses.size());
+        assertTrue(storedAddresses.contains(add1));
+        assertTrue(storedAddresses.contains(add2));
+        assertTrue(storedAddresses.contains(add3));
     }
 
     @Test
@@ -266,10 +267,10 @@
             new PortAddresses(CP1, Sets.newHashSet(IA1, IA2), MAC1);
 
         mgr.bindAddressesToPort(add1);
-        PortAddresses storedAddresses = mgr.getAddressBindingsForPort(CP1);
+        Set<PortAddresses> storedAddresses = mgr.getAddressBindingsForPort(CP1);
 
-        assertTrue(storedAddresses.ipAddresses().size() == 2);
-        assertNotNull(storedAddresses.mac());
+        assertEquals(1, storedAddresses.size());
+        assertTrue(storedAddresses.contains(add1));
 
         PortAddresses rem1 =
             new PortAddresses(CP1, Sets.newHashSet(IA1), null);
@@ -277,25 +278,15 @@
         mgr.unbindAddressesFromPort(rem1);
         storedAddresses = mgr.getAddressBindingsForPort(CP1);
 
-        assertTrue(storedAddresses.ipAddresses().equals(Sets.newHashSet(IA2)));
-        assertTrue(storedAddresses.mac().equals(MAC1));
+        // It shouldn't have been removed because it didn't match the originally
+        // submitted address object
+        assertEquals(1, storedAddresses.size());
+        assertTrue(storedAddresses.contains(add1));
 
-        PortAddresses rem2 = new PortAddresses(CP1, null, MAC1);
-
-        mgr.unbindAddressesFromPort(rem2);
+        mgr.unbindAddressesFromPort(add1);
         storedAddresses = mgr.getAddressBindingsForPort(CP1);
 
-        assertTrue(storedAddresses.ipAddresses().equals(Sets.newHashSet(IA2)));
-        assertNull(storedAddresses.mac());
-
-        PortAddresses rem3 =
-            new PortAddresses(CP1, Sets.newHashSet(IA2), MAC1);
-
-        mgr.unbindAddressesFromPort(rem3);
-        storedAddresses = mgr.getAddressBindingsForPort(CP1);
-
-        assertTrue(storedAddresses.ipAddresses().isEmpty());
-        assertNull(storedAddresses.mac());
+        assertTrue(storedAddresses.isEmpty());
     }
 
     @Test
@@ -304,16 +295,15 @@
             new PortAddresses(CP1, Sets.newHashSet(IA1, IA2), MAC1);
 
         mgr.bindAddressesToPort(add1);
-        PortAddresses storedAddresses = mgr.getAddressBindingsForPort(CP1);
+        Set<PortAddresses> storedAddresses = mgr.getAddressBindingsForPort(CP1);
 
-        assertTrue(storedAddresses.ipAddresses().size() == 2);
-        assertNotNull(storedAddresses.mac());
+        assertEquals(1, storedAddresses.size());
+        assertTrue(storedAddresses.contains(add1));
 
         mgr.clearAddresses(CP1);
         storedAddresses = mgr.getAddressBindingsForPort(CP1);
 
-        assertTrue(storedAddresses.ipAddresses().isEmpty());
-        assertNull(storedAddresses.mac());
+        assertTrue(storedAddresses.isEmpty());
     }
 
     @Test
@@ -322,12 +312,10 @@
             new PortAddresses(CP1, Sets.newHashSet(IA1, IA2), MAC1);
 
         mgr.bindAddressesToPort(add1);
-        PortAddresses storedAddresses = mgr.getAddressBindingsForPort(CP1);
+        Set<PortAddresses> storedAddresses = mgr.getAddressBindingsForPort(CP1);
 
-        assertTrue(storedAddresses.connectPoint().equals(CP1));
-        assertTrue(storedAddresses.ipAddresses().equals(
-                        Sets.newHashSet(IA1, IA2)));
-        assertTrue(storedAddresses.mac().equals(MAC1));
+        assertEquals(1, storedAddresses.size());
+        assertTrue(storedAddresses.contains(add1));
     }
 
     @Test
diff --git a/core/net/src/test/java/org/onlab/onos/net/host/impl/HostMonitorTest.java b/core/net/src/test/java/org/onlab/onos/net/host/impl/HostMonitorTest.java
index 654cab1..a8febd7 100644
--- a/core/net/src/test/java/org/onlab/onos/net/host/impl/HostMonitorTest.java
+++ b/core/net/src/test/java/org/onlab/onos/net/host/impl/HostMonitorTest.java
@@ -20,7 +20,9 @@
 import static org.easymock.EasyMock.expectLastCall;
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -130,7 +132,7 @@
         expect(hostManager.getHostsByIp(TARGET_IP_ADDR))
                 .andReturn(Collections.<Host>emptySet()).anyTimes();
         expect(hostManager.getAddressBindingsForPort(cp))
-                .andReturn(pa).anyTimes();
+                .andReturn(Collections.singleton(pa)).anyTimes();
         replay(hostManager);
 
         TestPacketService packetService = new TestPacketService();
diff --git a/core/net/src/test/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManagerTest.java b/core/net/src/test/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManagerTest.java
index e82151e..8beac4a 100644
--- a/core/net/src/test/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManagerTest.java
+++ b/core/net/src/test/java/org/onlab/onos/net/proxyarp/impl/ProxyArpManagerTest.java
@@ -19,7 +19,10 @@
 import static org.easymock.EasyMock.createMock;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.replay;
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -207,13 +210,18 @@
             IpAddress addr2 = IpAddress.valueOf("10.0." + (2 * i) + ".1");
             InterfaceIpAddress ia1 = new InterfaceIpAddress(addr1, prefix1);
             InterfaceIpAddress ia2 = new InterfaceIpAddress(addr2, prefix2);
-            PortAddresses pa =
-                new PortAddresses(cp, Sets.newHashSet(ia1, ia2),
-                                  MacAddress.valueOf(i));
-            addresses.add(pa);
+            PortAddresses pa1 =
+                new PortAddresses(cp, Sets.newHashSet(ia1),
+                                  MacAddress.valueOf(2 * i - 1));
+            PortAddresses pa2 =
+                    new PortAddresses(cp, Sets.newHashSet(ia2),
+                                      MacAddress.valueOf(2 * i));
+
+            addresses.add(pa1);
+            addresses.add(pa2);
 
             expect(hostService.getAddressBindingsForPort(cp))
-                    .andReturn(pa).anyTimes();
+                    .andReturn(Sets.newHashSet(pa1, pa2)).anyTimes();
         }
 
         expect(hostService.getAddressBindings()).andReturn(addresses).anyTimes();
@@ -222,7 +230,7 @@
             ConnectPoint cp = new ConnectPoint(getDeviceId(i + NUM_ADDRESS_PORTS),
                     P1);
             expect(hostService.getAddressBindingsForPort(cp))
-                    .andReturn(new PortAddresses(cp, null, null)).anyTimes();
+                    .andReturn(Collections.<PortAddresses>emptySet()).anyTimes();
         }
     }
 
@@ -339,7 +347,8 @@
         IpAddress theirIp = IpAddress.valueOf("10.0.1.254");
         IpAddress ourFirstIp = IpAddress.valueOf("10.0.1.1");
         IpAddress ourSecondIp = IpAddress.valueOf("10.0.2.1");
-        MacAddress ourMac = MacAddress.valueOf(1L);
+        MacAddress firstMac = MacAddress.valueOf(1L);
+        MacAddress secondMac = MacAddress.valueOf(2L);
 
         Host requestor = new DefaultHost(PID, HID2, MAC2, VLAN1, LOC1,
                 Collections.singleton(theirIp));
@@ -352,7 +361,7 @@
         proxyArp.reply(arpRequest, LOC1);
 
         assertEquals(1, packetService.packets.size());
-        Ethernet arpReply = buildArp(ARP.OP_REPLY, ourMac, MAC2, ourFirstIp, theirIp);
+        Ethernet arpReply = buildArp(ARP.OP_REPLY, firstMac, MAC2, ourFirstIp, theirIp);
         verifyPacketOut(arpReply, LOC1, packetService.packets.get(0));
 
         // Test a request for the second address on that port
@@ -362,7 +371,7 @@
         proxyArp.reply(arpRequest, LOC1);
 
         assertEquals(1, packetService.packets.size());
-        arpReply = buildArp(ARP.OP_REPLY, ourMac, MAC2, ourSecondIp, theirIp);
+        arpReply = buildArp(ARP.OP_REPLY, secondMac, MAC2, ourSecondIp, theirIp);
         verifyPacketOut(arpReply, LOC1, packetService.packets.get(0));
     }
 
diff --git a/core/store/dist/pom.xml b/core/store/dist/pom.xml
index eebb8ff..846583f 100644
--- a/core/store/dist/pom.xml
+++ b/core/store/dist/pom.xml
@@ -44,6 +44,24 @@
             <version>${project.version}</version>
         </dependency>
 
+       <dependency>
+            <groupId>net.kuujo.copycat</groupId>
+            <artifactId>copycat</artifactId>
+            <version>0.4.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>net.kuujo.copycat</groupId>
+            <artifactId>copycat-chronicle</artifactId>
+            <version>0.4.0-SNAPSHOT</version>
+        </dependency>
+
+        <dependency>
+            <groupId>net.kuujo.copycat</groupId>
+            <artifactId>copycat-tcp</artifactId>
+            <version>0.4.0-SNAPSHOT</version>
+        </dependency>
+
         <dependency>
             <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/impl/ClusterDefinitionStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/impl/ClusterDefinitionStore.java
index 97ec769..19b6485 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/impl/ClusterDefinitionStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/impl/ClusterDefinitionStore.java
@@ -48,7 +48,7 @@
         file = new File(filePath);
     }
 
-    /**
+    /*
      * Returns set of the controller nodes, including self.
      *
      * @return set of controller nodes
@@ -67,7 +67,7 @@
         return nodes;
     }
 
-    /**
+    /*
      * Writes the given set of the controller nodes.
      *
      * @param nodes set of controller nodes
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
index 79612c9..38e1322 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/cluster/messaging/impl/ClusterCommunicationManager.java
@@ -88,7 +88,7 @@
             log.error("NettyMessagingService#activate", e);
         }
         messagingService = netty;
-        log.info("Started");
+        log.info("Started on {}:{}", localNode.ip(), localNode.tcpPort());
     }
 
     @Deactivate
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
index 66366c1..19dc3e3 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStore.java
@@ -32,6 +32,8 @@
 import org.onlab.onos.cluster.ControllerNode;
 import org.onlab.onos.cluster.NodeId;
 import org.onlab.onos.mastership.MastershipService;
+import org.onlab.onos.mastership.MastershipTerm;
+import org.onlab.onos.mastership.MastershipTermService;
 import org.onlab.onos.net.AnnotationsUtil;
 import org.onlab.onos.net.DefaultAnnotations;
 import org.onlab.onos.net.DefaultDevice;
@@ -39,6 +41,7 @@
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.Device.Type;
 import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.MastershipRole;
 import org.onlab.onos.net.Port;
 import org.onlab.onos.net.PortNumber;
 import org.onlab.onos.net.device.DeviceClockService;
@@ -89,6 +92,7 @@
 import static org.onlab.util.Tools.namedThreads;
 import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
 import static org.onlab.onos.store.device.impl.GossipDeviceStoreMessageSubjects.DEVICE_ADVERTISE;
+import static org.onlab.onos.store.device.impl.GossipDeviceStoreMessageSubjects.DEVICE_REMOVE_REQ;
 
 // TODO: give me a better name
 /**
@@ -160,6 +164,7 @@
                 GossipDeviceStoreMessageSubjects.DEVICE_UPDATE, new InternalDeviceEventListener());
         clusterCommunicator.addSubscriber(
                 GossipDeviceStoreMessageSubjects.DEVICE_OFFLINE, new InternalDeviceOfflineEventListener());
+        clusterCommunicator.addSubscriber(DEVICE_REMOVE_REQ, new InternalRemoveRequestListener());
         clusterCommunicator.addSubscriber(
                 GossipDeviceStoreMessageSubjects.DEVICE_REMOVED, new InternalDeviceRemovedEventListener());
         clusterCommunicator.addSubscriber(
@@ -715,14 +720,48 @@
 
     @Override
     public synchronized DeviceEvent removeDevice(DeviceId deviceId) {
-        final NodeId master = mastershipService.getMasterFor(deviceId);
-        if (!clusterService.getLocalNode().id().equals(master)) {
-            log.info("Removal of device {} requested on non master node", deviceId);
-            // FIXME silently ignoring. Should be forwarding or broadcasting to
-            // master.
-            return null;
+        final NodeId myId = clusterService.getLocalNode().id();
+        NodeId master = mastershipService.getMasterFor(deviceId);
+
+        // if there exist a master, forward
+        // if there is no master, try to become one and process
+
+        boolean relinquishAtEnd = false;
+        if (master == null) {
+            final MastershipRole myRole = mastershipService.getLocalRole(deviceId);
+            if (myRole != MastershipRole.NONE) {
+                relinquishAtEnd = true;
+            }
+            log.info("Temporarlily requesting role for {} to remove", deviceId);
+            mastershipService.requestRoleFor(deviceId);
+            MastershipTermService termService = mastershipService.requestTermService();
+            MastershipTerm term = termService.getMastershipTerm(deviceId);
+            if (myId.equals(term.master())) {
+                master = myId;
+            }
         }
 
+        if (!myId.equals(master)) {
+            log.info("{} has control of {}, forwarding remove request",
+                     master, deviceId);
+
+             ClusterMessage message = new ClusterMessage(
+                     myId,
+                     DEVICE_REMOVE_REQ,
+                     SERIALIZER.encode(deviceId));
+
+             try {
+                 clusterCommunicator.unicast(message, master);
+             } catch (IOException e) {
+                 log.error("Failed to forward {} remove request to {}", deviceId, master, e);
+             }
+
+             // event will be triggered after master processes it.
+             return null;
+        }
+
+        // I have control..
+
         Timestamp timestamp = deviceClockService.getTimestamp(deviceId);
         DeviceEvent event = removeDeviceInternal(deviceId, timestamp);
         if (event != null) {
@@ -735,6 +774,10 @@
                      deviceId);
             }
         }
+        if (relinquishAtEnd) {
+            log.info("Relinquishing temporary role acquired for {}", deviceId);
+            mastershipService.relinquishMastership(deviceId);
+        }
         return event;
     }
 
@@ -1241,6 +1284,16 @@
         }
     }
 
+    private final class InternalRemoveRequestListener
+            implements ClusterMessageHandler {
+        @Override
+        public void handle(ClusterMessage message) {
+            log.debug("Received device remove request from peer: {}", message.sender());
+            DeviceId did = SERIALIZER.decode(message.payload());
+            removeDevice(did);
+        }
+    }
+
     private class InternalDeviceRemovedEventListener implements ClusterMessageHandler {
         @Override
         public void handle(ClusterMessage message) {
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStoreMessageSubjects.java b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStoreMessageSubjects.java
index 89577a8..4c9e48c 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStoreMessageSubjects.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/device/impl/GossipDeviceStoreMessageSubjects.java
@@ -27,6 +27,7 @@
 
     public static final MessageSubject DEVICE_UPDATE = new MessageSubject("peer-device-update");
     public static final MessageSubject DEVICE_OFFLINE = new MessageSubject("peer-device-offline");
+    public static final MessageSubject DEVICE_REMOVE_REQ = new MessageSubject("peer-device-remove-request");
     public static final MessageSubject DEVICE_REMOVED = new MessageSubject("peer-device-removed");
     public static final MessageSubject PORT_UPDATE = new MessageSubject("peer-port-update");
     public static final MessageSubject PORT_STATUS_UPDATE = new MessageSubject("peer-port-status-update");
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
index ca3b29c..81f2bfd 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/DistributedFlowRuleStore.java
@@ -106,10 +106,11 @@
 
     private final Logger log = getLogger(getClass());
 
+    // primary data:
+    //  read/write needs to be synchronized
     // store entries as a pile of rules, no info about device tables
-    private final Multimap<DeviceId, StoredFlowEntry> flowEntries =
-            ArrayListMultimap.<DeviceId, StoredFlowEntry>create();
-
+    private final Multimap<DeviceId, StoredFlowEntry> flowEntries
+        = ArrayListMultimap.<DeviceId, StoredFlowEntry>create();
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected ReplicaInfoService replicaInfoManager;
@@ -135,6 +136,7 @@
                 //.removalListener(listener)
                 .build();
 
+    // Cache of SMaps used for backup data.  each SMap contain device flow table
     private LoadingCache<DeviceId, SMap<FlowId, ImmutableList<StoredFlowEntry>>> smaps;
 
 
@@ -311,7 +313,7 @@
         }
     }
 
-    private Set<FlowEntry> getFlowEntriesInternal(DeviceId deviceId) {
+    private synchronized Set<FlowEntry> getFlowEntriesInternal(DeviceId deviceId) {
         Collection<? extends FlowEntry> rules = flowEntries.get(deviceId);
         if (rules == null) {
             return Collections.emptySet();
@@ -365,7 +367,9 @@
         }
     }
 
-    private ListenableFuture<CompletedBatchOperation> storeBatchInternal(FlowRuleBatchOperation operation) {
+    private synchronized ListenableFuture<CompletedBatchOperation>
+                        storeBatchInternal(FlowRuleBatchOperation operation) {
+
         final List<StoredFlowEntry> toRemove = new ArrayList<>();
         final List<StoredFlowEntry> toAdd = new ArrayList<>();
         DeviceId did = null;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java
index 8029e27..30a73e0 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/host/impl/GossipHostStore.java
@@ -15,12 +15,25 @@
  */
 package org.onlab.onos.store.host.impl;
 
-import com.google.common.collect.FluentIterable;
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
+import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
+import static org.onlab.onos.cluster.ControllerNodeToNodeId.toNodeId;
+import static org.onlab.onos.net.host.HostEvent.Type.HOST_ADDED;
+import static org.onlab.onos.net.host.HostEvent.Type.HOST_MOVED;
+import static org.onlab.onos.net.host.HostEvent.Type.HOST_REMOVED;
+import static org.onlab.onos.net.host.HostEvent.Type.HOST_UPDATED;
+import static org.onlab.util.Tools.namedThreads;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
 
 import org.apache.commons.lang3.RandomUtils;
 import org.apache.felix.scr.annotations.Activate;
@@ -45,7 +58,6 @@
 import org.onlab.onos.net.host.HostEvent;
 import org.onlab.onos.net.host.HostStore;
 import org.onlab.onos.net.host.HostStoreDelegate;
-import org.onlab.onos.net.host.InterfaceIpAddress;
 import org.onlab.onos.net.host.PortAddresses;
 import org.onlab.onos.net.provider.ProviderId;
 import org.onlab.onos.store.AbstractStore;
@@ -63,21 +75,13 @@
 import org.onlab.util.KryoNamespace;
 import org.slf4j.Logger;
 
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.Map.Entry;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-
-import static java.util.concurrent.Executors.newSingleThreadScheduledExecutor;
-import static org.onlab.onos.cluster.ControllerNodeToNodeId.toNodeId;
-import static org.onlab.onos.net.host.HostEvent.Type.*;
-import static org.onlab.util.Tools.namedThreads;
-import static org.slf4j.LoggerFactory.getLogger;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.SetMultimap;
 
 //TODO: multi-provider, annotation not supported.
 /**
@@ -100,8 +104,9 @@
     // Hosts tracked by their location
     private final Multimap<ConnectPoint, Host> locations = HashMultimap.create();
 
-    private final Map<ConnectPoint, PortAddresses> portAddresses =
-            new ConcurrentHashMap<>();
+    private final SetMultimap<ConnectPoint, PortAddresses> portAddresses =
+            Multimaps.synchronizedSetMultimap(
+                    HashMultimap.<ConnectPoint, PortAddresses>create());
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected HostClockService hostClockService;
@@ -343,77 +348,37 @@
 
     @Override
     public void updateAddressBindings(PortAddresses addresses) {
-        synchronized (portAddresses) {
-            PortAddresses existing = portAddresses.get(addresses.connectPoint());
-            if (existing == null) {
-                portAddresses.put(addresses.connectPoint(), addresses);
-            } else {
-                Set<InterfaceIpAddress> union =
-                    Sets.union(existing.ipAddresses(),
-                               addresses.ipAddresses()).immutableCopy();
-
-                MacAddress newMac = (addresses.mac() == null) ? existing.mac()
-                        : addresses.mac();
-
-                PortAddresses newAddresses =
-                        new PortAddresses(addresses.connectPoint(), union, newMac);
-
-                portAddresses.put(newAddresses.connectPoint(), newAddresses);
-            }
-        }
+        portAddresses.put(addresses.connectPoint(), addresses);
     }
 
     @Override
     public void removeAddressBindings(PortAddresses addresses) {
-        synchronized (portAddresses) {
-            PortAddresses existing = portAddresses.get(addresses.connectPoint());
-            if (existing != null) {
-                Set<InterfaceIpAddress> difference =
-                    Sets.difference(existing.ipAddresses(),
-                                    addresses.ipAddresses()).immutableCopy();
-
-                // If they removed the existing mac, set the new mac to null.
-                // Otherwise, keep the existing mac.
-                MacAddress newMac = existing.mac();
-                if (addresses.mac() != null && addresses.mac().equals(existing.mac())) {
-                    newMac = null;
-                }
-
-                PortAddresses newAddresses =
-                        new PortAddresses(addresses.connectPoint(), difference, newMac);
-
-                portAddresses.put(newAddresses.connectPoint(), newAddresses);
-            }
-        }
+        portAddresses.remove(addresses.connectPoint(), addresses);
     }
 
     @Override
     public void clearAddressBindings(ConnectPoint connectPoint) {
-        synchronized (portAddresses) {
-            portAddresses.remove(connectPoint);
-        }
+        portAddresses.removeAll(connectPoint);
     }
 
     @Override
     public Set<PortAddresses> getAddressBindings() {
         synchronized (portAddresses) {
-            return new HashSet<>(portAddresses.values());
+            return ImmutableSet.copyOf(portAddresses.values());
         }
     }
 
     @Override
-    public PortAddresses getAddressBindingsForPort(ConnectPoint connectPoint) {
-        PortAddresses addresses;
-
+    public Set<PortAddresses> getAddressBindingsForPort(ConnectPoint connectPoint) {
         synchronized (portAddresses) {
-            addresses = portAddresses.get(connectPoint);
-        }
+            Set<PortAddresses> addresses = portAddresses.get(connectPoint);
 
-        if (addresses == null) {
-            addresses = new PortAddresses(connectPoint, null, null);
+            if (addresses == null) {
+                return Collections.emptySet();
+            } else {
+                return ImmutableSet.copyOf(addresses);
+            }
         }
-
-        return addresses;
     }
 
     // Auxiliary extension to allow location to mutate.
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java
index ab5cc8a..351581d 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/link/impl/GossipLinkStore.java
@@ -122,7 +122,7 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected ClusterService clusterService;
 
-    private static final KryoSerializer SERIALIZER = new KryoSerializer() {
+    protected static final KryoSerializer SERIALIZER = new KryoSerializer() {
         @Override
         protected void setupKryoPool() {
             serializerPool = KryoNamespace.newBuilder()
@@ -377,6 +377,7 @@
         try {
             timestamp = deviceClockService.getTimestamp(dstDeviceId);
         } catch (IllegalStateException e) {
+            log.warn("Failed to remove link {}, was not the master", key);
             //there are times when this is called before mastership
             // handoff correctly completes.
             return null;
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
index c9d4422..42e0799 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
@@ -447,7 +447,16 @@
             RoleValue oldValue = event.getOldValue();
             RoleValue newValue = event.getValue();
 
-            if (Objects.equal(oldValue.get(MASTER), newValue.get(MASTER))) {
+            // There will be no oldValue at the very first instance of an EntryEvent.
+            // Technically, the progression is: null event -> null master -> some master;
+            // We say a null master and a null oldValue are the same condition.
+            NodeId oldMaster = null;
+            if (oldValue != null) {
+                oldMaster = oldValue.get(MASTER);
+            }
+            NodeId newMaster = newValue.get(MASTER);
+
+            if (!Objects.equal(oldMaster, newMaster)) {
                 notifyDelegate(new MastershipEvent(
                         MASTER_CHANGED, event.getKey(), event.getValue().roleInfo()));
             } else {
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/RoleValue.java b/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/RoleValue.java
index aa5d911..a074b94 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/RoleValue.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/mastership/impl/RoleValue.java
@@ -31,6 +31,7 @@
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.collect.Lists;
 
 /**
  * A structure that holds node mastership roles associated with a
@@ -40,12 +41,26 @@
 
     protected final Map<MastershipRole, List<NodeId>> value = new EnumMap<>(MastershipRole.class);
 
+    /**
+     * Constructs empty RoleValue.
+     */
     public RoleValue() {
         value.put(MastershipRole.MASTER, new LinkedList<NodeId>());
         value.put(MastershipRole.STANDBY, new LinkedList<NodeId>());
         value.put(MastershipRole.NONE, new LinkedList<NodeId>());
     }
 
+    /**
+     * Constructs copy of specified RoleValue.
+     *
+     * @param original original to create copy from
+     */
+    public RoleValue(final RoleValue original) {
+        value.put(MASTER, Lists.newLinkedList(original.value.get(MASTER)));
+        value.put(STANDBY, Lists.newLinkedList(original.value.get(STANDBY)));
+        value.put(NONE, Lists.newLinkedList(original.value.get(NONE)));
+    }
+
     // exposing internals for serialization purpose only
     Map<MastershipRole, List<NodeId>> value() {
         return Collections.unmodifiableMap(value);
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/resource/impl/DistributedLinkResourceStore.java b/core/store/dist/src/main/java/org/onlab/onos/store/resource/impl/DistributedLinkResourceStore.java
index 41172cd..ecccd2b 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/resource/impl/DistributedLinkResourceStore.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/resource/impl/DistributedLinkResourceStore.java
@@ -195,7 +195,7 @@
     @Override
     public void releaseResources(LinkResourceAllocations allocations) {
         checkNotNull(allocations);
-        linkResourceAllocationsMap.remove(allocations);
+        linkResourceAllocationsMap.remove(allocations.intendId());
         for (Link link : allocations.links()) {
             addFreeResources(link, allocations);
             Set<LinkResourceAllocations> linkAllocs = allocatedResources.get(link);
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/DatabaseAdminService.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/DatabaseAdminService.java
new file mode 100644
index 0000000..3ec4e9b
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/DatabaseAdminService.java
@@ -0,0 +1,35 @@
+package org.onlab.onos.store.service;
+
+import java.util.List;
+
+/**
+ * Service interface for running administrative tasks on a Database.
+ */
+public interface DatabaseAdminService {
+
+    /**
+     * Creates a new table.
+     * Table creation is idempotent. Attempting to create a table
+     * that already exists will be a noop.
+     * @param name table name.
+     * @return true if the table was created by this call, false otherwise.
+     */
+    public boolean createTable(String name);
+
+    /**
+     * Lists all the tables in the database.
+     * @return list of table names.
+     */
+    public List<String> listTables();
+
+    /**
+     * Deletes a table from the database.
+     * @param name name of the table to delete.
+     */
+    public void dropTable(String name);
+
+    /**
+     * Deletes all tables from the database.
+     */
+    public void dropAllTables();
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/DatabaseException.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/DatabaseException.java
new file mode 100644
index 0000000..bbc2daf
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/DatabaseException.java
@@ -0,0 +1,22 @@
+package org.onlab.onos.store.service;
+
+/**
+ * Base exception type for database failures.
+ */
+@SuppressWarnings("serial")
+public class DatabaseException extends RuntimeException {
+    public DatabaseException(String message, Throwable t) {
+        super(message, t);
+    }
+
+    public DatabaseException(String message) {
+        super(message);
+    }
+
+    public DatabaseException(Throwable t) {
+        super(t);
+    }
+
+    public DatabaseException() {
+    };
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/DatabaseService.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/DatabaseService.java
new file mode 100644
index 0000000..11cc5ff
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/DatabaseService.java
@@ -0,0 +1,43 @@
+package org.onlab.onos.store.service;
+
+import java.util.List;
+
+/**
+ * Service interface for a strongly consistent and durable
+ * key value data store.
+ */
+public interface DatabaseService {
+
+    /**
+     * Performs a read on the database.
+     * @param request read request.
+     * @return ReadResult
+     * @throws DatabaseException if there is a failure in executing read.
+     */
+    ReadResult read(ReadRequest request);
+
+    /**
+     * Performs a batch read operation on the database.
+     * The main advantage of batch read operation is parallelization.
+     * @param batch batch of read requests to execute.
+     * @return batch read result.
+     */
+    List<OptionalResult<ReadResult, DatabaseException>> batchRead(List<ReadRequest> batch);
+
+    /**
+     * Performs a write operation on the database.
+     * @param request write request
+     * @return write result.
+     * @throws DatabaseException if there is failure in execution write.
+     */
+    WriteResult write(WriteRequest request);
+
+    /**
+     * Performs a batch write operation on the database.
+     * Batch write provides transactional semantics. Either all operations
+     * succeed or none of them do.
+     * @param batch batch of write requests to execute as a transaction.
+     * @return result of executing the batch write operation.
+     */
+    List<OptionalResult<WriteResult, DatabaseException>> batchWrite(List<WriteRequest> batch);
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/NoSuchTableException.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/NoSuchTableException.java
new file mode 100644
index 0000000..6e02252
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/NoSuchTableException.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.store.service;
+
+
+/**
+ * Exception thrown when an operation (read or write) is requested for
+ * a table that does not exist.
+ */
+@SuppressWarnings("serial")
+public class NoSuchTableException extends DatabaseException {
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/OptimisticLockException.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/OptimisticLockException.java
new file mode 100644
index 0000000..090eb63
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/OptimisticLockException.java
@@ -0,0 +1,8 @@
+package org.onlab.onos.store.service;
+
+/**
+ * Exception that indicates a optimistic lock failure.
+ */
+@SuppressWarnings("serial")
+public class OptimisticLockException extends PreconditionFailedException {
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/OptionalResult.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/OptionalResult.java
new file mode 100644
index 0000000..a9341cb
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/OptionalResult.java
@@ -0,0 +1,26 @@
+package org.onlab.onos.store.service;
+
+/**
+ * A container object which either has a result or an exception.
+ * <p>
+ * If a result is present, get() will return it otherwise get() will throw
+ * the exception that was encountered in the process of generating the result.
+ * </p>
+ * @param <R> type of result.
+ * @param <E> exception encountered in generating the result.
+ */
+public interface OptionalResult<R, E extends Throwable> {
+
+    /**
+     * Returns the result or throws an exception if there is no
+     * valid result.
+     * @return result
+     */
+    public R get();
+
+    /**
+     * Returns true if there is a valid result.
+     * @return true is yes, false otherwise.
+     */
+    public boolean hasValidResult();
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/PreconditionFailedException.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/PreconditionFailedException.java
new file mode 100644
index 0000000..9eda528
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/PreconditionFailedException.java
@@ -0,0 +1,16 @@
+package org.onlab.onos.store.service;
+
+
+/**
+ * Exception that indicates a precondition failure.
+ * Scenarios that can cause this exception:
+ * <ul>
+ * <li>An operation that attempts to write a new value iff the current value is equal
+ * to some specified value.</li>
+ * <li>An operation that attempts to write a new value iff the current version
+ * matches a specified value</li>
+ * </ul>
+ */
+@SuppressWarnings("serial")
+public class PreconditionFailedException extends DatabaseException {
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/ReadRequest.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/ReadRequest.java
new file mode 100644
index 0000000..a22464a
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/ReadRequest.java
@@ -0,0 +1,41 @@
+package org.onlab.onos.store.service;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Database read request.
+ */
+public class ReadRequest {
+
+    private final String tableName;
+    private final String key;
+
+    public ReadRequest(String tableName, String key) {
+        this.tableName = tableName;
+        this.key = key;
+    }
+
+    /**
+     * Return the name of the table.
+     * @return table name.
+     */
+    public String tableName() {
+        return tableName;
+    }
+
+    /**
+     * Returns the key.
+     * @return key.
+     */
+    public String key() {
+        return key;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("tableName", tableName)
+                .add("key", key)
+                .toString();
+    }
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/ReadResult.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/ReadResult.java
new file mode 100644
index 0000000..6d28fc2
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/ReadResult.java
@@ -0,0 +1,53 @@
+package org.onlab.onos.store.service;
+
+import com.google.common.base.MoreObjects;
+
+
+/**
+ * Database read result.
+ */
+public class ReadResult {
+
+    private final String tableName;
+    private final String key;
+    private final VersionedValue value;
+
+    public ReadResult(String tableName, String key, VersionedValue value) {
+        this.tableName = tableName;
+        this.key = key;
+        this.value = value;
+    }
+
+    /**
+     * Returns database table name.
+     * @return table name
+     */
+    public String tableName() {
+        return tableName;
+    }
+
+    /**
+     * Returns database table key.
+     * @return key
+     */
+    public String key() {
+        return key;
+    }
+
+    /**
+     * Returns value associated with the key.
+     * @return non-null value if the table contains one, null otherwise.
+     */
+    public VersionedValue value() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("tableName", tableName)
+                .add("key", key)
+                .add("value", value)
+                .toString();
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/VersionedValue.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/VersionedValue.java
new file mode 100644
index 0000000..852fb07
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/VersionedValue.java
@@ -0,0 +1,48 @@
+package org.onlab.onos.store.service;
+
+import java.util.Arrays;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Wrapper object that holds the object (as byte array) and its version.
+ */
+public class VersionedValue {
+
+    private final byte[] value;
+    private final long version;
+
+    /**
+     * Creates a new instance with the specified value and version.
+     * @param value value
+     * @param version version
+     */
+    public VersionedValue(byte[] value, long version) {
+        this.value = value;
+        this.version = version;
+    }
+
+    /**
+     * Returns the value.
+     * @return value.
+     */
+    public byte[] value() {
+        return value;
+    }
+
+    /**
+     * Returns the version.
+     * @return version.
+     */
+    public long version() {
+        return version;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("version", version)
+                .add("value", Arrays.toString(value))
+                .toString();
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/WriteAborted.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/WriteAborted.java
new file mode 100644
index 0000000..a2ebd2a
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/WriteAborted.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.store.service;
+
+
+/**
+ * Exception that indicates a write operation is aborted.
+ * Aborted operations do not mutate database state is any form.
+ */
+@SuppressWarnings("serial")
+public class WriteAborted extends DatabaseException {
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/WriteRequest.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/WriteRequest.java
new file mode 100644
index 0000000..99f73c1
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/WriteRequest.java
@@ -0,0 +1,102 @@
+package org.onlab.onos.store.service;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.util.Objects;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Database write request.
+ */
+public class WriteRequest {
+
+    private final String tableName;
+    private final String key;
+    private final byte[] newValue;
+    private final long previousVersion;
+    private final byte[] oldValue;
+
+    // put regardless of previous value
+    public WriteRequest(String tableName, String key, byte[] newValue) {
+        this(tableName, key, newValue, -1, null);
+    }
+
+    // put if version matches
+    public WriteRequest(String tableName, String key, byte[] newValue, long previousVersion) {
+        this(tableName, key, newValue, previousVersion, null);
+        checkArgument(previousVersion >= 0);
+    }
+
+    // put if value matches
+    public WriteRequest(String tableName, String key, byte[] newValue, byte[] oldValue) {
+        this(tableName, key, newValue, -1, oldValue);
+    }
+
+    // hidden constructor
+    private WriteRequest(String tableName, String key, byte[] newValue, long previousVersion, byte[] oldValue) {
+
+        checkArgument(tableName != null);
+        checkArgument(key != null);
+        checkArgument(newValue != null);
+
+        this.tableName = tableName;
+        this.key = key;
+        this.newValue = newValue;
+        this.previousVersion = previousVersion;
+        this.oldValue = oldValue;
+    }
+
+    public String tableName() {
+        return tableName;
+    }
+
+    public String key() {
+        return key;
+    }
+
+    public byte[] newValue() {
+        return newValue;
+    }
+
+    public long previousVersion() {
+        return previousVersion;
+    }
+
+    public byte[] oldValue() {
+        return oldValue;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("tableName", tableName)
+                .add("key", key)
+                .add("newValue", newValue)
+                .add("previousVersion", previousVersion)
+                .add("oldValue", oldValue)
+                .toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(key, tableName, previousVersion);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        WriteRequest other = (WriteRequest) obj;
+        return Objects.equals(this.key, other.key) &&
+                Objects.equals(this.tableName, other.tableName) &&
+                Objects.equals(this.previousVersion, other.previousVersion);
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/WriteResult.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/WriteResult.java
new file mode 100644
index 0000000..aec3046
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/WriteResult.java
@@ -0,0 +1,41 @@
+package org.onlab.onos.store.service;
+
+import com.google.common.base.MoreObjects;
+
+
+/**
+ * Database write result.
+ */
+public class WriteResult {
+
+    private final String tableName;
+    private final String key;
+    private final VersionedValue previousValue;
+
+    public WriteResult(String tableName, String key, VersionedValue previousValue) {
+        this.tableName = tableName;
+        this.key = key;
+        this.previousValue = previousValue;
+    }
+
+    public String tableName() {
+        return tableName;
+    }
+
+    public String key() {
+        return key;
+    }
+
+    public VersionedValue previousValue() {
+        return previousValue;
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("tableName", tableName)
+                .add("key", key)
+                .add("previousValue", previousValue)
+                .toString();
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocol.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocol.java
new file mode 100644
index 0000000..6de66bc
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocol.java
@@ -0,0 +1,180 @@
+package org.onlab.onos.store.service.impl;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Vector;
+
+import net.kuujo.copycat.cluster.TcpClusterConfig;
+import net.kuujo.copycat.cluster.TcpMember;
+import net.kuujo.copycat.internal.log.ConfigurationEntry;
+import net.kuujo.copycat.internal.log.CopycatEntry;
+import net.kuujo.copycat.internal.log.OperationEntry;
+import net.kuujo.copycat.internal.log.SnapshotEntry;
+import net.kuujo.copycat.protocol.PingRequest;
+import net.kuujo.copycat.protocol.PingResponse;
+import net.kuujo.copycat.protocol.PollRequest;
+import net.kuujo.copycat.protocol.PollResponse;
+import net.kuujo.copycat.protocol.Response.Status;
+import net.kuujo.copycat.protocol.SubmitRequest;
+import net.kuujo.copycat.protocol.SubmitResponse;
+import net.kuujo.copycat.protocol.SyncRequest;
+import net.kuujo.copycat.protocol.SyncResponse;
+import net.kuujo.copycat.spi.protocol.Protocol;
+import net.kuujo.copycat.spi.protocol.ProtocolClient;
+import net.kuujo.copycat.spi.protocol.ProtocolServer;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.onos.cluster.ClusterService;
+import org.onlab.onos.cluster.ControllerNode;
+import org.onlab.onos.store.cluster.messaging.ClusterCommunicationService;
+import org.onlab.onos.store.cluster.messaging.MessageSubject;
+import org.onlab.onos.store.serializers.ImmutableListSerializer;
+import org.onlab.onos.store.serializers.ImmutableMapSerializer;
+import org.onlab.onos.store.serializers.ImmutableSetSerializer;
+import org.onlab.onos.store.serializers.KryoSerializer;
+import org.onlab.onos.store.service.ReadRequest;
+import org.onlab.onos.store.service.ReadResult;
+import org.onlab.onos.store.service.VersionedValue;
+import org.onlab.onos.store.service.WriteRequest;
+import org.onlab.onos.store.service.WriteResult;
+import org.onlab.util.KryoNamespace;
+import org.slf4j.Logger;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.io.Input;
+import com.esotericsoftware.kryo.serializers.CollectionSerializer;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+/**
+ * ONOS Cluster messaging based Copycat protocol.
+ */
+@Component(immediate = true)
+@Service
+public class ClusterMessagingProtocol implements Protocol<TcpMember> {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterCommunicationService clusterCommunicator;
+
+    public static final MessageSubject COPYCAT_PING =
+            new MessageSubject("copycat-raft-consensus-ping");
+    public static final MessageSubject COPYCAT_SYNC =
+            new MessageSubject("copycat-raft-consensus-sync");
+    public static final MessageSubject COPYCAT_POLL =
+            new MessageSubject("copycat-raft-consensus-poll");
+    public static final MessageSubject COPYCAT_SUBMIT =
+            new MessageSubject("copycat-raft-consensus-submit");
+
+    private static final KryoNamespace COPYCAT = KryoNamespace.newBuilder()
+            .register(PingRequest.class)
+            .register(PingResponse.class)
+            .register(PollRequest.class)
+            .register(PollResponse.class)
+            .register(SyncRequest.class)
+            .register(SyncResponse.class)
+            .register(SubmitRequest.class)
+            .register(SubmitResponse.class)
+            .register(Status.class)
+            .register(ConfigurationEntry.class)
+            .register(SnapshotEntry.class)
+            .register(CopycatEntry.class)
+            .register(OperationEntry.class)
+            .register(TcpClusterConfig.class)
+            .register(TcpMember.class)
+            .build();
+
+    private static final KryoNamespace DATABASE = KryoNamespace.newBuilder()
+            .register(ReadRequest.class)
+            .register(WriteRequest.class)
+            .register(InternalReadResult.class)
+            .register(InternalWriteResult.class)
+            .register(InternalReadResult.Status.class)
+            .register(WriteResult.class)
+            .register(ReadResult.class)
+            .register(InternalWriteResult.Status.class)
+            .register(VersionedValue.class)
+            .build();
+
+    public static final KryoNamespace COMMON = KryoNamespace.newBuilder()
+            .register(Arrays.asList().getClass(), new CollectionSerializer() {
+                @Override
+                @SuppressWarnings("rawtypes")
+                protected Collection<?> create(Kryo kryo, Input input, Class<Collection> type) {
+                    return new ArrayList();
+                }
+            })
+            .register(ImmutableMap.class, new ImmutableMapSerializer())
+            .register(ImmutableList.class, new ImmutableListSerializer())
+            .register(ImmutableSet.class, new ImmutableSetSerializer())
+            .register(
+                    Vector.class,
+                    ArrayList.class,
+                    Arrays.asList().getClass(),
+                    HashMap.class,
+                    HashSet.class,
+                    LinkedList.class,
+                    byte[].class)
+            .build();
+
+    public static final KryoSerializer SERIALIZER = new KryoSerializer() {
+        @Override
+        protected void setupKryoPool() {
+            serializerPool = KryoNamespace.newBuilder()
+                    .register(COPYCAT)
+                    .register(COMMON)
+                    .register(DATABASE)
+                    .build()
+                    .populate(1);
+        }
+    };
+
+    @Activate
+    public void activate() {
+        log.info("Started.");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        log.info("Stopped.");
+    }
+
+    @Override
+    public ProtocolServer createServer(TcpMember member) {
+        return new ClusterMessagingProtocolServer(clusterCommunicator);
+    }
+
+    @Override
+    public ProtocolClient createClient(TcpMember member) {
+        ControllerNode node = getControllerNode(member.host(), member.port());
+        checkNotNull(node, "A valid controller node is expected");
+        return new ClusterMessagingProtocolClient(
+                clusterCommunicator, node);
+    }
+
+    private ControllerNode getControllerNode(String host, int port) {
+        for (ControllerNode node : clusterService.getNodes()) {
+            if (node.ip().toString().equals(host) && node.tcpPort() == port) {
+                return node;
+            }
+        }
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolClient.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolClient.java
new file mode 100644
index 0000000..0c2aacd
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolClient.java
@@ -0,0 +1,149 @@
+package org.onlab.onos.store.service.impl;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.io.IOException;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import net.kuujo.copycat.protocol.PingRequest;
+import net.kuujo.copycat.protocol.PingResponse;
+import net.kuujo.copycat.protocol.PollRequest;
+import net.kuujo.copycat.protocol.PollResponse;
+import net.kuujo.copycat.protocol.SubmitRequest;
+import net.kuujo.copycat.protocol.SubmitResponse;
+import net.kuujo.copycat.protocol.SyncRequest;
+import net.kuujo.copycat.protocol.SyncResponse;
+import net.kuujo.copycat.spi.protocol.ProtocolClient;
+
+import org.onlab.onos.cluster.ControllerNode;
+import org.onlab.onos.store.cluster.messaging.ClusterCommunicationService;
+import org.onlab.onos.store.cluster.messaging.ClusterMessage;
+import org.onlab.onos.store.cluster.messaging.MessageSubject;
+import org.slf4j.Logger;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+
+/**
+ * ONOS Cluster messaging based Copycat protocol client.
+ */
+public class ClusterMessagingProtocolClient implements ProtocolClient {
+
+    private final Logger log = getLogger(getClass());
+
+    private static final ThreadFactory THREAD_FACTORY =
+            new ThreadFactoryBuilder().setNameFormat("copycat-netty-messaging-%d").build();
+
+    public static final long RETRY_INTERVAL_MILLIS = 2000;
+
+    private final ClusterCommunicationService clusterCommunicator;
+    private final ControllerNode remoteNode;
+
+    // FIXME: Thread pool sizing.
+    private static final ScheduledExecutorService THREAD_POOL =
+            new ScheduledThreadPoolExecutor(10, THREAD_FACTORY);
+
+    public ClusterMessagingProtocolClient(
+            ClusterCommunicationService clusterCommunicator,
+            ControllerNode remoteNode) {
+        this.clusterCommunicator = clusterCommunicator;
+        this.remoteNode = remoteNode;
+    }
+
+    @Override
+    public CompletableFuture<PingResponse> ping(PingRequest request) {
+        return requestReply(request);
+    }
+
+    @Override
+    public CompletableFuture<SyncResponse> sync(SyncRequest request) {
+        return requestReply(request);
+    }
+
+    @Override
+    public CompletableFuture<PollResponse> poll(PollRequest request) {
+        return requestReply(request);
+    }
+
+    @Override
+    public CompletableFuture<SubmitResponse> submit(SubmitRequest request) {
+        return requestReply(request);
+    }
+
+    @Override
+    public CompletableFuture<Void> connect() {
+        return CompletableFuture.completedFuture(null);
+    }
+
+    @Override
+    public CompletableFuture<Void> close() {
+        return CompletableFuture.completedFuture(null);
+    }
+
+    public <I> MessageSubject messageType(I input) {
+        Class<?> clazz = input.getClass();
+        if (clazz.equals(PollRequest.class)) {
+            return ClusterMessagingProtocol.COPYCAT_POLL;
+        } else if (clazz.equals(SyncRequest.class)) {
+            return ClusterMessagingProtocol.COPYCAT_SYNC;
+        } else if (clazz.equals(SubmitRequest.class)) {
+            return ClusterMessagingProtocol.COPYCAT_SUBMIT;
+        } else if (clazz.equals(PingRequest.class)) {
+            return ClusterMessagingProtocol.COPYCAT_PING;
+        } else {
+            throw new IllegalArgumentException("Unknown class " + clazz.getName());
+        }
+
+    }
+
+    private <I, O> CompletableFuture<O> requestReply(I request) {
+        CompletableFuture<O> future = new CompletableFuture<>();
+        THREAD_POOL.schedule(new RPCTask<I, O>(request, future), 0, TimeUnit.MILLISECONDS);
+        return future;
+    }
+
+    private class RPCTask<I, O> implements Runnable {
+
+        private final ClusterMessage message;
+        private final CompletableFuture<O> future;
+
+        public RPCTask(I request, CompletableFuture<O> future) {
+            this.message =
+                    new ClusterMessage(
+                            null,
+                            messageType(request),
+                            ClusterMessagingProtocol.SERIALIZER.encode(request));
+            this.future = future;
+        }
+
+        @Override
+        public void run() {
+            try {
+                byte[] response = clusterCommunicator
+                    .sendAndReceive(message, remoteNode.id())
+                    .get(RETRY_INTERVAL_MILLIS, TimeUnit.MILLISECONDS);
+                future.complete(ClusterMessagingProtocol.SERIALIZER.decode(response));
+
+            } catch (IOException | InterruptedException | ExecutionException | TimeoutException e) {
+                if (message.subject().equals(ClusterMessagingProtocol.COPYCAT_SYNC) ||
+                        message.subject().equals(ClusterMessagingProtocol.COPYCAT_PING)) {
+                    log.warn("Request to {} failed. Will retry "
+                            + "in {} ms", remoteNode, RETRY_INTERVAL_MILLIS);
+                    THREAD_POOL.schedule(
+                            this,
+                            RETRY_INTERVAL_MILLIS,
+                            TimeUnit.MILLISECONDS);
+                } else {
+                    future.completeExceptionally(e);
+                }
+            } catch (Exception e) {
+                future.completeExceptionally(e);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolServer.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolServer.java
new file mode 100644
index 0000000..7d94847
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/ClusterMessagingProtocolServer.java
@@ -0,0 +1,94 @@
+package org.onlab.onos.store.service.impl;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.concurrent.CompletableFuture;
+
+import net.kuujo.copycat.protocol.PingRequest;
+import net.kuujo.copycat.protocol.PollRequest;
+import net.kuujo.copycat.protocol.RequestHandler;
+import net.kuujo.copycat.protocol.SubmitRequest;
+import net.kuujo.copycat.protocol.SyncRequest;
+import net.kuujo.copycat.spi.protocol.ProtocolServer;
+
+import org.onlab.onos.store.cluster.messaging.ClusterCommunicationService;
+import org.onlab.onos.store.cluster.messaging.ClusterMessage;
+import org.onlab.onos.store.cluster.messaging.ClusterMessageHandler;
+import org.slf4j.Logger;
+
+/**
+ * ONOS Cluster messaging based Copycat protocol server.
+ */
+public class ClusterMessagingProtocolServer implements ProtocolServer {
+
+    private final Logger log = getLogger(getClass());
+    private RequestHandler handler;
+
+    public ClusterMessagingProtocolServer(ClusterCommunicationService clusterCommunicator) {
+
+        clusterCommunicator.addSubscriber(
+                ClusterMessagingProtocol.COPYCAT_PING, new CopycatMessageHandler<PingRequest>());
+        clusterCommunicator.addSubscriber(
+                ClusterMessagingProtocol.COPYCAT_SYNC, new CopycatMessageHandler<SyncRequest>());
+        clusterCommunicator.addSubscriber(
+                ClusterMessagingProtocol.COPYCAT_POLL, new CopycatMessageHandler<PollRequest>());
+        clusterCommunicator.addSubscriber(
+                ClusterMessagingProtocol.COPYCAT_SUBMIT, new CopycatMessageHandler<SubmitRequest>());
+    }
+
+    @Override
+    public void requestHandler(RequestHandler handler) {
+        this.handler = handler;
+    }
+
+    @Override
+    public CompletableFuture<Void> listen() {
+        return CompletableFuture.completedFuture(null);
+    }
+
+    @Override
+    public CompletableFuture<Void> close() {
+        return CompletableFuture.completedFuture(null);
+    }
+
+    private class CopycatMessageHandler<T> implements ClusterMessageHandler {
+
+        @Override
+        public void handle(ClusterMessage message) {
+            T request = ClusterMessagingProtocol.SERIALIZER.decode(message.payload());
+            if (request.getClass().equals(PingRequest.class)) {
+                handler.ping((PingRequest) request).whenComplete((response, error) -> {
+                    try {
+                        message.respond(ClusterMessagingProtocol.SERIALIZER.encode(response));
+                    } catch (Exception e) {
+                        log.error("Failed to respond to ping request", e);
+                    }
+                });
+            } else if (request.getClass().equals(PollRequest.class)) {
+                handler.poll((PollRequest) request).whenComplete((response, error) -> {
+                    try {
+                        message.respond(ClusterMessagingProtocol.SERIALIZER.encode(response));
+                    } catch (Exception e) {
+                        log.error("Failed to respond to poll request", e);
+                    }
+                });
+            } else if (request.getClass().equals(SyncRequest.class)) {
+                handler.sync((SyncRequest) request).whenComplete((response, error) -> {
+                    try {
+                        message.respond(ClusterMessagingProtocol.SERIALIZER.encode(response));
+                    } catch (Exception e) {
+                        log.error("Failed to respond to sync request", e);
+                    }
+                });
+            } else if (request.getClass().equals(SubmitRequest.class)) {
+                handler.submit((SubmitRequest) request).whenComplete((response, error) -> {
+                    try {
+                        message.respond(ClusterMessagingProtocol.SERIALIZER.encode(response));
+                    } catch (Exception e) {
+                        log.error("Failed to respond to submit request", e);
+                    }
+                });
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseClient.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseClient.java
new file mode 100644
index 0000000..8af567e
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseClient.java
@@ -0,0 +1,132 @@
+package org.onlab.onos.store.service.impl;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+import net.kuujo.copycat.protocol.Response.Status;
+import net.kuujo.copycat.protocol.SubmitRequest;
+import net.kuujo.copycat.protocol.SubmitResponse;
+import net.kuujo.copycat.spi.protocol.ProtocolClient;
+
+import org.onlab.onos.store.service.DatabaseException;
+import org.onlab.onos.store.service.ReadRequest;
+import org.onlab.onos.store.service.WriteRequest;
+
+/**
+ * Client for interacting with the Copycat Raft cluster.
+ */
+public class DatabaseClient {
+
+    private final ProtocolClient client;
+
+    public DatabaseClient(ProtocolClient client) {
+        this.client = client;
+    }
+
+    private static String nextId() {
+        return UUID.randomUUID().toString();
+    }
+
+    public boolean createTable(String tableName) {
+
+        SubmitRequest request =
+                new SubmitRequest(
+                        nextId(),
+                        "createTable",
+                        Arrays.asList(tableName));
+        CompletableFuture<SubmitResponse> future = client.submit(request);
+        try {
+            return (boolean) future.get().result();
+        } catch (InterruptedException | ExecutionException e) {
+            throw new DatabaseException(e);
+        }
+    }
+
+    public void dropTable(String tableName) {
+
+        SubmitRequest request =
+                new SubmitRequest(
+                        nextId(),
+                        "dropTable",
+                        Arrays.asList(tableName));
+        CompletableFuture<SubmitResponse> future = client.submit(request);
+        try {
+            if (future.get().status() == Status.OK) {
+                throw new DatabaseException(future.get().toString());
+            }
+
+        } catch (InterruptedException | ExecutionException e) {
+            throw new DatabaseException(e);
+        }
+    }
+
+    public void dropAllTables() {
+
+        SubmitRequest request =
+                new SubmitRequest(
+                        nextId(),
+                        "dropAllTables",
+                        Arrays.asList());
+        CompletableFuture<SubmitResponse> future = client.submit(request);
+        try {
+            if (future.get().status() != Status.OK) {
+                throw new DatabaseException(future.get().toString());
+            }
+        } catch (InterruptedException | ExecutionException e) {
+            throw new DatabaseException(e);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<String> listTables() {
+
+        SubmitRequest request =
+                new SubmitRequest(
+                        nextId(),
+                        "listTables",
+                        Arrays.asList());
+        CompletableFuture<SubmitResponse> future = client.submit(request);
+        try {
+            return (List<String>) future.get().result();
+        } catch (InterruptedException | ExecutionException e) {
+            throw new DatabaseException(e);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<InternalReadResult> batchRead(List<ReadRequest> requests) {
+
+        SubmitRequest request = new SubmitRequest(
+                        nextId(),
+                        "read",
+                        Arrays.asList(requests));
+
+        CompletableFuture<SubmitResponse> future = client.submit(request);
+        try {
+            List<InternalReadResult> internalReadResults = (List<InternalReadResult>) future.get().result();
+            return internalReadResults;
+        } catch (InterruptedException | ExecutionException e) {
+            throw new DatabaseException(e);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<InternalWriteResult> batchWrite(List<WriteRequest> requests) {
+
+        SubmitRequest request = new SubmitRequest(
+                        nextId(),
+                        "write",
+                        Arrays.asList(requests));
+
+        CompletableFuture<SubmitResponse> future = client.submit(request);
+        try {
+            List<InternalWriteResult> internalWriteResults = (List<InternalWriteResult>) future.get().result();
+            return internalWriteResults;
+        } catch (InterruptedException | ExecutionException e) {
+            throw new DatabaseException(e);
+        }
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java
new file mode 100644
index 0000000..7db4bc7
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseManager.java
@@ -0,0 +1,217 @@
+package org.onlab.onos.store.service.impl;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import net.kuujo.copycat.Copycat;
+import net.kuujo.copycat.StateMachine;
+import net.kuujo.copycat.cluster.TcpCluster;
+import net.kuujo.copycat.cluster.TcpClusterConfig;
+import net.kuujo.copycat.cluster.TcpMember;
+import net.kuujo.copycat.log.ChronicleLog;
+import net.kuujo.copycat.log.Log;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
+import org.onlab.onos.cluster.ClusterService;
+import org.onlab.onos.cluster.ControllerNode;
+import org.onlab.onos.store.service.DatabaseAdminService;
+import org.onlab.onos.store.service.DatabaseException;
+import org.onlab.onos.store.service.DatabaseService;
+import org.onlab.onos.store.service.NoSuchTableException;
+import org.onlab.onos.store.service.OptimisticLockException;
+import org.onlab.onos.store.service.OptionalResult;
+import org.onlab.onos.store.service.PreconditionFailedException;
+import org.onlab.onos.store.service.ReadRequest;
+import org.onlab.onos.store.service.ReadResult;
+import org.onlab.onos.store.service.WriteAborted;
+import org.onlab.onos.store.service.WriteRequest;
+import org.onlab.onos.store.service.WriteResult;
+import org.slf4j.Logger;
+
+import com.google.common.collect.Lists;
+
+/**
+ * Strongly consistent and durable state management service based on
+ * Copycat implementation of Raft consensus protocol.
+ */
+@Component(immediate = true)
+@Service
+public class DatabaseManager implements DatabaseService, DatabaseAdminService {
+
+    private final Logger log = getLogger(getClass());
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected ClusterMessagingProtocol copycatMessagingProtocol;
+
+    public static final String LOG_FILE_PREFIX = "onos-copy-cat-log";
+
+    private Copycat copycat;
+    private DatabaseClient client;
+
+    @Activate
+    public void activate() {
+
+        // TODO: Not every node can be part of the consensus ring.
+
+        TcpMember localMember =
+                new TcpMember(
+                        clusterService.getLocalNode().ip().toString(),
+                        clusterService.getLocalNode().tcpPort());
+        List<TcpMember> remoteMembers = Lists.newArrayList();
+
+        for (ControllerNode node : clusterService.getNodes()) {
+            TcpMember member = new TcpMember(node.ip().toString(), node.tcpPort());
+            if (!member.equals(localMember)) {
+                remoteMembers.add(member);
+            }
+        }
+
+        // Configure the cluster.
+        TcpClusterConfig config = new TcpClusterConfig();
+
+        config.setLocalMember(localMember);
+        config.setRemoteMembers(remoteMembers.toArray(new TcpMember[]{}));
+
+        // Create the cluster.
+        TcpCluster cluster = new TcpCluster(config);
+
+        StateMachine stateMachine = new DatabaseStateMachine();
+        ControllerNode thisNode = clusterService.getLocalNode();
+        Log consensusLog = new ChronicleLog(LOG_FILE_PREFIX + "_" + thisNode.id());
+
+        copycat = new Copycat(stateMachine, consensusLog, cluster, copycatMessagingProtocol);
+        copycat.start();
+
+        client = new DatabaseClient(copycatMessagingProtocol.createClient(localMember));
+
+        log.info("Started.");
+    }
+
+    @Deactivate
+    public void deactivate() {
+        copycat.stop();
+        log.info("Stopped.");
+    }
+
+    @Override
+    public boolean createTable(String name) {
+        return client.createTable(name);
+    }
+
+    @Override
+    public void dropTable(String name) {
+        client.dropTable(name);
+    }
+
+    @Override
+    public void dropAllTables() {
+        client.dropAllTables();
+    }
+
+    @Override
+    public List<String> listTables() {
+        return client.listTables();
+    }
+
+    @Override
+    public ReadResult read(ReadRequest request) {
+        return batchRead(Arrays.asList(request)).get(0).get();
+    }
+
+    @Override
+    public List<OptionalResult<ReadResult, DatabaseException>> batchRead(
+            List<ReadRequest> batch) {
+        List<OptionalResult<ReadResult, DatabaseException>> readResults = new ArrayList<>(batch.size());
+        for (InternalReadResult internalReadResult : client.batchRead(batch)) {
+            if (internalReadResult.status() == InternalReadResult.Status.NO_SUCH_TABLE) {
+                readResults.add(new DatabaseOperationResult<ReadResult, DatabaseException>(
+                        new NoSuchTableException()));
+            } else {
+                readResults.add(new DatabaseOperationResult<ReadResult, DatabaseException>(
+                        internalReadResult.result()));
+            }
+        }
+        return readResults;
+    }
+
+    @Override
+    public WriteResult write(WriteRequest request) {
+        return batchWrite(Arrays.asList(request)).get(0).get();
+    }
+
+    @Override
+    public List<OptionalResult<WriteResult, DatabaseException>> batchWrite(
+            List<WriteRequest> batch) {
+        List<OptionalResult<WriteResult, DatabaseException>> writeResults = new ArrayList<>(batch.size());
+        for (InternalWriteResult internalWriteResult : client.batchWrite(batch)) {
+            if (internalWriteResult.status() == InternalWriteResult.Status.NO_SUCH_TABLE) {
+                writeResults.add(new DatabaseOperationResult<WriteResult, DatabaseException>(
+                        new NoSuchTableException()));
+            } else if (internalWriteResult.status() == InternalWriteResult.Status.OPTIMISTIC_LOCK_FAILURE) {
+                writeResults.add(new DatabaseOperationResult<WriteResult, DatabaseException>(
+                        new OptimisticLockException()));
+            } else if (internalWriteResult.status() == InternalWriteResult.Status.PREVIOUS_VALUE_MISMATCH) {
+                // TODO: throw a different exception?
+                writeResults.add(new DatabaseOperationResult<WriteResult, DatabaseException>(
+                        new PreconditionFailedException()));
+            } else if (internalWriteResult.status() == InternalWriteResult.Status.ABORTED) {
+                writeResults.add(new DatabaseOperationResult<WriteResult, DatabaseException>(
+                        new WriteAborted()));
+            } else {
+                writeResults.add(new DatabaseOperationResult<WriteResult, DatabaseException>(
+                        internalWriteResult.result()));
+            }
+        }
+        return writeResults;
+
+    }
+
+    private class DatabaseOperationResult<R, E extends DatabaseException> implements OptionalResult<R, E> {
+
+        private final R result;
+        private final DatabaseException exception;
+
+        public DatabaseOperationResult(R result) {
+            this.result = result;
+            this.exception = null;
+        }
+
+        public DatabaseOperationResult(DatabaseException exception) {
+            this.result = null;
+            this.exception = exception;
+        }
+
+        @Override
+        public R get() {
+            if (result != null) {
+                return result;
+            }
+            throw exception;
+        }
+
+        @Override
+        public boolean hasValidResult() {
+            return result != null;
+        }
+
+        @Override
+        public String toString() {
+            if (result != null) {
+                return result.toString();
+            } else {
+                return exception.toString();
+            }
+        }
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseStateMachine.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseStateMachine.java
new file mode 100644
index 0000000..ad6773e
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/DatabaseStateMachine.java
@@ -0,0 +1,177 @@
+package org.onlab.onos.store.service.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import net.kuujo.copycat.Command;
+import net.kuujo.copycat.Query;
+import net.kuujo.copycat.StateMachine;
+
+import org.onlab.onos.store.serializers.KryoSerializer;
+import org.onlab.onos.store.service.ReadRequest;
+import org.onlab.onos.store.service.ReadResult;
+import org.onlab.onos.store.service.VersionedValue;
+import org.onlab.onos.store.service.WriteRequest;
+import org.onlab.onos.store.service.WriteResult;
+import org.onlab.util.KryoNamespace;
+
+import com.google.common.collect.Maps;
+
+/**
+ * StateMachine whose transitions are coordinated/replicated
+ * by Raft consensus.
+ * Each Raft cluster member has a instance of this state machine that is
+ * independently updated in lock step once there is consensus
+ * on the next transition.
+ */
+public class DatabaseStateMachine implements StateMachine {
+
+    public static final KryoSerializer SERIALIZER = new KryoSerializer() {
+        @Override
+        protected void setupKryoPool() {
+            serializerPool = KryoNamespace.newBuilder()
+                    .register(VersionedValue.class)
+                    .register(State.class)
+                    .register(ClusterMessagingProtocol.COMMON)
+                    .build()
+                    .populate(1);
+        }
+    };
+
+    private State state = new State();
+
+    @Command
+    public boolean createTable(String tableName) {
+        return state.getTables().putIfAbsent(tableName, Maps.newHashMap()) == null;
+    }
+
+    @Command
+    public boolean dropTable(String tableName) {
+        return state.getTables().remove(tableName) != null;
+    }
+
+    @Command
+    public boolean dropAllTables() {
+        state.getTables().clear();
+        return true;
+    }
+
+    @Query
+    public Set<String> listTables() {
+        return state.getTables().keySet();
+    }
+
+    @Query
+    public List<InternalReadResult> read(List<ReadRequest> requests) {
+        List<InternalReadResult> results = new ArrayList<>(requests.size());
+        for (ReadRequest request : requests) {
+            Map<String, VersionedValue> table = state.getTables().get(request.tableName());
+            if (table == null) {
+                results.add(new InternalReadResult(InternalReadResult.Status.NO_SUCH_TABLE, null));
+                continue;
+            }
+            VersionedValue value = table.get(request.key());
+            results.add(new InternalReadResult(
+                    InternalReadResult.Status.OK,
+                    new ReadResult(
+                            request.tableName(),
+                            request.key(),
+                            value)));
+        }
+        return results;
+    }
+
+    @Command
+    public List<InternalWriteResult> write(List<WriteRequest> requests) {
+        boolean abort = false;
+        List<InternalWriteResult.Status> validationResults = new ArrayList<>(requests.size());
+        for (WriteRequest request : requests) {
+            Map<String, VersionedValue> table = state.getTables().get(request.tableName());
+            if (table == null) {
+                validationResults.add(InternalWriteResult.Status.NO_SUCH_TABLE);
+                abort = true;
+                continue;
+            }
+            VersionedValue value = table.get(request.key());
+            if (value == null) {
+                if (request.oldValue() != null) {
+                    validationResults.add(InternalWriteResult.Status.PREVIOUS_VALUE_MISMATCH);
+                    abort = true;
+                    continue;
+                } else if (request.previousVersion() >= 0) {
+                    validationResults.add(InternalWriteResult.Status.OPTIMISTIC_LOCK_FAILURE);
+                    abort = true;
+                    continue;
+                }
+            }
+            if (request.previousVersion() >= 0 && value.version() != request.previousVersion()) {
+                validationResults.add(InternalWriteResult.Status.OPTIMISTIC_LOCK_FAILURE);
+                abort = true;
+                continue;
+            }
+
+            validationResults.add(InternalWriteResult.Status.OK);
+        }
+
+        List<InternalWriteResult> results = new ArrayList<>(requests.size());
+
+        if (abort) {
+            for (InternalWriteResult.Status validationResult : validationResults) {
+                if (validationResult == InternalWriteResult.Status.OK) {
+                    results.add(new InternalWriteResult(InternalWriteResult.Status.ABORTED, null));
+                } else {
+                    results.add(new InternalWriteResult(validationResult, null));
+                }
+            }
+            return results;
+        }
+
+        for (WriteRequest request : requests) {
+            Map<String, VersionedValue> table = state.getTables().get(request.tableName());
+            synchronized (table) {
+                VersionedValue previousValue =
+                        table.put(request.key(), new VersionedValue(request.newValue(), state.nextVersion()));
+                results.add(new InternalWriteResult(
+                        InternalWriteResult.Status.OK,
+                        new WriteResult(request.tableName(), request.key(), previousValue)));
+            }
+        }
+        return results;
+    }
+
+    public class State {
+
+        private final Map<String, Map<String, VersionedValue>> tables =
+                Maps.newHashMap();
+        private long versionCounter = 1;
+
+        Map<String, Map<String, VersionedValue>> getTables() {
+            return tables;
+        }
+
+        long nextVersion() {
+            return versionCounter++;
+        }
+    }
+
+    @Override
+    public byte[] takeSnapshot() {
+        try {
+            return SERIALIZER.encode(state);
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    @Override
+    public void installSnapshot(byte[] data) {
+        try {
+            this.state = SERIALIZER.decode(data);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/InternalReadResult.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/InternalReadResult.java
new file mode 100644
index 0000000..aadbcfb
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/InternalReadResult.java
@@ -0,0 +1,36 @@
+package org.onlab.onos.store.service.impl;
+
+import org.onlab.onos.store.service.ReadResult;
+
+/**
+ * Result of a read operation executed on the DatabaseStateMachine.
+ */
+public class InternalReadResult {
+
+    public enum Status {
+        OK,
+        NO_SUCH_TABLE
+    }
+
+    private final Status status;
+    private final ReadResult result;
+
+    public InternalReadResult(Status status, ReadResult result) {
+        this.status = status;
+        this.result = result;
+    }
+
+    public Status status() {
+        return status;
+    }
+
+    public ReadResult result() {
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "InternalReadResult [status=" + status + ", result=" + result
+                + "]";
+    }
+}
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/InternalWriteResult.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/InternalWriteResult.java
new file mode 100644
index 0000000..d757dfb
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/InternalWriteResult.java
@@ -0,0 +1,33 @@
+package org.onlab.onos.store.service.impl;
+
+import org.onlab.onos.store.service.WriteResult;
+
+/**
+ * Result of a write operation executed on the DatabaseStateMachine.
+ */
+public class InternalWriteResult {
+
+    public enum Status {
+        OK,
+        ABORTED,
+        NO_SUCH_TABLE,
+        OPTIMISTIC_LOCK_FAILURE,
+        PREVIOUS_VALUE_MISMATCH
+    }
+
+    private final Status status;
+    private final WriteResult result;
+
+    public InternalWriteResult(Status status, WriteResult result) {
+        this.status = status;
+        this.result = result;
+    }
+
+    public Status status() {
+        return status;
+    }
+
+    public WriteResult result() {
+        return result;
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/package-info.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/package-info.java
new file mode 100644
index 0000000..f9fd80f
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/impl/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Strongly consistent, fault-tolerant and durable state management
+ * based on Raft consensus protocol.
+ */
+package org.onlab.onos.store.service.impl;
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/service/package-info.java b/core/store/dist/src/main/java/org/onlab/onos/store/service/package-info.java
new file mode 100644
index 0000000..d630870
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/service/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * Distributed core state management services.
+ */
+package org.onlab.onos.store.service;
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/topology/impl/DefaultTopology.java b/core/store/dist/src/main/java/org/onlab/onos/store/topology/impl/DefaultTopology.java
index 6fc2360e..f2e07f1 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/topology/impl/DefaultTopology.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/topology/impl/DefaultTopology.java
@@ -248,8 +248,9 @@
      * Computes on-demand the set of shortest paths between source and
      * destination devices.
      *
-     * @param src source device
-     * @param dst destination device
+     * @param src    source device
+     * @param dst    destination device
+     * @param weight link weight function
      * @return set of shortest paths
      */
     Set<Path> getPaths(DeviceId src, DeviceId dst, LinkWeight weight) {
diff --git a/core/store/dist/src/test/java/org/onlab/onos/store/cluster/StaticClusterService.java b/core/store/dist/src/test/java/org/onlab/onos/store/cluster/StaticClusterService.java
new file mode 100644
index 0000000..6f34d32
--- /dev/null
+++ b/core/store/dist/src/test/java/org/onlab/onos/store/cluster/StaticClusterService.java
@@ -0,0 +1,48 @@
+package org.onlab.onos.store.cluster;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.onlab.onos.cluster.ClusterEventListener;
+import org.onlab.onos.cluster.ClusterService;
+import org.onlab.onos.cluster.ControllerNode;
+import org.onlab.onos.cluster.NodeId;
+import org.onlab.onos.cluster.ControllerNode.State;
+
+import com.google.common.collect.Sets;
+
+public abstract class StaticClusterService implements ClusterService {
+
+    protected final Map<NodeId, ControllerNode> nodes = new HashMap<>();
+    protected final Map<NodeId, ControllerNode.State> nodeStates = new HashMap<>();
+    protected ControllerNode localNode;
+
+    @Override
+    public ControllerNode getLocalNode() {
+        return localNode;
+    }
+
+    @Override
+    public Set<ControllerNode> getNodes() {
+        return Sets.newHashSet(nodes.values());
+    }
+
+    @Override
+    public ControllerNode getNode(NodeId nodeId) {
+        return nodes.get(nodeId);
+    }
+
+    @Override
+    public State getState(NodeId nodeId) {
+        return nodeStates.get(nodeId);
+    }
+
+    @Override
+    public void addListener(ClusterEventListener listener) {
+    }
+
+    @Override
+    public void removeListener(ClusterEventListener listener) {
+    }
+}
\ No newline at end of file
diff --git a/core/store/dist/src/test/java/org/onlab/onos/store/device/impl/GossipDeviceStoreTest.java b/core/store/dist/src/test/java/org/onlab/onos/store/device/impl/GossipDeviceStoreTest.java
index 33b3c14..ccebb72 100644
--- a/core/store/dist/src/test/java/org/onlab/onos/store/device/impl/GossipDeviceStoreTest.java
+++ b/core/store/dist/src/test/java/org/onlab/onos/store/device/impl/GossipDeviceStoreTest.java
@@ -41,10 +41,8 @@
 import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
-import org.onlab.onos.cluster.ClusterEventListener;
 import org.onlab.onos.cluster.ClusterService;
 import org.onlab.onos.cluster.ControllerNode;
-import org.onlab.onos.cluster.ControllerNode.State;
 import org.onlab.onos.cluster.DefaultControllerNode;
 import org.onlab.onos.cluster.NodeId;
 import org.onlab.onos.mastership.MastershipServiceAdapter;
@@ -65,6 +63,7 @@
 import org.onlab.onos.net.device.DeviceStoreDelegate;
 import org.onlab.onos.net.device.PortDescription;
 import org.onlab.onos.net.provider.ProviderId;
+import org.onlab.onos.store.cluster.StaticClusterService;
 import org.onlab.onos.store.cluster.messaging.ClusterCommunicationService;
 import org.onlab.onos.store.cluster.messaging.ClusterMessage;
 import org.onlab.onos.store.cluster.messaging.ClusterMessageHandler;
@@ -133,6 +132,7 @@
     private DeviceClockManager deviceClockManager;
     private DeviceClockService deviceClockService;
     private ClusterCommunicationService clusterCommunicator;
+
     @BeforeClass
     public static void setUpBeforeClass() throws Exception {
     }
@@ -838,45 +838,15 @@
         }
     }
 
-    private static final class TestClusterService implements ClusterService {
-
-        private final Map<NodeId, ControllerNode> nodes = new HashMap<>();
-        private final Map<NodeId, ControllerNode.State> nodeStates = new HashMap<>();
+    private static final class TestClusterService extends StaticClusterService {
 
         public TestClusterService() {
+            localNode = ONOS1;
             nodes.put(NID1, ONOS1);
             nodeStates.put(NID1, ACTIVE);
 
             nodes.put(NID2, ONOS2);
             nodeStates.put(NID2, ACTIVE);
         }
-
-        @Override
-        public ControllerNode getLocalNode() {
-            return GossipDeviceStoreTest.ONOS1;
-        }
-
-        @Override
-        public Set<ControllerNode> getNodes() {
-            return Sets.newHashSet(nodes.values());
-        }
-
-        @Override
-        public ControllerNode getNode(NodeId nodeId) {
-            return nodes.get(nodeId);
-        }
-
-        @Override
-        public State getState(NodeId nodeId) {
-            return nodeStates.get(nodeId);
-        }
-
-        @Override
-        public void addListener(ClusterEventListener listener) {
-        }
-
-        @Override
-        public void removeListener(ClusterEventListener listener) {
-        }
     }
 }
diff --git a/core/store/dist/src/test/java/org/onlab/onos/store/link/impl/GossipLinkStoreTest.java b/core/store/dist/src/test/java/org/onlab/onos/store/link/impl/GossipLinkStoreTest.java
new file mode 100644
index 0000000..d62d0bb
--- /dev/null
+++ b/core/store/dist/src/test/java/org/onlab/onos/store/link/impl/GossipLinkStoreTest.java
@@ -0,0 +1,618 @@
+/*
+ * Copyright 2014 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.onlab.onos.store.link.impl;
+
+import com.google.common.collect.Iterables;
+
+import org.easymock.Capture;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.onlab.onos.cluster.ControllerNode;
+import org.onlab.onos.cluster.DefaultControllerNode;
+import org.onlab.onos.cluster.NodeId;
+import org.onlab.onos.mastership.MastershipTerm;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DefaultAnnotations;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.Link;
+import org.onlab.onos.net.Link.Type;
+import org.onlab.onos.net.LinkKey;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.SparseAnnotations;
+import org.onlab.onos.net.device.DeviceClockService;
+import org.onlab.onos.net.link.DefaultLinkDescription;
+import org.onlab.onos.net.link.LinkDescription;
+import org.onlab.onos.net.link.LinkEvent;
+import org.onlab.onos.net.link.LinkStore;
+import org.onlab.onos.net.link.LinkStoreDelegate;
+import org.onlab.onos.net.provider.ProviderId;
+import org.onlab.onos.store.cluster.StaticClusterService;
+import org.onlab.onos.store.cluster.messaging.ClusterCommunicationService;
+import org.onlab.onos.store.cluster.messaging.ClusterMessage;
+import org.onlab.onos.store.cluster.messaging.ClusterMessageHandler;
+import org.onlab.onos.store.cluster.messaging.MessageSubject;
+import org.onlab.onos.store.device.impl.DeviceClockManager;
+import org.onlab.packet.IpAddress;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.capture;
+import static org.easymock.EasyMock.createNiceMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.*;
+import static org.onlab.onos.cluster.ControllerNode.State.ACTIVE;
+import static org.onlab.onos.net.DeviceId.deviceId;
+import static org.onlab.onos.net.Link.Type.*;
+import static org.onlab.onos.net.link.LinkEvent.Type.*;
+import static org.onlab.onos.net.NetTestTools.assertAnnotationsEquals;
+
+/**
+ * Test of the GossipLinkStoreTest implementation.
+ */
+public class GossipLinkStoreTest {
+
+    private static final ProviderId PID = new ProviderId("of", "foo");
+    private static final ProviderId PIDA = new ProviderId("of", "bar", true);
+    private static final DeviceId DID1 = deviceId("of:foo");
+    private static final DeviceId DID2 = deviceId("of:bar");
+
+    private static final PortNumber P1 = PortNumber.portNumber(1);
+    private static final PortNumber P2 = PortNumber.portNumber(2);
+    private static final PortNumber P3 = PortNumber.portNumber(3);
+
+    private static final SparseAnnotations A1 = DefaultAnnotations.builder()
+            .set("A1", "a1")
+            .set("B1", "b1")
+            .build();
+    private static final SparseAnnotations A1_2 = DefaultAnnotations.builder()
+            .remove("A1")
+            .set("B3", "b3")
+            .build();
+    private static final SparseAnnotations A2 = DefaultAnnotations.builder()
+            .set("A2", "a2")
+            .set("B2", "b2")
+            .build();
+    private static final SparseAnnotations A2_2 = DefaultAnnotations.builder()
+            .remove("A2")
+            .set("B4", "b4")
+            .build();
+
+    // local node
+    private static final NodeId NID1 = new NodeId("local");
+    private static final ControllerNode ONOS1 =
+            new DefaultControllerNode(NID1, IpAddress.valueOf("127.0.0.1"));
+
+    // remote node
+    private static final NodeId NID2 = new NodeId("remote");
+    private static final ControllerNode ONOS2 =
+            new DefaultControllerNode(NID2, IpAddress.valueOf("127.0.0.2"));
+
+    private GossipLinkStore linkStoreImpl;
+    private LinkStore linkStore;
+
+    private DeviceClockManager deviceClockManager;
+    private DeviceClockService deviceClockService;
+    private ClusterCommunicationService clusterCommunicator;
+
+
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception {
+    }
+
+    @AfterClass
+    public static void tearDownAfterClass() throws Exception {
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        deviceClockManager = new DeviceClockManager();
+        deviceClockManager.activate();
+        deviceClockService = deviceClockManager;
+
+        // set initial terms
+        deviceClockManager.setMastershipTerm(DID1, MastershipTerm.of(NID1, 1));
+        deviceClockManager.setMastershipTerm(DID2, MastershipTerm.of(NID1, 2));
+
+        // TODO mock clusterCommunicator
+        clusterCommunicator = createNiceMock(ClusterCommunicationService.class);
+        clusterCommunicator.addSubscriber(anyObject(MessageSubject.class),
+                                    anyObject(ClusterMessageHandler.class));
+        expectLastCall().anyTimes();
+        replay(clusterCommunicator);
+
+        linkStoreImpl = new GossipLinkStore();
+        linkStoreImpl.deviceClockService = deviceClockService;
+        linkStoreImpl.clusterCommunicator = clusterCommunicator;
+        linkStoreImpl.clusterService = new TestClusterService();
+        linkStoreImpl.activate();
+        linkStore = linkStoreImpl;
+
+        verify(clusterCommunicator);
+        reset(clusterCommunicator);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        linkStoreImpl.deactivate();
+    }
+
+    private void putLink(DeviceId srcId, PortNumber srcNum,
+                         DeviceId dstId, PortNumber dstNum, Type type,
+                         SparseAnnotations... annotations) {
+        ConnectPoint src = new ConnectPoint(srcId, srcNum);
+        ConnectPoint dst = new ConnectPoint(dstId, dstNum);
+        reset(clusterCommunicator);
+        try {
+            expect(clusterCommunicator.broadcast(anyObject(ClusterMessage.class)))
+                .andReturn(true).anyTimes();
+        } catch (IOException e) {
+            fail("Should never reach here");
+        }
+        replay(clusterCommunicator);
+        linkStore.createOrUpdateLink(PID, new DefaultLinkDescription(src, dst, type, annotations));
+        verify(clusterCommunicator);
+    }
+
+    private void resetCommunicatorExpectingNoBroadcast(
+                                                       Capture<ClusterMessage> bcast) {
+        bcast.reset();
+        reset(clusterCommunicator);
+        replay(clusterCommunicator);
+    }
+
+    private void resetCommunicatorExpectingSingleBroadcast(
+                                                           Capture<ClusterMessage> bcast) {
+
+        bcast.reset();
+        reset(clusterCommunicator);
+        try {
+            expect(clusterCommunicator.broadcast(capture(bcast))).andReturn(true).once();
+        } catch (IOException e) {
+            fail("Should never reach here");
+        }
+        replay(clusterCommunicator);
+    }
+
+    private void putLink(LinkKey key, Type type, SparseAnnotations... annotations) {
+        putLink(key.src().deviceId(), key.src().port(),
+                key.dst().deviceId(), key.dst().port(),
+                type, annotations);
+    }
+
+    private static void assertLink(DeviceId srcId, PortNumber srcNum,
+                            DeviceId dstId, PortNumber dstNum, Type type,
+                            Link link) {
+        assertEquals(srcId, link.src().deviceId());
+        assertEquals(srcNum, link.src().port());
+        assertEquals(dstId, link.dst().deviceId());
+        assertEquals(dstNum, link.dst().port());
+        assertEquals(type, link.type());
+    }
+
+    private static void assertLink(LinkKey key, Type type, Link link) {
+        assertLink(key.src().deviceId(), key.src().port(),
+                   key.dst().deviceId(), key.dst().port(),
+                   type, link);
+    }
+
+    @Test
+    public final void testGetLinkCount() {
+        assertEquals("initialy empty", 0, linkStore.getLinkCount());
+
+        putLink(DID1, P1, DID2, P2, DIRECT);
+        putLink(DID2, P2, DID1, P1, DIRECT);
+        putLink(DID1, P1, DID2, P2, DIRECT);
+
+        assertEquals("expecting 2 unique link", 2, linkStore.getLinkCount());
+    }
+
+    @Test
+    public final void testGetLinks() {
+        assertEquals("initialy empty", 0,
+                Iterables.size(linkStore.getLinks()));
+
+        LinkKey linkId1 = LinkKey.linkKey(new ConnectPoint(DID1, P1), new ConnectPoint(DID2, P2));
+        LinkKey linkId2 = LinkKey.linkKey(new ConnectPoint(DID2, P2), new ConnectPoint(DID1, P1));
+
+        putLink(linkId1, DIRECT);
+        putLink(linkId2, DIRECT);
+        putLink(linkId1, DIRECT);
+
+        assertEquals("expecting 2 unique link", 2,
+                Iterables.size(linkStore.getLinks()));
+
+        Map<LinkKey, Link> links = new HashMap<>();
+        for (Link link : linkStore.getLinks()) {
+            links.put(LinkKey.linkKey(link), link);
+        }
+
+        assertLink(linkId1, DIRECT, links.get(linkId1));
+        assertLink(linkId2, DIRECT, links.get(linkId2));
+    }
+
+    @Test
+    public final void testGetDeviceEgressLinks() {
+        LinkKey linkId1 = LinkKey.linkKey(new ConnectPoint(DID1, P1), new ConnectPoint(DID2, P2));
+        LinkKey linkId2 = LinkKey.linkKey(new ConnectPoint(DID2, P2), new ConnectPoint(DID1, P1));
+        LinkKey linkId3 = LinkKey.linkKey(new ConnectPoint(DID1, P2), new ConnectPoint(DID2, P3));
+
+        putLink(linkId1, DIRECT);
+        putLink(linkId2, DIRECT);
+        putLink(linkId3, DIRECT);
+
+        // DID1,P1 => DID2,P2
+        // DID2,P2 => DID1,P1
+        // DID1,P2 => DID2,P3
+
+        Set<Link> links1 = linkStore.getDeviceEgressLinks(DID1);
+        assertEquals(2, links1.size());
+        // check
+
+        Set<Link> links2 = linkStore.getDeviceEgressLinks(DID2);
+        assertEquals(1, links2.size());
+        assertLink(linkId2, DIRECT, links2.iterator().next());
+    }
+
+    @Test
+    public final void testGetDeviceIngressLinks() {
+        LinkKey linkId1 = LinkKey.linkKey(new ConnectPoint(DID1, P1), new ConnectPoint(DID2, P2));
+        LinkKey linkId2 = LinkKey.linkKey(new ConnectPoint(DID2, P2), new ConnectPoint(DID1, P1));
+        LinkKey linkId3 = LinkKey.linkKey(new ConnectPoint(DID1, P2), new ConnectPoint(DID2, P3));
+
+        putLink(linkId1, DIRECT);
+        putLink(linkId2, DIRECT);
+        putLink(linkId3, DIRECT);
+
+        // DID1,P1 => DID2,P2
+        // DID2,P2 => DID1,P1
+        // DID1,P2 => DID2,P3
+
+        Set<Link> links1 = linkStore.getDeviceIngressLinks(DID2);
+        assertEquals(2, links1.size());
+        // check
+
+        Set<Link> links2 = linkStore.getDeviceIngressLinks(DID1);
+        assertEquals(1, links2.size());
+        assertLink(linkId2, DIRECT, links2.iterator().next());
+    }
+
+    @Test
+    public final void testGetLink() {
+        ConnectPoint src = new ConnectPoint(DID1, P1);
+        ConnectPoint dst = new ConnectPoint(DID2, P2);
+        LinkKey linkId1 = LinkKey.linkKey(src, dst);
+
+        putLink(linkId1, DIRECT);
+
+        Link link = linkStore.getLink(src, dst);
+        assertLink(linkId1, DIRECT, link);
+
+        assertNull("There shouldn't be reverese link",
+                linkStore.getLink(dst, src));
+    }
+
+    @Test
+    public final void testGetEgressLinks() {
+        final ConnectPoint d1P1 = new ConnectPoint(DID1, P1);
+        final ConnectPoint d2P2 = new ConnectPoint(DID2, P2);
+        LinkKey linkId1 = LinkKey.linkKey(d1P1, d2P2);
+        LinkKey linkId2 = LinkKey.linkKey(d2P2, d1P1);
+        LinkKey linkId3 = LinkKey.linkKey(new ConnectPoint(DID1, P2), new ConnectPoint(DID2, P3));
+
+        putLink(linkId1, DIRECT);
+        putLink(linkId2, DIRECT);
+        putLink(linkId3, DIRECT);
+
+        // DID1,P1 => DID2,P2
+        // DID2,P2 => DID1,P1
+        // DID1,P2 => DID2,P3
+
+        Set<Link> links1 = linkStore.getEgressLinks(d1P1);
+        assertEquals(1, links1.size());
+        assertLink(linkId1, DIRECT, links1.iterator().next());
+
+        Set<Link> links2 = linkStore.getEgressLinks(d2P2);
+        assertEquals(1, links2.size());
+        assertLink(linkId2, DIRECT, links2.iterator().next());
+    }
+
+    @Test
+    public final void testGetIngressLinks() {
+        final ConnectPoint d1P1 = new ConnectPoint(DID1, P1);
+        final ConnectPoint d2P2 = new ConnectPoint(DID2, P2);
+        LinkKey linkId1 = LinkKey.linkKey(d1P1, d2P2);
+        LinkKey linkId2 = LinkKey.linkKey(d2P2, d1P1);
+        LinkKey linkId3 = LinkKey.linkKey(new ConnectPoint(DID1, P2), new ConnectPoint(DID2, P3));
+
+        putLink(linkId1, DIRECT);
+        putLink(linkId2, DIRECT);
+        putLink(linkId3, DIRECT);
+
+        // DID1,P1 => DID2,P2
+        // DID2,P2 => DID1,P1
+        // DID1,P2 => DID2,P3
+
+        Set<Link> links1 = linkStore.getIngressLinks(d2P2);
+        assertEquals(1, links1.size());
+        assertLink(linkId1, DIRECT, links1.iterator().next());
+
+        Set<Link> links2 = linkStore.getIngressLinks(d1P1);
+        assertEquals(1, links2.size());
+        assertLink(linkId2, DIRECT, links2.iterator().next());
+    }
+
+    @Test
+    public final void testCreateOrUpdateLink() {
+        ConnectPoint src = new ConnectPoint(DID1, P1);
+        ConnectPoint dst = new ConnectPoint(DID2, P2);
+
+        Capture<ClusterMessage> bcast = new Capture<>();
+
+        // add link
+        resetCommunicatorExpectingSingleBroadcast(bcast);
+        final DefaultLinkDescription linkDescription = new DefaultLinkDescription(src, dst, INDIRECT);
+        LinkEvent event = linkStore.createOrUpdateLink(PID,
+                    linkDescription);
+        verifyLinkBroadcastMessage(PID, NID1, src, dst, INDIRECT, bcast);
+
+        assertLink(DID1, P1, DID2, P2, INDIRECT, event.subject());
+        assertEquals(LINK_ADDED, event.type());
+
+        // update link type
+        resetCommunicatorExpectingSingleBroadcast(bcast);
+        LinkEvent event2 = linkStore.createOrUpdateLink(PID,
+                new DefaultLinkDescription(src, dst, DIRECT));
+        verifyLinkBroadcastMessage(PID, NID1, src, dst, DIRECT, bcast);
+
+        assertLink(DID1, P1, DID2, P2, DIRECT, event2.subject());
+        assertEquals(LINK_UPDATED, event2.type());
+
+        // no change
+        resetCommunicatorExpectingSingleBroadcast(bcast);
+        LinkEvent event3 = linkStore.createOrUpdateLink(PID,
+                new DefaultLinkDescription(src, dst, DIRECT));
+        verifyNoBroadcastMessage(bcast);
+
+        assertNull("No change event expected", event3);
+    }
+
+    private void verifyNoBroadcastMessage(Capture<ClusterMessage> bcast) {
+        assertFalse("No broadcast expected", bcast.hasCaptured());
+    }
+
+    private void verifyLinkBroadcastMessage(ProviderId providerId,
+                                            NodeId sender,
+                                            ConnectPoint src,
+                                            ConnectPoint dst,
+                                            Type type,
+                                            Capture<ClusterMessage> actualMsg) {
+        verify(clusterCommunicator);
+        assertTrue(actualMsg.hasCaptured());
+        assertEquals(sender, actualMsg.getValue().sender());
+        assertEquals(GossipLinkStoreMessageSubjects.LINK_UPDATE,
+                     actualMsg.getValue().subject());
+        InternalLinkEvent linkEvent
+            = GossipLinkStore.SERIALIZER.decode(actualMsg.getValue().payload());
+        assertEquals(providerId, linkEvent.providerId());
+        assertLinkDescriptionEquals(src, dst, type, linkEvent.linkDescription().value());
+
+    }
+
+    private static void assertLinkDescriptionEquals(ConnectPoint src,
+                                             ConnectPoint dst,
+                                             Type type,
+                                             LinkDescription actual) {
+        assertEquals(src, actual.src());
+        assertEquals(dst, actual.dst());
+        assertEquals(type, actual.type());
+        // TODO check annotations
+    }
+
+    @Test
+    public final void testCreateOrUpdateLinkAncillary() {
+        ConnectPoint src = new ConnectPoint(DID1, P1);
+        ConnectPoint dst = new ConnectPoint(DID2, P2);
+
+        Capture<ClusterMessage> bcast = new Capture<>();
+
+        // add Ancillary link
+        resetCommunicatorExpectingSingleBroadcast(bcast);
+        LinkEvent event = linkStore.createOrUpdateLink(PIDA,
+                    new DefaultLinkDescription(src, dst, INDIRECT, A1));
+        verifyLinkBroadcastMessage(PIDA, NID1, src, dst, INDIRECT, bcast);
+
+        assertNotNull("Ancillary only link is ignored", event);
+
+        // add Primary link
+        resetCommunicatorExpectingSingleBroadcast(bcast);
+        LinkEvent event2 = linkStore.createOrUpdateLink(PID,
+                new DefaultLinkDescription(src, dst, INDIRECT, A2));
+        verifyLinkBroadcastMessage(PID, NID1, src, dst, INDIRECT, bcast);
+
+        assertLink(DID1, P1, DID2, P2, INDIRECT, event2.subject());
+        assertAnnotationsEquals(event2.subject().annotations(), A2, A1);
+        assertEquals(LINK_UPDATED, event2.type());
+
+        // update link type
+        resetCommunicatorExpectingSingleBroadcast(bcast);
+        LinkEvent event3 = linkStore.createOrUpdateLink(PID,
+                new DefaultLinkDescription(src, dst, DIRECT, A2));
+        verifyLinkBroadcastMessage(PID, NID1, src, dst, DIRECT, bcast);
+
+        assertLink(DID1, P1, DID2, P2, DIRECT, event3.subject());
+        assertAnnotationsEquals(event3.subject().annotations(), A2, A1);
+        assertEquals(LINK_UPDATED, event3.type());
+
+
+        // no change
+        resetCommunicatorExpectingNoBroadcast(bcast);
+        LinkEvent event4 = linkStore.createOrUpdateLink(PID,
+                new DefaultLinkDescription(src, dst, DIRECT));
+        verifyNoBroadcastMessage(bcast);
+
+        assertNull("No change event expected", event4);
+
+        // update link annotation (Primary)
+        resetCommunicatorExpectingSingleBroadcast(bcast);
+        LinkEvent event5 = linkStore.createOrUpdateLink(PID,
+                new DefaultLinkDescription(src, dst, DIRECT, A2_2));
+        verifyLinkBroadcastMessage(PID, NID1, src, dst, DIRECT, bcast);
+
+        assertLink(DID1, P1, DID2, P2, DIRECT, event5.subject());
+        assertAnnotationsEquals(event5.subject().annotations(), A2, A2_2, A1);
+        assertEquals(LINK_UPDATED, event5.type());
+
+        // update link annotation (Ancillary)
+        resetCommunicatorExpectingSingleBroadcast(bcast);
+        LinkEvent event6 = linkStore.createOrUpdateLink(PIDA,
+                new DefaultLinkDescription(src, dst, DIRECT, A1_2));
+        verifyLinkBroadcastMessage(PIDA, NID1, src, dst, DIRECT, bcast);
+
+        assertLink(DID1, P1, DID2, P2, DIRECT, event6.subject());
+        assertAnnotationsEquals(event6.subject().annotations(), A2, A2_2, A1, A1_2);
+        assertEquals(LINK_UPDATED, event6.type());
+
+        // update link type (Ancillary) : ignored
+        resetCommunicatorExpectingNoBroadcast(bcast);
+        LinkEvent event7 = linkStore.createOrUpdateLink(PIDA,
+                new DefaultLinkDescription(src, dst, EDGE));
+        verifyNoBroadcastMessage(bcast);
+        assertNull("Ancillary change other than annotation is ignored", event7);
+    }
+
+
+    @Test
+    public final void testRemoveLink() {
+        final ConnectPoint d1P1 = new ConnectPoint(DID1, P1);
+        final ConnectPoint d2P2 = new ConnectPoint(DID2, P2);
+        LinkKey linkId1 = LinkKey.linkKey(d1P1, d2P2);
+        LinkKey linkId2 = LinkKey.linkKey(d2P2, d1P1);
+
+        putLink(linkId1, DIRECT, A1);
+        putLink(linkId2, DIRECT, A2);
+
+        // DID1,P1 => DID2,P2
+        // DID2,P2 => DID1,P1
+        // DID1,P2 => DID2,P3
+
+        LinkEvent event = linkStore.removeLink(d1P1, d2P2);
+        assertEquals(LINK_REMOVED, event.type());
+        assertAnnotationsEquals(event.subject().annotations(), A1);
+        LinkEvent event2 = linkStore.removeLink(d1P1, d2P2);
+        assertNull(event2);
+
+        assertLink(linkId2, DIRECT, linkStore.getLink(d2P2, d1P1));
+        assertAnnotationsEquals(linkStore.getLink(d2P2, d1P1).annotations(), A2);
+
+        // annotations, etc. should not survive remove
+        putLink(linkId1, DIRECT);
+        assertLink(linkId1, DIRECT, linkStore.getLink(d1P1, d2P2));
+        assertAnnotationsEquals(linkStore.getLink(d1P1, d2P2).annotations());
+    }
+
+    @Test
+    public final void testAncillaryVisible() {
+        ConnectPoint src = new ConnectPoint(DID1, P1);
+        ConnectPoint dst = new ConnectPoint(DID2, P2);
+
+        // add Ancillary link
+        linkStore.createOrUpdateLink(PIDA,
+                    new DefaultLinkDescription(src, dst, INDIRECT, A1));
+
+        // Ancillary only link should not be visible
+        assertEquals(1, linkStore.getLinkCount());
+        assertNotNull(linkStore.getLink(src, dst));
+    }
+
+    // If Delegates should be called only on remote events,
+    // then Simple* should never call them, thus not test required.
+    @Ignore("Ignore until Delegate spec. is clear.")
+    @Test
+    public final void testEvents() throws InterruptedException {
+
+        final ConnectPoint d1P1 = new ConnectPoint(DID1, P1);
+        final ConnectPoint d2P2 = new ConnectPoint(DID2, P2);
+        final LinkKey linkId1 = LinkKey.linkKey(d1P1, d2P2);
+
+        final CountDownLatch addLatch = new CountDownLatch(1);
+        LinkStoreDelegate checkAdd = new LinkStoreDelegate() {
+            @Override
+            public void notify(LinkEvent event) {
+                assertEquals(LINK_ADDED, event.type());
+                assertLink(linkId1, INDIRECT, event.subject());
+                addLatch.countDown();
+            }
+        };
+        final CountDownLatch updateLatch = new CountDownLatch(1);
+        LinkStoreDelegate checkUpdate = new LinkStoreDelegate() {
+            @Override
+            public void notify(LinkEvent event) {
+                assertEquals(LINK_UPDATED, event.type());
+                assertLink(linkId1, DIRECT, event.subject());
+                updateLatch.countDown();
+            }
+        };
+        final CountDownLatch removeLatch = new CountDownLatch(1);
+        LinkStoreDelegate checkRemove = new LinkStoreDelegate() {
+            @Override
+            public void notify(LinkEvent event) {
+                assertEquals(LINK_REMOVED, event.type());
+                assertLink(linkId1, DIRECT, event.subject());
+                removeLatch.countDown();
+            }
+        };
+
+        linkStore.setDelegate(checkAdd);
+        putLink(linkId1, INDIRECT);
+        assertTrue("Add event fired", addLatch.await(1, TimeUnit.SECONDS));
+
+        linkStore.unsetDelegate(checkAdd);
+        linkStore.setDelegate(checkUpdate);
+        putLink(linkId1, DIRECT);
+        assertTrue("Update event fired", updateLatch.await(1, TimeUnit.SECONDS));
+
+        linkStore.unsetDelegate(checkUpdate);
+        linkStore.setDelegate(checkRemove);
+        linkStore.removeLink(d1P1, d2P2);
+        assertTrue("Remove event fired", removeLatch.await(1, TimeUnit.SECONDS));
+    }
+
+    private static final class TestClusterService extends StaticClusterService {
+
+        public TestClusterService() {
+            localNode = ONOS1;
+            nodes.put(NID1, ONOS1);
+            nodeStates.put(NID1, ACTIVE);
+
+            nodes.put(NID2, ONOS2);
+            nodeStates.put(NID2, ACTIVE);
+        }
+    }
+}
diff --git a/core/store/dist/src/test/java/org/onlab/onos/store/link/impl/LinkFragmentIdTest.java b/core/store/dist/src/test/java/org/onlab/onos/store/link/impl/LinkFragmentIdTest.java
new file mode 100644
index 0000000..5109782
--- /dev/null
+++ b/core/store/dist/src/test/java/org/onlab/onos/store/link/impl/LinkFragmentIdTest.java
@@ -0,0 +1,48 @@
+package org.onlab.onos.store.link.impl;
+
+import static org.onlab.onos.net.DeviceId.deviceId;
+
+import org.junit.Test;
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.LinkKey;
+import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.provider.ProviderId;
+import com.google.common.testing.EqualsTester;
+
+public class LinkFragmentIdTest {
+
+    private static final ProviderId PID = new ProviderId("of", "foo");
+    private static final ProviderId PIDA = new ProviderId("of", "bar", true);
+
+    private static final DeviceId DID1 = deviceId("of:foo");
+    private static final DeviceId DID2 = deviceId("of:bar");
+
+    private static final PortNumber P1 = PortNumber.portNumber(1);
+    private static final PortNumber P2 = PortNumber.portNumber(2);
+    private static final PortNumber P3 = PortNumber.portNumber(3);
+
+    private static final ConnectPoint CP1 = new ConnectPoint(DID1, P1);
+    private static final ConnectPoint CP2 = new ConnectPoint(DID2, P2);
+
+    private static final ConnectPoint CP3 = new ConnectPoint(DID1, P2);
+    private static final ConnectPoint CP4 = new ConnectPoint(DID2, P3);
+
+    private static final LinkKey L1 = LinkKey.linkKey(CP1, CP2);
+    private static final LinkKey L2 = LinkKey.linkKey(CP3, CP4);
+
+    @Test
+    public void testEquals() {
+        new EqualsTester()
+            .addEqualityGroup(new LinkFragmentId(L1, PID),
+                              new LinkFragmentId(L1, PID))
+            .addEqualityGroup(new LinkFragmentId(L2, PID),
+                              new LinkFragmentId(L2, PID))
+            .addEqualityGroup(new LinkFragmentId(L1, PIDA),
+                              new LinkFragmentId(L1, PIDA))
+            .addEqualityGroup(new LinkFragmentId(L2, PIDA),
+                              new LinkFragmentId(L2, PIDA))
+            .testEquals();
+    }
+
+}
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
index 2a4c10c..80db22e 100644
--- a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/KryoNamespaces.java
@@ -75,9 +75,15 @@
 import org.onlab.onos.net.intent.OpticalPathIntent;
 import org.onlab.onos.net.intent.PathIntent;
 import org.onlab.onos.net.intent.PointToPointIntent;
+import org.onlab.onos.net.intent.constraint.BandwidthConstraint;
+import org.onlab.onos.net.intent.constraint.BooleanConstraint;
+import org.onlab.onos.net.intent.constraint.LambdaConstraint;
+import org.onlab.onos.net.intent.constraint.LinkTypeConstraint;
 import org.onlab.onos.net.link.DefaultLinkDescription;
 import org.onlab.onos.net.packet.DefaultOutboundPacket;
 import org.onlab.onos.net.provider.ProviderId;
+import org.onlab.onos.net.resource.Bandwidth;
+import org.onlab.onos.net.resource.Lambda;
 import org.onlab.onos.net.resource.LinkResourceRequest;
 import org.onlab.onos.store.Timestamp;
 import org.onlab.packet.ChassisId;
@@ -188,7 +194,13 @@
                     LinkCollectionIntent.class,
                     OpticalConnectivityIntent.class,
                     OpticalPathIntent.class,
-                    LinkResourceRequest.class
+                    LinkResourceRequest.class,
+                    Lambda.class,
+                    Bandwidth.class,
+                    LambdaConstraint.class,
+                    BandwidthConstraint.class,
+                    LinkTypeConstraint.class,
+                    BooleanConstraint.class
                     )
             .register(DefaultApplicationId.class, new DefaultApplicationIdSerializer())
             .register(URI.class, new URISerializer())
diff --git a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/StoreSerializer.java b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/StoreSerializer.java
index 48fff65..8ebb0e6 100644
--- a/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/StoreSerializer.java
+++ b/core/store/serializers/src/main/java/org/onlab/onos/store/serializers/StoreSerializer.java
@@ -44,6 +44,7 @@
      *
      * @param bytes bytes to be deserialized
      * @return deserialized object
+     * @param <T> decoded type
      */
     public <T> T decode(final byte[] bytes);
 
@@ -52,6 +53,7 @@
      *
      * @param buffer bytes to be deserialized
      * @return deserialized object
+     * @param <T> decoded type
      */
     public <T> T decode(final ByteBuffer buffer);
 }
diff --git a/core/store/trivial/pom.xml b/core/store/trivial/pom.xml
index 59cf1b8..d1e6bb5 100644
--- a/core/store/trivial/pom.xml
+++ b/core/store/trivial/pom.xml
@@ -36,6 +36,13 @@
           <groupId>org.apache.commons</groupId>
           <artifactId>commons-lang3</artifactId>
         </dependency>
+
+        <dependency>
+          <groupId>org.onlab.onos</groupId>
+          <artifactId>onos-api</artifactId>
+          <classifier>tests</classifier>
+          <scope>test</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleFlowRuleStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleFlowRuleStore.java
index 49b0c71..b7d26fb 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleFlowRuleStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleFlowRuleStore.java
@@ -16,8 +16,12 @@
 package org.onlab.onos.store.trivial.impl;
 
 import com.google.common.base.Function;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.FluentIterable;
 import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.SettableFuture;
+
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -43,13 +47,15 @@
 import org.onlab.util.NewConcurrentHashMap;
 import org.slf4j.Logger;
 
-import java.util.Arrays;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import static org.apache.commons.lang3.concurrent.ConcurrentUtils.createIfAbsentUnchecked;
 import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_REMOVED;
@@ -72,6 +78,18 @@
     private final ConcurrentMap<DeviceId, ConcurrentMap<FlowId, List<StoredFlowEntry>>>
             flowEntries = new ConcurrentHashMap<>();
 
+    private final AtomicInteger localBatchIdGen = new AtomicInteger();
+
+    // TODO: make this configurable
+    private int pendingFutureTimeoutMinutes = 5;
+
+    private Cache<Integer, SettableFuture<CompletedBatchOperation>> pendingFutures =
+            CacheBuilder.newBuilder()
+                .expireAfterWrite(pendingFutureTimeoutMinutes, TimeUnit.MINUTES)
+                // TODO Explicitly fail the future if expired?
+                //.removalListener(listener)
+                .build();
+
     @Activate
     public void activate() {
         log.info("Started");
@@ -173,10 +191,6 @@
             }
             // new flow rule added
             existing.add(f);
-            notifyDelegate(FlowRuleBatchEvent.requested(
-                    new FlowRuleBatchRequest(1, /* FIXME generate something */
-                                             Arrays.<FlowEntry>asList(f),
-                                             Collections.<FlowEntry>emptyList())));
         }
     }
 
@@ -190,11 +204,6 @@
                 if (entry.equals(rule)) {
                     synchronized (entry) {
                         entry.setState(FlowEntryState.PENDING_REMOVE);
-                        // TODO: Should we notify only if it's "remote" event?
-                        notifyDelegate(FlowRuleBatchEvent.requested(
-                                new FlowRuleBatchRequest(1, /* FIXME generate something */
-                                                         Collections.<FlowEntry>emptyList(),
-                                                         Arrays.<FlowEntry>asList(entry))));
                     }
                 }
             }
@@ -251,20 +260,47 @@
     @Override
     public Future<CompletedBatchOperation> storeBatch(
             FlowRuleBatchOperation batchOperation) {
+        List<FlowRule> toAdd = new ArrayList<>();
+        List<FlowRule> toRemove = new ArrayList<>();
         for (FlowRuleBatchEntry entry : batchOperation.getOperations()) {
+            final FlowRule flowRule = entry.getTarget();
             if (entry.getOperator().equals(FlowRuleOperation.ADD)) {
-                storeFlowRule(entry.getTarget());
+                if (!getFlowEntries(flowRule.deviceId(), flowRule.id()).contains(flowRule)) {
+                    storeFlowRule(flowRule);
+                    toAdd.add(flowRule);
+                }
             } else if (entry.getOperator().equals(FlowRuleOperation.REMOVE)) {
-                deleteFlowRule(entry.getTarget());
+                if (getFlowEntries(flowRule.deviceId(), flowRule.id()).contains(flowRule)) {
+                    deleteFlowRule(flowRule);
+                    toRemove.add(flowRule);
+                }
             } else {
                 throw new UnsupportedOperationException("Unsupported operation type");
             }
         }
-        return Futures.immediateFuture(new CompletedBatchOperation(true, Collections.<FlowEntry>emptySet()));
+
+        if (toAdd.isEmpty() && toRemove.isEmpty()) {
+            return Futures.immediateFuture(new CompletedBatchOperation(true, Collections.<FlowRule>emptySet()));
+        }
+
+        SettableFuture<CompletedBatchOperation> r = SettableFuture.create();
+        final int batchId = localBatchIdGen.incrementAndGet();
+
+        pendingFutures.put(batchId, r);
+        notifyDelegate(FlowRuleBatchEvent.requested(new FlowRuleBatchRequest(batchId, toAdd, toRemove)));
+
+        return r;
     }
 
     @Override
     public void batchOperationComplete(FlowRuleBatchEvent event) {
+        final Integer batchId = event.subject().batchId();
+        SettableFuture<CompletedBatchOperation> future
+            = pendingFutures.getIfPresent(batchId);
+        if (future != null) {
+            future.set(event.result());
+            pendingFutures.invalidate(batchId);
+        }
         notifyDelegate(event);
     }
 }
diff --git a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleHostStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleHostStore.java
index 6c27ea8..2f8fbd2 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleHostStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleHostStore.java
@@ -15,10 +15,18 @@
  */
 package org.onlab.onos.store.trivial.impl;
 
-import com.google.common.collect.HashMultimap;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Multimap;
-import com.google.common.collect.Sets;
+import static org.onlab.onos.net.host.HostEvent.Type.HOST_ADDED;
+import static org.onlab.onos.net.host.HostEvent.Type.HOST_MOVED;
+import static org.onlab.onos.net.host.HostEvent.Type.HOST_REMOVED;
+import static org.onlab.onos.net.host.HostEvent.Type.HOST_UPDATED;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -34,7 +42,6 @@
 import org.onlab.onos.net.host.HostEvent;
 import org.onlab.onos.net.host.HostStore;
 import org.onlab.onos.net.host.HostStoreDelegate;
-import org.onlab.onos.net.host.InterfaceIpAddress;
 import org.onlab.onos.net.host.PortAddresses;
 import org.onlab.onos.net.provider.ProviderId;
 import org.onlab.onos.store.AbstractStore;
@@ -43,13 +50,11 @@
 import org.onlab.packet.VlanId;
 import org.slf4j.Logger;
 
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-import static org.onlab.onos.net.host.HostEvent.Type.*;
-import static org.slf4j.LoggerFactory.getLogger;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+import com.google.common.collect.SetMultimap;
 
 // TODO: multi-provider, annotation not supported.
 /**
@@ -70,8 +75,9 @@
     // Hosts tracked by their location
     private final Multimap<ConnectPoint, Host> locations = HashMultimap.create();
 
-    private final Map<ConnectPoint, PortAddresses> portAddresses =
-            new ConcurrentHashMap<>();
+    private final SetMultimap<ConnectPoint, PortAddresses> portAddresses =
+            Multimaps.synchronizedSetMultimap(
+                    HashMultimap.<ConnectPoint, PortAddresses>create());
 
     @Activate
     public void activate() {
@@ -213,77 +219,37 @@
 
     @Override
     public void updateAddressBindings(PortAddresses addresses) {
-        synchronized (portAddresses) {
-            PortAddresses existing = portAddresses.get(addresses.connectPoint());
-            if (existing == null) {
-                portAddresses.put(addresses.connectPoint(), addresses);
-            } else {
-                Set<InterfaceIpAddress> union =
-                    Sets.union(existing.ipAddresses(),
-                               addresses.ipAddresses()).immutableCopy();
-
-                MacAddress newMac = (addresses.mac() == null) ? existing.mac()
-                        : addresses.mac();
-
-                PortAddresses newAddresses =
-                        new PortAddresses(addresses.connectPoint(), union, newMac);
-
-                portAddresses.put(newAddresses.connectPoint(), newAddresses);
-            }
-        }
+        portAddresses.put(addresses.connectPoint(), addresses);
     }
 
     @Override
     public void removeAddressBindings(PortAddresses addresses) {
-        synchronized (portAddresses) {
-            PortAddresses existing = portAddresses.get(addresses.connectPoint());
-            if (existing != null) {
-                Set<InterfaceIpAddress> difference =
-                        Sets.difference(existing.ipAddresses(),
-                                        addresses.ipAddresses()).immutableCopy();
-
-                // If they removed the existing mac, set the new mac to null.
-                // Otherwise, keep the existing mac.
-                MacAddress newMac = existing.mac();
-                if (addresses.mac() != null && addresses.mac().equals(existing.mac())) {
-                    newMac = null;
-                }
-
-                PortAddresses newAddresses =
-                        new PortAddresses(addresses.connectPoint(), difference, newMac);
-
-                portAddresses.put(newAddresses.connectPoint(), newAddresses);
-            }
-        }
+        portAddresses.remove(addresses.connectPoint(), addresses);
     }
 
     @Override
     public void clearAddressBindings(ConnectPoint connectPoint) {
-        synchronized (portAddresses) {
-            portAddresses.remove(connectPoint);
-        }
+        portAddresses.removeAll(connectPoint);
     }
 
     @Override
     public Set<PortAddresses> getAddressBindings() {
         synchronized (portAddresses) {
-            return new HashSet<>(portAddresses.values());
+            return ImmutableSet.copyOf(portAddresses.values());
         }
     }
 
     @Override
-    public PortAddresses getAddressBindingsForPort(ConnectPoint connectPoint) {
-        PortAddresses addresses;
-
+    public Set<PortAddresses> getAddressBindingsForPort(ConnectPoint connectPoint) {
         synchronized (portAddresses) {
-            addresses = portAddresses.get(connectPoint);
-        }
+            Set<PortAddresses> addresses = portAddresses.get(connectPoint);
 
-        if (addresses == null) {
-            addresses = new PortAddresses(connectPoint, null, null);
+            if (addresses == null) {
+                return Collections.emptySet();
+            } else {
+                return ImmutableSet.copyOf(addresses);
+            }
         }
-
-        return addresses;
     }
 
     // Auxiliary extension to allow location to mutate.
diff --git a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkResourceStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkResourceStore.java
index f97c0ab..5369375 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkResourceStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleLinkResourceStore.java
@@ -195,7 +195,7 @@
     @Override
     public void releaseResources(LinkResourceAllocations allocations) {
         checkNotNull(allocations);
-        linkResourceAllocationsMap.remove(allocations);
+        linkResourceAllocationsMap.remove(allocations.intendId());
         for (Link link : allocations.links()) {
             addFreeResources(link, allocations);
             Set<LinkResourceAllocations> linkAllocs = allocatedResources.get(link);
diff --git a/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java b/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java
index f43ae10..cc65052 100644
--- a/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java
+++ b/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java
@@ -23,6 +23,7 @@
 import static org.onlab.onos.net.Device.Type.SWITCH;
 import static org.onlab.onos.net.DeviceId.deviceId;
 import static org.onlab.onos.net.device.DeviceEvent.Type.*;
+import static org.onlab.onos.net.NetTestTools.assertAnnotationsEquals;
 
 import java.util.Arrays;
 import java.util.HashMap;
@@ -38,7 +39,6 @@
 import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
-import org.onlab.onos.net.Annotations;
 import org.onlab.onos.net.DefaultAnnotations;
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.DeviceId;
@@ -56,6 +56,7 @@
 
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
+
 import org.onlab.packet.ChassisId;
 
 /**
@@ -146,25 +147,6 @@
         assertEquals(SN, device.serialNumber());
     }
 
-    // TODO slice this out somewhere
-    /**
-     * Verifies that Annotations created by merging {@code annotations} is
-     * equal to actual Annotations.
-     *
-     * @param actual Annotations to check
-     * @param annotations
-     */
-    public static void assertAnnotationsEquals(Annotations actual, SparseAnnotations... annotations) {
-        DefaultAnnotations expected = DefaultAnnotations.builder().build();
-        for (SparseAnnotations a : annotations) {
-            expected = DefaultAnnotations.merge(expected, a);
-        }
-        assertEquals(expected.keys(), actual.keys());
-        for (String key : expected.keys()) {
-            assertEquals(expected.value(key), actual.value(key));
-        }
-    }
-
     @Test
     public final void testGetDeviceCount() {
         assertEquals("initialy empty", 0, deviceStore.getDeviceCount());
diff --git a/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleLinkStoreTest.java b/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleLinkStoreTest.java
index 043cff3..735f99c 100644
--- a/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleLinkStoreTest.java
+++ b/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleLinkStoreTest.java
@@ -16,6 +16,7 @@
 package org.onlab.onos.store.trivial.impl;
 
 import com.google.common.collect.Iterables;
+
 import org.junit.After;
 import org.junit.AfterClass;
 import org.junit.Before;
@@ -46,7 +47,7 @@
 import static org.onlab.onos.net.DeviceId.deviceId;
 import static org.onlab.onos.net.Link.Type.*;
 import static org.onlab.onos.net.link.LinkEvent.Type.*;
-import static org.onlab.onos.store.trivial.impl.SimpleDeviceStoreTest.assertAnnotationsEquals;
+import static org.onlab.onos.net.NetTestTools.assertAnnotationsEquals;
 
 /**
  * Test of the simple LinkStore implementation.
diff --git a/docs/external.xml b/docs/external.xml
index 7942d80..5b73366 100644
--- a/docs/external.xml
+++ b/docs/external.xml
@@ -49,7 +49,7 @@
                 <version>2.10.1</version>
                 <configuration>
                     <show>package</show>
-                    <excludePackageNames>org.onlab.thirdparty:*.impl:*.impl.*:org.onlab.onos.provider.*:org.onlab.onos.gui:org.onlab.onos.rest:org.onlab.onos.cli*:org.onlab.onos.tvue:org.onlab.onos.foo:org.onlab.onos.mobility:org.onlab.onos.proxyarp:org.onlab.onos.fwd:org.onlab.onos.ifwd:org.onlab.onos.optical:org.onlab.onos.config:org.onlab.onos.calendar:org.onlab.onos.sdnip*:org.onlab.onos.metrics:org.onlab.onos.store.*:org.onlab.onos.openflow.*</excludePackageNames>
+                    <excludePackageNames>org.onlab.thirdparty:*.impl:*.impl.*:org.onlab.onos.provider.*:org.onlab.onos.gui:org.onlab.onos.rest:org.onlab.onos.cli*:org.onlab.onos.tvue:org.onlab.onos.foo:org.onlab.onos.mobility:org.onlab.onos.proxyarp:org.onlab.onos.fwd:org.onlab.onos.ifwd:org.onlab.onos.optical:org.onlab.onos.config:org.onlab.onos.calendar:org.onlab.onos.sdnip*:org.onlab.onos.oecfg:org.onlab.onos.metrics:org.onlab.onos.store.*:org.onlab.onos.openflow.*</excludePackageNames>
                     <docfilessubdirs>true</docfilessubdirs>
                     <doctitle>ONOS Java API</doctitle>
                     <groups>
diff --git a/docs/pom.xml b/docs/pom.xml
index d2581eb..c0d1fcd 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -48,7 +48,6 @@
                 <artifactId>maven-javadoc-plugin</artifactId>
                 <version>2.10.1</version>
                 <configuration>
-                    <!--additionalparam>-Xdoclint:none</additionalparam-->
                     <show>package</show>
                     <docfilessubdirs>true</docfilessubdirs>
                     <doctitle>ONOS Java API</doctitle>
@@ -87,7 +86,13 @@
                         <group>
                             <title>Sample Applications</title>
                             <packages>
-                                org.onlab.onos.tvue:org.onlab.onos.fwd:org.onlab.onos.ifwd:org.onlab.onos.mobility:org.onlab.onos.proxyarp:org.onlab.onos.foo:org.onlab.onos.calendar:org.onlab.onos.sdnip:org.onlab.onos.sdnip.*:org.onlab.onos.optical:org.onlab.onos.optical.*:org.onlab.onos.metrics.*:org.onlab.onos.config
+                                org.onlab.onos.tvue:org.onlab.onos.fwd:org.onlab.onos.ifwd:org.onlab.onos.mobility:org.onlab.onos.proxyarp:org.onlab.onos.foo:org.onlab.onos.calendar:org.onlab.onos.optical.*:org.onlab.onos.sdnip:org.onlab.onos.sdnip.*:org.onlab.onos.config
+                            </packages>
+                        </group>
+                        <group>
+                            <title>Test Instrumentation</title>
+                            <packages>
+                                org.onlab.onos.metrics.*:org.onlab.onos.oecfg
                             </packages>
                         </group>
                     </groups>
diff --git a/features/features.xml b/features/features.xml
index 088e42f..2e0718f 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -56,6 +56,9 @@
         <bundle>mvn:org.codehaus.jackson/jackson-mapper-asl/1.9.13</bundle>
 
         <bundle>mvn:org.onlab.onos/onlab-thirdparty/1.0.0-SNAPSHOT</bundle>
+
+        <!-- FIXME fix the version before release -->
+        <bundle>wrap:mvn:http://mavenrepo.onlab.us:8081/nexus/content/groups/public@id=onlab-temp!net.kuujo.copycat/copycat/0.4.0-SNAPSHOT$Bundle-SymbolicName=net.kuujo.copycat.copycat&amp;Bundle-Version=0.4.0.SNAPSHOT</bundle>
     </feature>
 
     <feature name="onos-thirdparty-web" version="1.0.0"
diff --git a/openflow/api/pom.xml b/openflow/api/pom.xml
index de9246b..a6dedd6 100644
--- a/openflow/api/pom.xml
+++ b/openflow/api/pom.xml
@@ -31,15 +31,6 @@
 
     <description>ONOS OpenFlow controller subsystem API</description>
 
-    <repositories>
-        <!-- FIXME: for Loxigen + optical experimenter. Decide how to use Loxigen before release. -->
-        <repository>
-            <id>onlab-temp</id>
-            <name>ON.lab temporary repository</name>
-            <url>http://mavenrepo.onlab.us:8081/nexus/content/repositories/releases</url>
-        </repository>
-    </repositories>
-
     <dependencies>
         <dependency>
             <groupId>org.projectfloodlight</groupId>
@@ -50,7 +41,6 @@
         <dependency>
             <groupId>io.netty</groupId>
             <artifactId>netty</artifactId>
-            <version>3.9.0.Final</version>
         </dependency>
     </dependencies>
 
diff --git a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowEventListener.java b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowEventListener.java
index bc842a0..fc9a872 100644
--- a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowEventListener.java
+++ b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowEventListener.java
@@ -26,6 +26,7 @@
     /**
      * Handles the message event.
      *
+     * @param dpid switch data path identifier
      * @param msg the message
      */
     public void handleMessage(Dpid dpid, OFMessage msg);
diff --git a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowPacketContext.java b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowPacketContext.java
index 13092b2..0d085dd 100644
--- a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowPacketContext.java
+++ b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowPacketContext.java
@@ -31,6 +31,7 @@
     /**
      * Blocks further responses (ie. send() calls) on this
      * packet in event.
+     * @return true if blocks
      */
     public boolean block();
 
diff --git a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitch.java b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitch.java
index 2d3f890..bd90241 100644
--- a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitch.java
+++ b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitch.java
@@ -133,7 +133,7 @@
      * @param requested the role requested by the controller
      * @param response the role set at the device
      */
-    public void returnRoleReply(RoleState requested, RoleState reponse);
+    public void returnRoleReply(RoleState requested, RoleState response);
 
     /**
      * Indicates if this switch is optical.
diff --git a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitchListener.java b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitchListener.java
index 192f045..3b6450b 100644
--- a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitchListener.java
+++ b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/OpenFlowSwitchListener.java
@@ -51,7 +51,8 @@
      * Notify that a role imposed on a switch failed to take hold.
      *
      * @param dpid the switch that failed role assertion
-     * @param role the role imposed by the controller
+     * @param requested the role controller requested
+     * @param response role reply from the switch
      */
     public void receivedRoleReply(Dpid dpid, RoleState requested, RoleState response);
 }
diff --git a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/OpenFlowAgent.java b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/OpenFlowAgent.java
index 6b73efc..468d3ae 100644
--- a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/OpenFlowAgent.java
+++ b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/OpenFlowAgent.java
@@ -95,7 +95,8 @@
      * Notifies the controller that role assertion has failed.
      *
      * @param dpid the switch that failed role assertion
-     * @param role the failed role
+     * @param requested the role controller requested
+     * @param response role reply from the switch
      */
     public void returnRoleReply(Dpid dpid, RoleState requested, RoleState response);
 }
diff --git a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/RoleHandler.java b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/RoleHandler.java
index 8611664..c32d298 100644
--- a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/RoleHandler.java
+++ b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/RoleHandler.java
@@ -58,8 +58,9 @@
      * OF1.3 switches, because Role.EQUAL is well defined and we can simulate
      * SLAVE behavior by using ASYNC messages.
      *
-     * @param role
-     * @throws IOException
+     * @param role role to request
+     * @param exp expectation
+     * @throws IOException when I/O exception of some sort has occurred
      * @return false if and only if the switch does not support role-request
      * messages, according to the switch driver; true otherwise.
      */
@@ -70,7 +71,7 @@
      * Extract the role information from an OF1.3 Role Reply Message.
      * @param rrmsg role reply message
      * @return RoleReplyInfo object
-     * @throws SwitchStateException
+     * @throws SwitchStateException If unknown role encountered
      */
     public RoleReplyInfo extractOFRoleReply(OFRoleReply rrmsg)
             throws SwitchStateException;
@@ -89,6 +90,7 @@
      *
      * @param rri information about role-reply in format that
      *                      controller can understand.
+     * @return result comparing expected and received reply
      * @throws SwitchStateException if no request is pending
      */
     public RoleRecvStatus deliverRoleReply(RoleReplyInfo rri)
@@ -102,6 +104,9 @@
      * Note: since we only keep the last pending request we might get
      * error messages for earlier role requests that we won't be able
      * to handle
+     * @param error error message
+     * @return result comparing expected and received reply
+     * @throws SwitchStateException if switch did not support requested role
      */
     public RoleRecvStatus deliverError(OFErrorMsg error)
             throws SwitchStateException;
diff --git a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/RoleReplyInfo.java b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/RoleReplyInfo.java
index b408c44..6efb333 100644
--- a/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/RoleReplyInfo.java
+++ b/openflow/api/src/main/java/org/onlab/onos/openflow/controller/driver/RoleReplyInfo.java
@@ -32,9 +32,15 @@
         this.genId = genId;
         this.xid = xid;
     }
-    public RoleState getRole() { return role; }
-    public U64 getGenId() { return genId; }
-    public long getXid() { return xid; }
+    public RoleState getRole() {
+        return role;
+    }
+    public U64 getGenId() {
+        return genId;
+    }
+    public long getXid() {
+        return xid;
+    }
     @Override
     public String toString() {
         return "[Role:" + role + " GenId:" + genId + " Xid:" + xid + "]";
diff --git a/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/Controller.java b/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/Controller.java
index d79227a..1d0e4cb 100644
--- a/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/Controller.java
+++ b/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/Controller.java
@@ -160,6 +160,7 @@
 
     /**
      * Initialize internal data structures.
+     * @param configParams configuration parameters
      */
     public void init(Map<String, String> configParams) {
         // These data structures are initialized here because other
@@ -192,7 +193,9 @@
 
     /**
      * Forward to the driver-manager to get an IOFSwitch instance.
-     * @param desc
+     * @param dpid data path id
+     * @param desc switch description
+     * @param ofv OpenFlow version
      * @return switch instance
      */
     protected OpenFlowSwitchDriver getOFSwitchInstance(long dpid,
diff --git a/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OFChannelHandler.java b/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OFChannelHandler.java
index 8dbff60..ca5f4f5 100644
--- a/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OFChannelHandler.java
+++ b/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/OFChannelHandler.java
@@ -115,7 +115,7 @@
 
     /**
      * Create a new unconnected OFChannelHandler.
-     * @param controller
+     * @param controller parent controller
      */
     OFChannelHandler(Controller controller) {
         this.controller = controller;
@@ -794,7 +794,7 @@
          * @param m The PortStatus message we received
          * @param doNotify if true switch port changed events will be
          * dispatched
-         * @throws SwitchStateException
+         * @throws SwitchStateException if the switch is not bound to the channel
          *
          */
         protected void handlePortStatusMessage(OFChannelHandler h, OFPortStatus m,
@@ -826,8 +826,8 @@
          *
          * @param h The OFChannelHandler that received the message
          * @param m The message we received.
-         * @throws SwitchStateException
-         * @throws IOException
+         * @throws SwitchStateException if the switch is not bound to the channel
+         * @throws IOException if unable to send message back to the switch
          */
         void processOFMessage(OFChannelHandler h, OFMessage m)
                 throws IOException, SwitchStateException {
diff --git a/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/RoleManager.java b/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/RoleManager.java
index 6e5b236..734e0ba 100644
--- a/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/RoleManager.java
+++ b/openflow/ctl/src/main/java/org/onlab/onos/openflow/controller/impl/RoleManager.java
@@ -347,7 +347,7 @@
 
         RoleState role = null;
         OFNiciraControllerRole ncr = nrr.getRole();
-        switch(ncr) {
+        switch (ncr) {
         case ROLE_MASTER:
             role = RoleState.MASTER;
             break;
@@ -383,7 +383,7 @@
             throws SwitchStateException {
         OFControllerRole cr = rrmsg.getRole();
         RoleState role = null;
-        switch(cr) {
+        switch (cr) {
         case ROLE_EQUAL:
             role = RoleState.EQUAL;
             break;
diff --git a/pom.xml b/pom.xml
index 83ed0f4..dfc86a3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,6 +42,20 @@
         <netty4.version>4.0.23.Final</netty4.version>
     </properties>
 
+    <repositories>
+        <!-- FIXME: Repository for copycat, Loxigen + optical experimenter.
+                    Decide how to deal with these before release. -->
+        <repository>
+            <id>onlab-temp</id>
+            <name>ON.lab temporary repository</name>
+            <url>http://mavenrepo.onlab.us:8081/nexus/content/groups/public</url>
+            <snapshots>
+                <enabled>true</enabled>
+            </snapshots>
+        </repository>
+    </repositories>
+
+
     <dependencyManagement>
         <dependencies>
             <dependency>
@@ -182,7 +196,7 @@
             <dependency>
                 <groupId>org.apache.karaf.shell</groupId>
                 <artifactId>org.apache.karaf.shell.console</artifactId>
-                <version>3.0.1</version>
+                <version>3.0.2</version>
                 <scope>provided</scope>
             </dependency>
 
@@ -318,6 +332,7 @@
                 <groupId>io.netty</groupId>
                 <artifactId>netty-transport-native-epoll</artifactId>
                 <version>${netty4.version}</version>
+                <classifier>${os.detected.classifier}</classifier>
             </dependency>
             <dependency>
                 <groupId>joda-time</groupId>
@@ -360,8 +375,8 @@
                     <!-- https://jira.codehaus.org/browse/MCOMPILER-205 -->
                     <version>2.5.1</version>
                     <configuration>
-                        <source>1.7</source>
-                        <target>1.7</target>
+                        <source>1.8</source>
+                        <target>1.8</target>
                     </configuration>
                 </plugin>
 
@@ -399,14 +414,20 @@
                 <plugin>
                     <groupId>org.apache.felix</groupId>
                     <artifactId>maven-bundle-plugin</artifactId>
-                    <version>2.3.7</version>
+                    <version>2.5.3</version>
                     <extensions>true</extensions>
                 </plugin>
 
                 <plugin>
+                    <groupId>org.codehaus.mojo</groupId>
+                    <artifactId>findbugs-maven-plugin</artifactId>
+                    <version>3.0.0</version>
+                </plugin>
+
+                <plugin>
                     <groupId>org.apache.felix</groupId>
                     <artifactId>maven-scr-plugin</artifactId>
-                    <version>1.15.0</version>
+                    <version>1.20.0</version>
                     <executions>
                         <execution>
                             <id>generate-scr-srcdescriptor</id>
@@ -472,6 +493,12 @@
                         <artifactId>onos-build-conf</artifactId>
                         <version>1.0</version>
                     </dependency>
+                    <!-- For Java 8 lambda support-->
+                    <dependency>
+                        <groupId>com.puppycrawl.tools</groupId>
+                        <artifactId>checkstyle</artifactId>
+                        <version>5.9</version>
+                    </dependency>
                 </dependencies>
                 <configuration>
                     <configLocation>onos/checkstyle.xml</configLocation>
diff --git a/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java b/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java
index a20ab90..db52454 100644
--- a/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java
+++ b/providers/lldp/src/main/java/org/onlab/onos/provider/lldp/impl/LinkDiscovery.java
@@ -82,7 +82,7 @@
     private final PacketService pktService;
     private final MastershipService mastershipService;
     private Timeout timeout;
-    private boolean isStopped;
+    private volatile boolean isStopped;
 
     /**
      * Instantiates discovery manager for the given physical switch. Creates a
@@ -90,7 +90,9 @@
      * Starts the the timer for the discovery process.
      *
      * @param device        the physical switch
+     * @param pktService    packet service
      * @param masterService mastership service
+     * @param providerService link provider service
      * @param useBDDP       flag to also use BDDP for discovery
      */
     public LinkDiscovery(Device device, PacketService pktService,
@@ -201,6 +203,8 @@
     /**
      * Handles an incoming LLDP packet. Creates link in topology and sends ACK
      * to port where LLDP originated.
+     * @param context packet context
+     * @return true if handled
      */
     public boolean handleLLDP(PacketContext context) {
         Ethernet eth = context.inPacket().parsed();
@@ -239,8 +243,10 @@
     public void run(final Timeout t) {
         boolean isMaster = mastershipService.getLocalRole(device.id()) == MASTER;
         if (!isMaster) {
-            // reschedule timer
-            timeout = Timer.getTimer().newTimeout(this, this.probeRate, MILLISECONDS);
+            if (!isStopped()) {
+                // reschedule timer
+                timeout = Timer.getTimer().newTimeout(this, this.probeRate, MILLISECONDS);
+            }
             return;
         }
 
@@ -276,16 +282,18 @@
             }
         }
 
-        // reschedule timer
-        timeout = Timer.getTimer().newTimeout(this, this.probeRate, MILLISECONDS);
+        if (!isStopped()) {
+            // reschedule timer
+            timeout = Timer.getTimer().newTimeout(this, this.probeRate, MILLISECONDS);
+        }
     }
 
-    public void stop() {
+    public synchronized void stop() {
         timeout.cancel();
         isStopped = true;
     }
 
-    public void start() {
+    public synchronized void start() {
         if (isStopped) {
             timeout = Timer.getTimer().newTimeout(this, 0, MILLISECONDS);
             isStopped = false;
diff --git a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowEntryBuilder.java b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowEntryBuilder.java
index 02ec827..e5131df 100644
--- a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowEntryBuilder.java
+++ b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowEntryBuilder.java
@@ -289,7 +289,10 @@
             case OCH_SIGID:
                 builder.matchLambda(match.get(MatchField.OCH_SIGID).getChannelNumber());
                 break;
-            case OCH_SIGTYPE_BASIC:
+            case OCH_SIGTYPE:
+                builder.matchOpticalSignalType(match.get(MatchField
+                                                                 .OCH_SIGTYPE).getValue());
+                break;
             case ARP_OP:
             case ARP_SHA:
             case ARP_SPA:
diff --git a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java
index b191784..0caf06b 100644
--- a/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java
+++ b/providers/openflow/flow/src/main/java/org/onlab/onos/provider/of/flow/impl/FlowModBuilder.java
@@ -19,6 +19,7 @@
 
 import org.onlab.onos.net.flow.FlowRule;
 import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.criteria.Criteria;
 import org.onlab.onos.net.flow.criteria.Criteria.EthCriterion;
 import org.onlab.onos.net.flow.criteria.Criteria.EthTypeCriterion;
 import org.onlab.onos.net.flow.criteria.Criteria.IPCriterion;
@@ -46,6 +47,7 @@
 import org.projectfloodlight.openflow.types.OFPort;
 import org.projectfloodlight.openflow.types.OFVlanVidMatch;
 import org.projectfloodlight.openflow.types.TransportPort;
+import org.projectfloodlight.openflow.types.U8;
 import org.projectfloodlight.openflow.types.VlanPcp;
 import org.projectfloodlight.openflow.types.VlanVid;
 import org.slf4j.Logger;
@@ -197,6 +199,12 @@
                 mBuilder.setExact(MatchField.OCH_SIGID,
                         new CircuitSignalID((byte) 1, (byte) 2, lc.lambda(), (short) 1));
                 break;
+            case OCH_SIGTYPE:
+                Criteria.OpticalSignalTypeCriterion sc =
+                        (Criteria.OpticalSignalTypeCriterion) c;
+                mBuilder.setExact(MatchField.OCH_SIGTYPE,
+                                  U8.of(sc.signalType()));
+                break;
             case ARP_OP:
             case ARP_SHA:
             case ARP_SPA:
diff --git a/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/LinkDiscovery.java b/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/LinkDiscovery.java
index 02176cc..00c7305 100644
--- a/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/LinkDiscovery.java
+++ b/providers/openflow/link/src/main/java/org/onlab/onos/provider/of/link/impl/LinkDiscovery.java
@@ -87,7 +87,7 @@
     protected final Map<Integer, OFPortDesc> ports;
     private Timeout timeout;
 
-    /**
+    /*
      * Instantiates discovery manager for the given physical switch. Creates a
      * generic LLDP packet that will be customized for the port it is sent out on.
      * Starts the the timer for the discovery process.
@@ -281,7 +281,7 @@
         return "LinkDiscovery " + this.sw.getStringId();
     }
 
-    /**
+    /*
      * Handles an incoming LLDP packet. Creates link in topology and sends ACK
      * to port where LLDP originated.
      */
diff --git a/tools/build/envDefaults b/tools/build/envDefaults
index 184ad33..bfe9366 100644
--- a/tools/build/envDefaults
+++ b/tools/build/envDefaults
@@ -5,7 +5,7 @@
 
 # M2 repository and Karaf gold bits
 export M2_REPO=${M2_REPO:-~/.m2/repository}
-export KARAF_VERSION=${KARAF_VERSION:-3.0.1}
+export KARAF_VERSION=${KARAF_VERSION:-3.0.2}
 export KARAF_ZIP=${KARAF_ZIP:-~/Downloads/apache-karaf-$KARAF_VERSION.zip}
 export KARAF_TAR=${KARAF_TAR:-~/Downloads/apache-karaf-$KARAF_VERSION.tar.gz}
 export KARAF_DIST=$(basename $KARAF_ZIP .zip)
diff --git a/tools/dev/bash_profile b/tools/dev/bash_profile
index 1a0c93f..7b21916 100644
--- a/tools/dev/bash_profile
+++ b/tools/dev/bash_profile
@@ -8,7 +8,9 @@
 # Setup some environmental context for developers
 if [ -z "${JAVA_HOME}" ]; then
     if [ -x /usr/libexec/java_home ]; then
-        export JAVA_HOME=$(/usr/libexec/java_home -v 1.7)
+        export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)
+    elif [ -d /usr/lib/jvm/java-8-oracle ]; then
+        export JAVA_HOME="/usr/lib/jvm/java-8-oracle"
     elif [ -d /usr/lib/jvm/java-7-openjdk-amd64 ]; then
         export JAVA_HOME="/usr/lib/jvm/java-7-openjdk-amd64"
     fi
@@ -16,7 +18,7 @@
 
 export MAVEN=${MAVEN:-~/Applications/apache-maven-3.2.2}
 
-export KARAF_VERSION=${KARAF_VERSION:-3.0.1}
+export KARAF_VERSION=${KARAF_VERSION:-3.0.2}
 export KARAF=${KARAF:-~/Applications/apache-karaf-$KARAF_VERSION}
 export KARAF_LOG=$KARAF/data/log/karaf.log
 
@@ -39,6 +41,7 @@
 alias ob='onos-build'
 alias obi='onos-build -Dmaven.test.failure.ignore=true'
 alias obs='onos-build-selective'
+alias obd='onos-build-docs'
 alias op='onos-package'
 alias ot='onos-test'
 alias ol='onos-log'
diff --git a/tools/dev/bin/onos-local-log b/tools/dev/bin/onos-local-log
index 83bdf9c..ea93d7f 100755
--- a/tools/dev/bin/onos-local-log
+++ b/tools/dev/bin/onos-local-log
@@ -2,7 +2,7 @@
 # ----------------------------------------------------------------------------
 # Continuously watches the Apache Karaf log; survives 'karaf clean'
 # ----------------------------------------------------------------------------
-KARAF_LOG=${KARAF_LOG:-~/apache-karaf-3.0.1/data/log/karaf.log}
+KARAF_LOG=${KARAF_LOG:-~/apache-karaf-3.0.2/data/log/karaf.log}
 
 while true; do
     [ ! -f $KARAF_LOG ] && sleep 2 && continue
diff --git a/tools/dev/header.txt b/tools/dev/header.txt
new file mode 100644
index 0000000..6c18c92
--- /dev/null
+++ b/tools/dev/header.txt
@@ -0,0 +1,13 @@
+Copyright $today.year 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.
\ No newline at end of file
diff --git a/tools/dev/onos.cshrc b/tools/dev/onos.cshrc
index feff165..17d1f06 100644
--- a/tools/dev/onos.cshrc
+++ b/tools/dev/onos.cshrc
@@ -16,7 +16,9 @@
 # Setup some environmental context for developers
 if ( ! $?JAVA_HOME ) then
     if ( -x /usr/libexec/java_home ) then
-        setenv JAVA_HOME `/usr/libexec/java_home -v 1.7`
+        setenv JAVA_HOME `/usr/libexec/java_home -v 1.8`
+    else if ( -d /usr/lib/jvm/java-8-oracle ) then
+        setenv JAVA_HOME /usr/lib/jvm/java-8-oracle
     else if ( -d /usr/lib/jvm/java-7-openjdk-amd64 ) then
         setenv JAVA_HOME /usr/lib/jvm/java-7-openjdk-amd64
     endif
@@ -24,8 +26,11 @@
 if ( ! $?MAVEN ) then
     setenv MAVEN $HOME/Applications/apache-maven-3.2.2
 endif
+if ( ! $?KARAF_VERSION ) then
+    setenv KARAF_VERSION 3.0.2
+endif
 if ( ! $?KARAF ) then
-    setenv KARAF $HOME/Applications/apache-karaf-3.0.1
+    setenv KARAF $HOME/Applications/apache-karaf-$KARAF_VERSION
 endif
 setenv KARAF_LOG $KARAF/data/log/karaf.log
 
diff --git a/tools/package/bin/onos b/tools/package/bin/onos
index c94304e..84a41e0 100755
--- a/tools/package/bin/onos
+++ b/tools/package/bin/onos
@@ -3,7 +3,15 @@
 # ONOS command-line client
 # -----------------------------------------------------------------------------
 
-export JAVA_HOME=${JAVA_HOME:-/usr/lib/jvm/java-7-openjdk-amd64/}
+if [ -z "${JAVA_HOME}" ]; then
+    if [ -x /usr/libexec/java_home ]; then
+        export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)
+    elif [ -d /usr/lib/jvm/java-8-oracle ]; then
+        export JAVA_HOME="/usr/lib/jvm/java-8-oracle"
+    elif [ -d /usr/lib/jvm/java-7-openjdk-amd64 ]; then
+        export JAVA_HOME="/usr/lib/jvm/java-7-openjdk-amd64"
+    fi
+fi
 
 cd $(dirname $0)/../apache-karaf-$KARAF_VERSION/bin
 ./client -h localhost -u karaf "$@"
diff --git a/tools/package/bin/onos-service b/tools/package/bin/onos-service
index 9250d5b..ae6d970 100755
--- a/tools/package/bin/onos-service
+++ b/tools/package/bin/onos-service
@@ -3,7 +3,7 @@
 # Starts ONOS Apache Karaf container
 # -----------------------------------------------------------------------------
 
-export JAVA_HOME=${JAVA_HOME:-/usr/lib/jvm/java-7-openjdk-amd64/}
+#export JAVA_HOME=${JAVA_HOME:-/usr/lib/jvm/java-7-openjdk-amd64/}
 export JAVA_OPTS="${JAVA_OPTS:--Xms256M -Xmx2048M}"
 
 cd /opt/onos
diff --git a/tools/package/debian/onos.conf b/tools/package/debian/onos.conf
index 1d8f10a..888c02b 100644
--- a/tools/package/debian/onos.conf
+++ b/tools/package/debian/onos.conf
@@ -11,8 +11,8 @@
 respawn
 
 env LANG=en_US.UTF-8
-env JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64
-env NEW_JAVA_HOME=/usr/lib/jvm/java-8-oracle/
+#env JAVA_HOME=/usr/lib/jvm/java-7-openjdk-amd64
+#env NEW_JAVA_HOME=/usr/lib/jvm/java-8-oracle/
 
 pre-stop script
     /opt/onos/bin/onos halt 2>/opt/onos/var/stderr.log
diff --git a/tools/test/bin/onos-log b/tools/test/bin/onos-log
index ff52eee..38ff50c 100755
--- a/tools/test/bin/onos-log
+++ b/tools/test/bin/onos-log
@@ -19,11 +19,11 @@
     LOG=$ONOS_INSTALL_DIR/$KARAF_DIST/instances/$instance/data/log/karaf.log || \
     LOG=$ONOS_INSTALL_DIR/log/karaf.log
 
-trap "ssh $remote 'ps -ef | grep \"tail -n 512\" | grep -v grep | cut -c10-15 | xargs kill'" EXIT
 
 if [ $less -eq 1 ]; then
     ssh -t $remote "less $LOG"
 else
+    trap "ssh $remote 'ps -ef | grep \"tail -n 512\" | grep -v grep | cut -c10-15 | xargs kill'" EXIT
     ssh $remote "
     while true; do
         echo ==================================================================
diff --git a/tools/test/cells/local b/tools/test/cells/local
index e8ca7e9..acf76ef 100644
--- a/tools/test/cells/local
+++ b/tools/test/cells/local
@@ -6,4 +6,4 @@
 export OCN="192.168.56.103"
 export OCI="${OC1}"
 
-export ONOS_FEATURES="${ONOS_FEATURES:-webconsole,onos-api,onos-core,onos-cli,onos-openflow,onos-rest,onos-app-fwd,onos-app-proxyarp,onos-app-tvue}"
+export ONOS_FEATURES="${ONOS_FEATURES:-webconsole,onos-api,onos-core,onos-cli,onos-openflow,onos-gui,onos-rest,onos-app-fwd,onos-app-proxyarp,onos-app-tvue}"
diff --git a/tools/test/cells/triple b/tools/test/cells/triple
index 439b837..880dd55 100644
--- a/tools/test/cells/triple
+++ b/tools/test/cells/triple
@@ -7,4 +7,4 @@
 export OCN="192.168.56.103"
 export OCI="${OC1}"
 
-export ONOS_FEATURES=""
+export ONOS_FEATURES="${ONOS_FEATURES:-webconsole,onos-api,onos-core,onos-cli,onos-openflow,onos-gui,onos-rest,onos-app-fwd,onos-app-proxyarp,onos-app-tvue}"
diff --git a/tools/test/topos/onos.py b/tools/test/topos/onos.py
index 9566d00..80a8005 100755
--- a/tools/test/topos/onos.py
+++ b/tools/test/topos/onos.py
@@ -36,7 +36,7 @@
         #self.checkListening()
 
         self.onosDir = onosDir
-        self.karafDir = onosDir + 'apache-karaf-3.0.1/'
+        self.karafDir = onosDir + 'apache-karaf-3.0.2/'
         self.instanceDir = self.karafDir
 
         # add default modules
diff --git a/utils/junit/src/test/java/org/onlab/junit/TestUtilsTest.java b/utils/junit/src/test/java/org/onlab/junit/TestUtilsTest.java
index 39712ba..68e407f 100644
--- a/utils/junit/src/test/java/org/onlab/junit/TestUtilsTest.java
+++ b/utils/junit/src/test/java/org/onlab/junit/TestUtilsTest.java
@@ -101,9 +101,9 @@
     @Test
     public void testSetGetPrivateField() throws TestUtilsException {
 
-        assertEquals(42, TestUtils.getField(test, "privateField"));
+        assertEquals(42, (int) TestUtils.getField(test, "privateField"));
         TestUtils.setField(test, "privateField", 0xDEAD);
-        assertEquals(0xDEAD, TestUtils.getField(test, "privateField"));
+        assertEquals(0xDEAD, (int) TestUtils.getField(test, "privateField"));
     }
 
     /**
@@ -114,9 +114,9 @@
     @Test
     public void testSetGetProtectedField() throws TestUtilsException {
 
-        assertEquals(2501, TestUtils.getField(test, "protectedField"));
+        assertEquals(2501, (int) TestUtils.getField(test, "protectedField"));
         TestUtils.setField(test, "protectedField", 0xBEEF);
-        assertEquals(0xBEEF, TestUtils.getField(test, "protectedField"));
+        assertEquals(0xBEEF, (int) TestUtils.getField(test, "protectedField"));
     }
 
     /**
diff --git a/utils/misc/src/main/java/org/onlab/graph/KshortestPathSearch.java b/utils/misc/src/main/java/org/onlab/graph/KshortestPathSearch.java
new file mode 100644
index 0000000..c01c0d0
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/graph/KshortestPathSearch.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.onlab.graph;
+
+import java.util.ArrayList;
+//import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+//import java.util.Map;
+//import java.util.PriorityQueue;
+import java.util.Set;
+
+/**
+ * K-shortest-path graph search algorithm capable of finding not just one,
+ * but K shortest paths with ascending order between the source and destinations.
+ */
+
+public class KshortestPathSearch<V extends Vertex, E extends Edge<V>> {
+
+    // Define class variables.
+    private Graph<V, E> immutableGraph;
+    private MutableGraph<V, E> mutableGraph;
+    private List<List<E>> pathResults = new ArrayList<List<E>>();
+    private List<List<E>> pathCandidates = new ArrayList<List<E>>();
+    private V source;
+    private V sink;
+    private int numK = 0;
+    private EdgeWeight<V, E> weight =  null;
+    // private PriorityQueue<List<E>> pathCandidates = new PriorityQueue<List<E>>();
+
+    // Initialize the graph.
+    public KshortestPathSearch(Graph<V, E> graph) {
+        immutableGraph = graph;
+        mutableGraph = new MutableAdjacencyListsGraph(graph.getVertexes(),
+                graph.getEdges());
+    }
+
+    public List<List<E>> search(V src,
+            V dst,
+            EdgeWeight<V, E> wei,
+            int k) {
+
+        weight = wei;
+        source = src;
+        sink = dst;
+        numK = k;
+        // pathCandidates = new PriorityQueue<List<E>>();
+
+        pathResults.clear();
+        pathCandidates.clear();
+
+        // Double check the parameters
+        checkArguments(immutableGraph, src, dst, numK);
+
+        // DefaultResult result = new DefaultResult(src, dst);
+
+        searchKShortestPaths();
+
+        return pathResults;
+    }
+
+    private void checkArguments(Graph<V, E> graph, V src, V dst, int k) {
+            if (graph == null) {
+                throw new NullPointerException("graph is null");
+            }
+            if (!graph.getVertexes().contains(src)) {
+                throw new NullPointerException("source node does not exist");
+            }
+            if (!graph.getVertexes().contains(dst)) {
+                throw new NullPointerException("target node does not exist");
+            }
+            if (k <= 0) {
+                throw new NullPointerException("K is negative or 0");
+            }
+            if (weight == null) {
+                throw new NullPointerException("the cost matrix is null");
+            }
+    }
+
+    private void searchKShortestPaths() {
+            // Step 1: find the shortest path.
+            List<E> shortestPath = searchShortestPath(immutableGraph, source, sink);
+            // no path exists, exit.
+            if (shortestPath == null) {
+                return;
+            }
+
+            // Step 2: update the results.
+            pathResults.add(shortestPath);
+            // pathCandidates.add(shortestPath);
+
+            // Step 3: find the other K-1 paths.
+            while (/*pathCandidates.size() > 0 &&*/pathResults.size() < numK) {
+                // 3.1 the spur node ranges from the first node to the last node in the previous k-shortest path.
+                List<E> lastPath = pathResults.get(pathResults.size() - 1);
+                for (int i = 0; i < lastPath.size(); i++) {
+                    // 3.1.1 convert the graph into mutable.
+                    convertGraph();
+                    // 3.1.2 transform the graph.
+                    List<E> rootPath = createSpurNode(lastPath, i);
+                    transformGraph(rootPath);
+                    // 3.1.3 find the deviation node.
+                    V devNode;
+                    devNode = getDevNode(rootPath);
+                    List<E> spurPath;
+                    // 3.1.4 find the shortest path in the transformed graph.
+                    spurPath = searchShortestPath(mutableGraph, devNode, sink);
+                    // 3.1.5 update the path candidates.
+                    if (spurPath != null) {
+                        // totalPath = rootPath + spurPath;
+                        rootPath.addAll(spurPath);
+                        pathCandidates.add(rootPath);
+                    }
+                }
+                // 3.2 if there is no spur path, exit.
+                if (pathCandidates.size() == 0) {
+                    break;
+                }
+                 // 3.3 add the path into the results.
+                addPathResult();
+            }
+        }
+
+    private List<E> searchShortestPath(Graph<V, E> graph, V src, V dst) {
+        // Determine the shortest path from the source to the destination by using the Dijkstra algorithm.
+        DijkstraGraphSearch dijkstraAlg = new DijkstraGraphSearch();
+        Set<Path> paths =  dijkstraAlg.search(graph, src, dst, weight).paths();
+        Iterator<Path> itr = paths.iterator();
+        if (!itr.hasNext()) {
+            return null;
+        }
+        // return the first shortest path only.
+        return (List<E>) itr.next().edges();
+    }
+
+    private void convertGraph() {
+        // clear the mutableGraph first
+        if (mutableGraph != null) {
+            ((MutableAdjacencyListsGraph) mutableGraph).clear();
+        }
+
+        // create a immutableGraph
+        Set<E> copyEa = immutableGraph.getEdges();
+        Set<V> copyVa = immutableGraph.getVertexes();
+        for (V vertex : copyVa) {
+            mutableGraph.addVertex(vertex);
+            }
+        for (E edge : copyEa) {
+            mutableGraph.addEdge(edge);
+            }
+    }
+
+    private V getDevNode(List<E> path) {
+            V srcA;
+            V dstB;
+
+            if (path.size() == 0) {
+                return source;
+            }
+
+            E temp1 = path.get(path.size() - 1);
+            srcA = temp1.src();
+            dstB = temp1.dst();
+
+            if (path.size() == 1) {
+                if (srcA.equals(source)) {
+                    return dstB;
+                } else {
+                    return srcA;
+                }
+            } else {
+                E temp2 = path.get(path.size() - 2);
+                if (srcA.equals(temp2.src()) || srcA.equals(temp2.dst())) {
+                    return dstB;
+                } else {
+                    return srcA;
+                }
+            }
+          }
+
+     private List<E> createSpurNode(List<E> path, int n) {
+            List<E> root = new ArrayList<E>();
+
+            for (int i = 0; i < n; i++) {
+                root.add(path.get(i));
+            }
+            return root;
+        }
+
+        private void transformGraph(List<E> rootPath) {
+            List<E> prePath;
+            //remove edges
+            for (int i = 0; i < pathResults.size(); i++) {
+                prePath = pathResults.get(i);
+                if (prePath.size() == 1) {
+                    mutableGraph.removeEdge(prePath.get(0));
+                } else if (comparePath(rootPath, prePath)) {
+                    for (int j = 0; j <= rootPath.size(); j++) {
+                        mutableGraph.removeEdge(prePath.get(j));
+                    }
+                }
+            }
+            for (int i = 0; i < pathCandidates.size(); i++) {
+                prePath = pathCandidates.get(i);
+                if (prePath.size() == 1) {
+                    mutableGraph.removeEdge(prePath.get(0));
+                } else if (comparePath(rootPath, prePath)) {
+                    for (int j = 0; j <= rootPath.size(); j++) {
+                        mutableGraph.removeEdge(prePath.get(j));
+                    }
+                }
+            }
+
+            if (rootPath.size() == 0) {
+                return;
+            }
+
+            //remove nodes
+            List<V> nodes = new ArrayList<V>();
+            nodes.add(source);
+            V pre = source;
+            V srcA;
+            V dstB;
+            for (int i = 0; i < rootPath.size() - 1; i++) {
+                E temp = rootPath.get(i);
+                srcA = temp.src();
+                dstB = temp.dst();
+
+                if (srcA.equals(pre)) {
+                    nodes.add(dstB);
+                    pre = dstB;
+                } else {
+                    nodes.add(srcA);
+                    pre = srcA;
+                }
+            }
+            for (int i = 0; i < nodes.size(); i++) {
+                mutableGraph.removeVertex(nodes.get(i));
+            }
+        }
+
+        private boolean comparePath(List<E> path1, List<E> path2) {
+            if (path1.size() > path2.size()) {
+                return false;
+            }
+            if (path1.size() == 0) {
+                return true;
+            }
+            for (int i = 0; i < path1.size(); i++) {
+                if (path1.get(i) != path2.get(i)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        private void addPathResult() {
+            List<E> sp;
+            sp = pathCandidates.get(0);
+            for (int i = 1; i < pathCandidates.size(); i++) {
+                if (sp.size() > pathCandidates.get(i).size()) {
+                    sp = pathCandidates.get(i);
+                }
+            }
+            pathResults.add(sp);
+            // Log.info(sp.toString());
+            pathCandidates.remove(sp);
+        }
+
+}
diff --git a/utils/misc/src/main/java/org/onlab/graph/MutableAdjacencyListsGraph.java b/utils/misc/src/main/java/org/onlab/graph/MutableAdjacencyListsGraph.java
new file mode 100644
index 0000000..044a60d
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/graph/MutableAdjacencyListsGraph.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.onlab.graph;
+
+import static com.google.common.base.MoreObjects.toStringHelper;
+
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.SetMultimap;
+
+public class MutableAdjacencyListsGraph<V extends Vertex, E extends Edge<V>>
+implements MutableGraph<V, E> {
+    private Set<V> vertexes = new HashSet<V>();
+    private Set<E> edges = new HashSet<E>();
+
+    private SetMultimap<V, E> sources = HashMultimap.create();
+    private SetMultimap<V, E> destinations = HashMultimap.create();
+
+    /**
+     * Creates a graph comprising of the specified vertexes and edges.
+     *
+     * @param vertex   set of graph vertexes
+     * @param edge     set of graph edges
+     */
+    public MutableAdjacencyListsGraph(Set<V> vertex, Set<E> edge) {
+        vertexes.addAll(vertex);
+        edges.addAll(edge);
+        for (E e : edge) {
+            sources.put(e.src(), e);
+            vertexes.add(e.src());
+            destinations.put(e.dst(), e);
+            vertexes.add(e.dst());
+        }
+    }
+
+    @Override
+    public Set<V> getVertexes() {
+        return vertexes;
+    }
+
+    @Override
+    public Set<E> getEdges() {
+        return edges;
+    }
+
+    @Override
+    public Set<E> getEdgesFrom(V src) {
+        return sources.get(src);
+    }
+
+    @Override
+    public Set<E> getEdgesTo(V dst) {
+        return destinations.get(dst);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof MutableAdjacencyListsGraph) {
+            MutableAdjacencyListsGraph that = (MutableAdjacencyListsGraph) obj;
+            return this.getClass() == that.getClass() &&
+                    Objects.equals(this.vertexes, that.vertexes) &&
+                    Objects.equals(this.edges, that.edges);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(vertexes, edges);
+    }
+
+    @Override
+    public String toString() {
+        return toStringHelper(this)
+                .add("vertexes", vertexes)
+                .add("edges", edges)
+                .toString();
+    }
+
+
+    @Override
+    public void addVertex(V vertex) {
+        if (vertexes != null) {
+              if (!vertexes.contains(vertex)) {
+                    vertexes.add(vertex);
+                }
+        }
+    }
+
+    @Override
+    public void removeVertex(V vertex) {
+        // TODO Auto-generated method stub
+        if (vertexes != null && edges != null) {
+            if (vertexes.contains(vertex)) {
+                vertexes.remove(vertex);
+                Set<E> srcEdgesList = sources.get(vertex);
+                Set<E> dstEdgesList = destinations.get(vertex);
+                edges.removeAll(srcEdgesList);
+                edges.removeAll(dstEdgesList);
+                sources.remove(vertex, srcEdgesList);
+                sources.remove(vertex, dstEdgesList);
+            }
+        }
+    }
+
+    @Override
+    public void addEdge(E edge) {
+        if (edges != null) {
+                if (!edges.contains(edge)) {
+                    edges.add(edge);
+                    sources.put(edge.src(), edge);
+                    destinations.put(edge.dst(), edge);
+                }
+        }
+    }
+
+    @Override
+    public void removeEdge(E edge) {
+        if (edges != null) {
+            if (edges.contains(edge)) {
+                edges.remove(edge);
+                sources.remove(edge.src(), edge);
+                destinations.remove(edge.dst(), edge);
+            }
+        }
+    }
+
+    @Override
+    public Graph<V, E> toImmutable() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    /**
+     * Clear the graph.
+     */
+    public void clear() {
+        edges.clear();
+        vertexes.clear();
+        sources.clear();
+        destinations.clear();
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/ARP.java b/utils/misc/src/main/java/org/onlab/packet/ARP.java
index 91713c1..86fb289 100644
--- a/utils/misc/src/main/java/org/onlab/packet/ARP.java
+++ b/utils/misc/src/main/java/org/onlab/packet/ARP.java
@@ -55,6 +55,7 @@
     /**
      * @param hwType
      *            the hardwareType to set
+     * @return this
      */
     public ARP setHardwareType(final short hwType) {
         this.hardwareType = hwType;
@@ -71,6 +72,7 @@
     /**
      * @param protoType
      *            the protocolType to set
+     * @return this
      */
     public ARP setProtocolType(final short protoType) {
         this.protocolType = protoType;
@@ -87,6 +89,7 @@
     /**
      * @param hwAddressLength
      *            the hardwareAddressLength to set
+     * @return this
      */
     public ARP setHardwareAddressLength(final byte hwAddressLength) {
         this.hardwareAddressLength = hwAddressLength;
@@ -103,6 +106,7 @@
     /**
      * @param protoAddressLength
      *            the protocolAddressLength to set
+     * @return this
      */
     public ARP setProtocolAddressLength(final byte protoAddressLength) {
         this.protocolAddressLength = protoAddressLength;
@@ -119,6 +123,7 @@
     /**
      * @param op
      *            the opCode to set
+     * @return this
      */
     public ARP setOpCode(final short op) {
         this.opCode = op;
@@ -135,6 +140,7 @@
     /**
      * @param senderHWAddress
      *            the senderHardwareAddress to set
+     * @return this
      */
     public ARP setSenderHardwareAddress(final byte[] senderHWAddress) {
         this.senderHardwareAddress = senderHWAddress;
@@ -151,6 +157,7 @@
     /**
      * @param senderProtoAddress
      *            the senderProtocolAddress to set
+     * @return this
      */
     public ARP setSenderProtocolAddress(final byte[] senderProtoAddress) {
         this.senderProtocolAddress = senderProtoAddress;
@@ -173,6 +180,7 @@
     /**
      * @param targetHWAddress
      *            the targetHardwareAddress to set
+     * @return this
      */
     public ARP setTargetHardwareAddress(final byte[] targetHWAddress) {
         this.targetHardwareAddress = targetHWAddress;
@@ -206,6 +214,7 @@
     /**
      * @param targetProtoAddress
      *            the targetProtocolAddress to set
+     * @return this
      */
     public ARP setTargetProtocolAddress(final byte[] targetProtoAddress) {
         this.targetProtocolAddress = targetProtoAddress;
diff --git a/utils/misc/src/main/java/org/onlab/packet/DHCP.java b/utils/misc/src/main/java/org/onlab/packet/DHCP.java
index a250010..8dba13c 100644
--- a/utils/misc/src/main/java/org/onlab/packet/DHCP.java
+++ b/utils/misc/src/main/java/org/onlab/packet/DHCP.java
@@ -95,6 +95,7 @@
     /**
      * @param opCode
      *            the opCode to set
+     * @return this
      */
     public DHCP setOpCode(final byte opCode) {
         this.opCode = opCode;
@@ -111,6 +112,7 @@
     /**
      * @param hardwareType
      *            the hardwareType to set
+     * @return this
      */
     public DHCP setHardwareType(final byte hardwareType) {
         this.hardwareType = hardwareType;
@@ -127,6 +129,7 @@
     /**
      * @param hardwareAddressLength
      *            the hardwareAddressLength to set
+     * @return this
      */
     public DHCP setHardwareAddressLength(final byte hardwareAddressLength) {
         this.hardwareAddressLength = hardwareAddressLength;
@@ -143,6 +146,7 @@
     /**
      * @param hops
      *            the hops to set
+     * @return this
      */
     public DHCP setHops(final byte hops) {
         this.hops = hops;
@@ -159,6 +163,7 @@
     /**
      * @param transactionId
      *            the transactionId to set
+     * @return this
      */
     public DHCP setTransactionId(final int transactionId) {
         this.transactionId = transactionId;
@@ -175,6 +180,7 @@
     /**
      * @param seconds
      *            the seconds to set
+     * @return this
      */
     public DHCP setSeconds(final short seconds) {
         this.seconds = seconds;
@@ -191,6 +197,7 @@
     /**
      * @param flags
      *            the flags to set
+     * @return this
      */
     public DHCP setFlags(final short flags) {
         this.flags = flags;
@@ -207,6 +214,7 @@
     /**
      * @param clientIPAddress
      *            the clientIPAddress to set
+     * @return this
      */
     public DHCP setClientIPAddress(final int clientIPAddress) {
         this.clientIPAddress = clientIPAddress;
@@ -223,6 +231,7 @@
     /**
      * @param yourIPAddress
      *            the yourIPAddress to set
+     * @return this
      */
     public DHCP setYourIPAddress(final int yourIPAddress) {
         this.yourIPAddress = yourIPAddress;
@@ -239,6 +248,7 @@
     /**
      * @param serverIPAddress
      *            the serverIPAddress to set
+     * @return this
      */
     public DHCP setServerIPAddress(final int serverIPAddress) {
         this.serverIPAddress = serverIPAddress;
@@ -255,6 +265,7 @@
     /**
      * @param gatewayIPAddress
      *            the gatewayIPAddress to set
+     * @return this
      */
     public DHCP setGatewayIPAddress(final int gatewayIPAddress) {
         this.gatewayIPAddress = gatewayIPAddress;
@@ -271,6 +282,7 @@
     /**
      * @param clientHardwareAddress
      *            the clientHardwareAddress to set
+     * @return this
      */
     public DHCP setClientHardwareAddress(final byte[] clientHardwareAddress) {
         this.clientHardwareAddress = clientHardwareAddress;
@@ -303,6 +315,7 @@
     /**
      * @param options
      *            the options to set
+     * @return this
      */
     public DHCP setOptions(final List<DHCPOption> options) {
         this.options = options;
@@ -334,6 +347,7 @@
     /**
      * @param server
      *            the serverName to set
+     * @return this
      */
     public DHCP setServerName(final String server) {
         this.serverName = server;
@@ -350,6 +364,7 @@
     /**
      * @param bootFile
      *            the bootFileName to set
+     * @return this
      */
     public DHCP setBootFileName(final String bootFile) {
         this.bootFileName = bootFile;
diff --git a/utils/misc/src/main/java/org/onlab/packet/DHCPOption.java b/utils/misc/src/main/java/org/onlab/packet/DHCPOption.java
index c1493d0..1b6c670 100644
--- a/utils/misc/src/main/java/org/onlab/packet/DHCPOption.java
+++ b/utils/misc/src/main/java/org/onlab/packet/DHCPOption.java
@@ -38,6 +38,7 @@
     /**
      * @param code
      *            the code to set
+     * @return this
      */
     public DHCPOption setCode(final byte code) {
         this.code = code;
@@ -54,6 +55,7 @@
     /**
      * @param length
      *            the length to set
+     * @return this
      */
     public DHCPOption setLength(final byte length) {
         this.length = length;
@@ -70,6 +72,7 @@
     /**
      * @param data
      *            the data to set
+     * @return this
      */
     public DHCPOption setData(final byte[] data) {
         this.data = data;
diff --git a/utils/misc/src/main/java/org/onlab/packet/Data.java b/utils/misc/src/main/java/org/onlab/packet/Data.java
index 311cc93..f3a1092 100644
--- a/utils/misc/src/main/java/org/onlab/packet/Data.java
+++ b/utils/misc/src/main/java/org/onlab/packet/Data.java
@@ -33,7 +33,7 @@
     }
 
     /**
-     * @param data
+     * @param data the data
      */
     public Data(final byte[] data) {
         this.data = data;
@@ -49,6 +49,7 @@
     /**
      * @param data
      *            the data to set
+     * @return self
      */
     public Data setData(final byte[] data) {
         this.data = data;
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ethernet.java b/utils/misc/src/main/java/org/onlab/packet/Ethernet.java
index 96ec021..3519338 100644
--- a/utils/misc/src/main/java/org/onlab/packet/Ethernet.java
+++ b/utils/misc/src/main/java/org/onlab/packet/Ethernet.java
@@ -255,6 +255,7 @@
      *
      * @param pd
      *            the pad to set
+     * @return this
      */
     public Ethernet setPad(final boolean pd) {
         this.pad = pd;
@@ -345,7 +346,7 @@
     /**
      * Checks to see if a string is a valid MAC address.
      *
-     * @param macAddress
+     * @param macAddress string to test if it is a valid MAC
      * @return True if macAddress is a valid MAC, False otherwise
      */
     public static boolean isMACAddress(final String macAddress) {
@@ -368,7 +369,7 @@
      * matter, and returns a corresponding byte[].
      *
      * @param macAddress
-     *            The MAC address to convert into a bye array
+     *            The MAC address to convert into a byte array
      * @return The macAddress as a byte array
      */
     public static byte[] toMACAddress(final String macAddress) {
@@ -379,7 +380,7 @@
      * Accepts a MAC address and returns the corresponding long, where the MAC
      * bytes are set on the lower order bytes of the long.
      *
-     * @param macAddress
+     * @param macAddress MAC address as a byte array
      * @return a long containing the mac address bytes
      */
     public static long toLong(final byte[] macAddress) {
@@ -389,7 +390,7 @@
     /**
      * Converts a long MAC address to a byte array.
      *
-     * @param macAddress
+     * @param macAddress MAC address set on the lower order bytes of the long
      * @return the bytes of the mac address
      */
     public static byte[] toByteArray(final long macAddress) {
diff --git a/utils/misc/src/main/java/org/onlab/packet/ICMP.java b/utils/misc/src/main/java/org/onlab/packet/ICMP.java
index d208727d..6e6dd5f 100644
--- a/utils/misc/src/main/java/org/onlab/packet/ICMP.java
+++ b/utils/misc/src/main/java/org/onlab/packet/ICMP.java
@@ -39,6 +39,7 @@
     /**
      * @param icmpType
      *            to set
+     * @return this
      */
     public ICMP setIcmpType(final byte icmpType) {
         this.icmpType = icmpType;
@@ -55,6 +56,7 @@
     /**
      * @param icmpCode
      *            code to set
+     * @return this
      */
     public ICMP setIcmpCode(final byte icmpCode) {
         this.icmpCode = icmpCode;
@@ -71,6 +73,7 @@
     /**
      * @param checksum
      *            the checksum to set
+     * @return this
      */
     public ICMP setChecksum(final short checksum) {
         this.checksum = checksum;
diff --git a/utils/misc/src/main/java/org/onlab/packet/IPacket.java b/utils/misc/src/main/java/org/onlab/packet/IPacket.java
index f1f01e4..6e2f2a0 100644
--- a/utils/misc/src/main/java/org/onlab/packet/IPacket.java
+++ b/utils/misc/src/main/java/org/onlab/packet/IPacket.java
@@ -64,7 +64,7 @@
     /**
      * Deserializes this packet layer and all possible payloads.
      *
-     * @param data
+     * @param data bytes to deserialize
      * @param offset
      *            offset to start deserializing from
      * @param length
diff --git a/utils/misc/src/main/java/org/onlab/packet/IPv4.java b/utils/misc/src/main/java/org/onlab/packet/IPv4.java
index 84ddb30..e54e9e7 100644
--- a/utils/misc/src/main/java/org/onlab/packet/IPv4.java
+++ b/utils/misc/src/main/java/org/onlab/packet/IPv4.java
@@ -76,6 +76,7 @@
     /**
      * @param version
      *            the version to set
+     * @return this
      */
     public IPv4 setVersion(final byte version) {
         this.version = version;
@@ -99,6 +100,7 @@
     /**
      * @param diffServ
      *            the diffServ to set
+     * @return this
      */
     public IPv4 setDiffServ(final byte diffServ) {
         this.diffServ = diffServ;
@@ -130,6 +132,7 @@
     /**
      * @param identification
      *            the identification to set
+     * @return this
      */
     public IPv4 setIdentification(final short identification) {
         this.identification = identification;
@@ -146,7 +149,8 @@
     /**
      * @param flags
      *            the flags to set
-     */
+     * @return this
+s     */
     public IPv4 setFlags(final byte flags) {
         this.flags = flags;
         return this;
@@ -162,6 +166,7 @@
     /**
      * @param fragmentOffset
      *            the fragmentOffset to set
+     * @return this
      */
     public IPv4 setFragmentOffset(final short fragmentOffset) {
         this.fragmentOffset = fragmentOffset;
@@ -178,6 +183,7 @@
     /**
      * @param ttl
      *            the ttl to set
+     * @return this
      */
     public IPv4 setTtl(final byte ttl) {
         this.ttl = ttl;
@@ -194,6 +200,7 @@
     /**
      * @param protocol
      *            the protocol to set
+     * @return this
      */
     public IPv4 setProtocol(final byte protocol) {
         this.protocol = protocol;
@@ -210,6 +217,7 @@
     /**
      * @param checksum
      *            the checksum to set
+     * @return this
      */
     public IPv4 setChecksum(final short checksum) {
         this.checksum = checksum;
@@ -232,6 +240,7 @@
     /**
      * @param sourceAddress
      *            the sourceAddress to set
+     * @return this
      */
     public IPv4 setSourceAddress(final int sourceAddress) {
         this.sourceAddress = sourceAddress;
@@ -241,6 +250,7 @@
     /**
      * @param sourceAddress
      *            the sourceAddress to set
+     * @return this
      */
     public IPv4 setSourceAddress(final String sourceAddress) {
         this.sourceAddress = IPv4.toIPv4Address(sourceAddress);
@@ -257,6 +267,7 @@
     /**
      * @param destinationAddress
      *            the destinationAddress to set
+     * @return this
      */
     public IPv4 setDestinationAddress(final int destinationAddress) {
         this.destinationAddress = destinationAddress;
@@ -266,6 +277,7 @@
     /**
      * @param destinationAddress
      *            the destinationAddress to set
+     * @return this
      */
     public IPv4 setDestinationAddress(final String destinationAddress) {
         this.destinationAddress = IPv4.toIPv4Address(destinationAddress);
@@ -282,6 +294,7 @@
     /**
      * @param options
      *            the options to set
+     * @return this
      */
     public IPv4 setOptions(final byte[] options) {
         if (options != null && options.length % 4 > 0) {
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip4Address.java b/utils/misc/src/main/java/org/onlab/packet/Ip4Address.java
index 7b7b989..114f126 100644
--- a/utils/misc/src/main/java/org/onlab/packet/Ip4Address.java
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip4Address.java
@@ -15,203 +15,160 @@
  */
 package org.onlab.packet;
 
+import java.net.InetAddress;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
 import java.nio.ByteBuffer;
-import static com.google.common.base.Preconditions.checkNotNull;
+import java.util.Arrays;
+
+import com.google.common.net.InetAddresses;
 
 /**
- * The class representing an IPv4 address.
+ * A class representing an IPv4 address.
  * This class is immutable.
  */
-public final class Ip4Address implements Comparable<Ip4Address> {
-    private final int value;
-
-    /** The length of the address in bytes (octets). */
-    public static final int BYTE_LENGTH = 4;
-
-    /** The length of the address in bits. */
-    public static final int BIT_LENGTH = BYTE_LENGTH * Byte.SIZE;
+public final class Ip4Address extends IpAddress {
+    public static final IpAddress.Version VERSION = IpAddress.Version.INET;
+    public static final int BYTE_LENGTH = IpAddress.INET_BYTE_LENGTH;
+    public static final int BIT_LENGTH = IpAddress.INET_BIT_LENGTH;
 
     /**
-     * Default constructor.
+     * Constructor for given IP address version and address octets.
+     *
+     * @param value the IP address value stored in network byte order
+     * (i.e., the most significant byte first)
+     * @throws IllegalArgumentException if the arguments are invalid
      */
-    public Ip4Address() {
-        this.value = 0;
+    private Ip4Address(byte[] value) {
+        super(VERSION, value);
     }
 
     /**
-     * Copy constructor.
+     * Returns the integer value of this IPv4 address.
      *
-     * @param other the object to copy from
+     * @return the IPv4 address's value as an integer
      */
-    public Ip4Address(Ip4Address other) {
-        this.value = other.value;
+    public int toInt() {
+        ByteBuffer bb = ByteBuffer.wrap(super.toOctets());
+        return bb.getInt();
     }
 
     /**
-     * Constructor from an integer value.
+     * Converts an integer into an IPv4 address.
      *
-     * @param value the value to use
+     * @param value an integer representing an IPv4 address value
+     * @return an IPv4 address
      */
-    public Ip4Address(int value) {
-        this.value = value;
+    public static Ip4Address valueOf(int value) {
+        byte[] bytes =
+            ByteBuffer.allocate(INET_BYTE_LENGTH).putInt(value).array();
+        return new Ip4Address(bytes);
     }
 
     /**
-     * Constructor from a byte array with the IPv4 address stored in network
-     * byte order (i.e., the most significant byte first).
+     * Converts a byte array into an IPv4 address.
      *
-     * @param value the value to use
+     * @param value the IPv4 address value stored in network byte order
+     * (i.e., the most significant byte first)
+     * @return an IPv4 address
+     * @throws IllegalArgumentException if the argument is invalid
      */
-    public Ip4Address(byte[] value) {
-        this(value, 0);
+    public static Ip4Address valueOf(byte[] value) {
+        return new Ip4Address(value);
     }
 
     /**
-     * Constructor from a byte array with the IPv4 address stored in network
-     * byte order (i.e., the most significant byte first), and a given offset
-     * from the beginning of the byte array.
-     *
+     * Converts a byte array and a given offset from the beginning of the
+     * array into an IPv4 address.
+     * <p>
+     * The IP address is stored in network byte order (i.e., the most
+     * significant byte first).
+     * </p>
      * @param value the value to use
      * @param offset the offset in bytes from the beginning of the byte array
+     * @return an IPv4 address
+     * @throws IllegalArgumentException if the arguments are invalid
      */
-    public Ip4Address(byte[] value, int offset) {
-        checkNotNull(value);
-
-        // Verify the arguments
-        if ((offset < 0) || (offset + BYTE_LENGTH > value.length)) {
-            String msg;
-            if (value.length < BYTE_LENGTH) {
-                msg = "Invalid IPv4 address array: array length: " +
-                    value.length + ". Must be at least " + BYTE_LENGTH;
-            } else {
-                msg = "Invalid IPv4 address array: array offset: " +
-                    offset + ". Must be in the interval [0, " +
-                    (value.length - BYTE_LENGTH) + "]";
-            }
-            throw new IllegalArgumentException(msg);
-        }
-
-        // Read the address
-        ByteBuffer bb = ByteBuffer.wrap(value);
-        this.value = bb.getInt(offset);
+    public static Ip4Address valueOf(byte[] value, int offset) {
+        IpAddress.checkArguments(VERSION, value, offset);
+        byte[] bc = Arrays.copyOfRange(value, offset, value.length);
+        return Ip4Address.valueOf(bc);
     }
 
     /**
-     * Constructs an IPv4 address from a string representation of the address.
-     *<p>
-     * Example: "1.2.3.4"
+     * Converts an InetAddress into an IPv4 address.
      *
-     * @param value the value to use
+     * @param inetAddress the InetAddress value to use. It must contain an IPv4
+     * address
+     * @return an IPv4 address
+     * @throws IllegalArgumentException if the argument is invalid
      */
-    public Ip4Address(String value) {
-        checkNotNull(value);
-
-        String[] splits = value.split("\\.");
-        if (splits.length != 4) {
-            final String msg = "Invalid IPv4 address string: " + value;
+    public static Ip4Address valueOf(InetAddress inetAddress) {
+        byte[] bytes = inetAddress.getAddress();
+        if (inetAddress instanceof Inet4Address) {
+            return new Ip4Address(bytes);
+        }
+        if ((inetAddress instanceof Inet6Address) ||
+            (bytes.length == INET6_BYTE_LENGTH)) {
+            final String msg = "Invalid IPv4 version address string: " +
+                inetAddress.toString();
             throw new IllegalArgumentException(msg);
         }
-
-        int result = 0;
-        for (int i = 0; i < BYTE_LENGTH; i++) {
-            result |= Integer.parseInt(splits[i]) <<
-                ((BYTE_LENGTH - (i + 1)) * Byte.SIZE);
+        // Use the number of bytes as a hint
+        if (bytes.length == INET_BYTE_LENGTH) {
+            return new Ip4Address(bytes);
         }
-        this.value = result;
+        final String msg = "Unrecognized IP version address string: " +
+            inetAddress.toString();
+        throw new IllegalArgumentException(msg);
     }
 
     /**
-     * Gets the IPv4 address as a byte array.
+     * Converts an IPv4 string literal (e.g., "10.2.3.4") into an IP address.
      *
-     * @return a byte array with the IPv4 address stored in network byte order
-     * (i.e., the most significant byte first).
+     * @param value an IPv4 address value in string form
+     * @return an IPv4 address
+     * @throws IllegalArgumentException if the argument is invalid
      */
-    public byte[] toOctets() {
-        return ByteBuffer.allocate(BYTE_LENGTH).putInt(value).array();
+    public static Ip4Address valueOf(String value) {
+        InetAddress inetAddress = null;
+        try {
+            inetAddress = InetAddresses.forString(value);
+        } catch (IllegalArgumentException e) {
+            final String msg = "Invalid IP address string: " + value;
+            throw new IllegalArgumentException(msg);
+        }
+        return valueOf(inetAddress);
     }
 
     /**
      * Creates an IPv4 network mask prefix.
      *
-     * @param prefixLen the length of the mask prefix. Must be in the interval
-     * [0, 32].
+     * @param prefixLength the length of the mask prefix. Must be in the
+     * interval [0, 32]
      * @return a new IPv4 address that contains a mask prefix of the
      * specified length
+     * @throws IllegalArgumentException if the argument is invalid
      */
-    public static Ip4Address makeMaskPrefix(int prefixLen) {
-        // Verify the prefix length
-        if ((prefixLen < 0) || (prefixLen > Ip4Address.BIT_LENGTH)) {
-            final String msg = "Invalid IPv4 prefix length: " + prefixLen +
-                ". Must be in the interval [0, 32].";
-            throw new IllegalArgumentException(msg);
-        }
-
-        long v =
-            (0xffffffffL << (Ip4Address.BIT_LENGTH - prefixLen)) & 0xffffffffL;
-        return new Ip4Address((int) v);
+    public static Ip4Address makeMaskPrefix(int prefixLength) {
+        byte[] mask = IpAddress.makeMaskPrefixArray(VERSION, prefixLength);
+        return new Ip4Address(mask);
     }
 
     /**
      * Creates an IPv4 address by masking it with a network mask of given
      * mask length.
      *
-     * @param addr the address to mask
-     * @param prefixLen the length of the mask prefix. Must be in the interval
-     * [0, 32].
+     * @param address the address to mask
+     * @param prefixLength the length of the mask prefix. Must be in the
+     * interval [0, 32]
      * @return a new IPv4 address that is masked with a mask prefix of the
      * specified length
+     * @throws IllegalArgumentException if the prefix length is invalid
      */
-    public static Ip4Address makeMaskedAddress(final Ip4Address addr,
-                                               int prefixLen) {
-        Ip4Address mask = Ip4Address.makeMaskPrefix(prefixLen);
-        long v = addr.value & mask.value;
-
-        return new Ip4Address((int) v);
-    }
-
-    /**
-     * Gets the value of the IPv4 address.
-     *
-     * @return the value of the IPv4 address
-     */
-    public int getValue() {
-        return value;
-    }
-
-    /**
-     * Converts the IPv4 value to a '.' separated string.
-     *
-     * @return the IPv4 value as a '.' separated string
-     */
-    @Override
-    public String toString() {
-        return ((this.value >> 24) & 0xff) + "." +
-                ((this.value >> 16) & 0xff) + "." +
-                ((this.value >> 8) & 0xff) + "." +
-                (this.value & 0xff);
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (!(o instanceof Ip4Address)) {
-            return false;
-        }
-        Ip4Address other = (Ip4Address) o;
-        if (this.value != other.value) {
-            return false;
-        }
-        return true;
-    }
-
-    @Override
-    public int hashCode() {
-        return this.value;
-    }
-
-    @Override
-    public int compareTo(Ip4Address o) {
-        Long lv = ((long) this.value) & 0xffffffffL;
-        Long rv = ((long) o.value) & 0xffffffffL;
-        return lv.compareTo(rv);
+    public static Ip4Address makeMaskedAddress(final Ip4Address address,
+                                               int prefixLength) {
+        byte[] net = makeMaskedAddressArray(address, prefixLength);
+        return Ip4Address.valueOf(net);
     }
 }
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java b/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java
index e3b5246..ca9fc61 100644
--- a/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java
@@ -15,110 +15,90 @@
  */
 package org.onlab.packet;
 
-import java.util.Objects;
-
 /**
  * The class representing an IPv4 network address.
  * This class is immutable.
  */
-public final class Ip4Prefix {
-    private final Ip4Address address;           // The IPv4 address
-    private final short prefixLen;              // The prefix length
+public final class Ip4Prefix extends IpPrefix {
+    public static final IpAddress.Version VERSION = IpAddress.Version.INET;
+    // Maximum network mask length
+    public static final int MAX_MASK_LENGTH = IpPrefix.MAX_INET_MASK_LENGTH;
 
     /**
-     * Default constructor.
+     * Constructor for given IPv4 address, and a prefix length.
+     *
+     * @param address the IPv4 address
+     * @param prefixLength the prefix length
+     * @throws IllegalArgumentException if the prefix length value is invalid
      */
-    public Ip4Prefix() {
-        this.address = new Ip4Address();
-        this.prefixLen = 0;
+    private Ip4Prefix(Ip4Address address, int prefixLength) {
+        super(address, prefixLength);
     }
 
     /**
-     * Copy constructor.
+     * Returns the IPv4 address value of the prefix.
      *
-     * @param other the object to copy from
+     * @return the IPv4 address value of the prefix
      */
-    public Ip4Prefix(Ip4Prefix other) {
-        this.address = new Ip4Address(other.address);
-        this.prefixLen = other.prefixLen;
+    public Ip4Address address() {
+        IpAddress a = super.address();
+        return (Ip4Address) a;
     }
 
     /**
-     * Constructor for a given address and prefix length.
+     * Converts an integer and a prefix length into an IPv4 prefix.
      *
-     * @param address   the address to use
-     * @param prefixLen the prefix length to use
+     * @param address an integer representing the IPv4 address
+     * @param prefixLength the prefix length
+     * @return an IPv4 prefix
+     * @throws IllegalArgumentException if the prefix length value is invalid
      */
-    public Ip4Prefix(Ip4Address address, short prefixLen) {
-        this.address = Ip4Address.makeMaskedAddress(address, prefixLen);
-        this.prefixLen = prefixLen;
+    public static Ip4Prefix valueOf(int address, int prefixLength) {
+        return new Ip4Prefix(Ip4Address.valueOf(address), prefixLength);
     }
 
     /**
-     * Constructs an IPv4 prefix from a string representation of the
-     * prefix.
-     *<p>
-     * Example: "1.2.0.0/16"
+     * Converts a byte array and a prefix length into an IPv4 prefix.
      *
-     * @param value the value to use
+     * @param address the IPv4 address value stored in network byte order
+     * @param prefixLength the prefix length
+     * @return an IPv4 prefix
+     * @throws IllegalArgumentException if the prefix length value is invalid
      */
-    public Ip4Prefix(String value) {
-        String[] splits = value.split("/");
-        if (splits.length != 2) {
-            throw new IllegalArgumentException("Specified IPv4 prefix must contain an IPv4 " +
-                    "address and a prefix length separated by '/'");
+    public static Ip4Prefix valueOf(byte[] address, int prefixLength) {
+        return new Ip4Prefix(Ip4Address.valueOf(address), prefixLength);
+    }
+
+    /**
+     * Converts an IPv4 address and a prefix length into an IPv4 prefix.
+     *
+     * @param address the IPv4 address
+     * @param prefixLength the prefix length
+     * @return an IPv4 prefix
+     * @throws IllegalArgumentException if the prefix length value is invalid
+     */
+    public static Ip4Prefix valueOf(Ip4Address address, int prefixLength) {
+        return new Ip4Prefix(address, prefixLength);
+    }
+
+    /**
+     * Converts a CIDR (slash) notation string (e.g., "10.1.0.0/16")
+     * into an IPv4 prefix.
+     *
+     * @param address an IP prefix in string form (e.g., "10.1.0.0/16")
+     * @return an IPv4 prefix
+     * @throws IllegalArgumentException if the arguments are invalid
+     */
+    public static Ip4Prefix valueOf(String address) {
+        final String[] parts = address.split("/");
+        if (parts.length != 2) {
+            String msg = "Malformed IPv4 prefix string: " + address + "." +
+                "Address must take form \"x.x.x.x/y\"";
+            throw new IllegalArgumentException(msg);
         }
-        this.prefixLen = Short.decode(splits[1]);
-        this.address = Ip4Address.makeMaskedAddress(new Ip4Address(splits[0]),
-                this.prefixLen);
-    }
+        Ip4Address ipAddress = Ip4Address.valueOf(parts[0]);
+        int prefixLength = Integer.parseInt(parts[1]);
 
-    /**
-     * Gets the address value of the IPv4 prefix.
-     *
-     * @return the address value of the IPv4 prefix
-     */
-    public Ip4Address getAddress() {
-        return address;
-    }
-
-    /**
-     * Gets the prefix length value of the IPv4 prefix.
-     *
-     * @return the prefix length value of the IPv4 prefix
-     */
-    public short getPrefixLen() {
-        return prefixLen;
-    }
-
-    /**
-     * Converts the IPv4 prefix value to an "address/prefixLen" string.
-     *
-     * @return the IPv4 prefix value as an "address/prefixLen" string
-     */
-    @Override
-    public String toString() {
-        return this.address.toString() + "/" + this.prefixLen;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-        if (other == this) {
-            return true;
-        }
-
-        if (!(other instanceof Ip4Prefix)) {
-            return false;
-        }
-
-        Ip4Prefix otherIp4Prefix = (Ip4Prefix) other;
-
-        return Objects.equals(this.address, otherIp4Prefix.address)
-                && this.prefixLen == otherIp4Prefix.prefixLen;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(address, prefixLen);
+        return new Ip4Prefix(ipAddress, prefixLength);
     }
 }
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip6Address.java b/utils/misc/src/main/java/org/onlab/packet/Ip6Address.java
index 8ad9112..d353422 100644
--- a/utils/misc/src/main/java/org/onlab/packet/Ip6Address.java
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip6Address.java
@@ -16,260 +16,137 @@
 package org.onlab.packet;
 
 import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.nio.ByteBuffer;
-import java.util.Objects;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.util.Arrays;
 
 import com.google.common.net.InetAddresses;
-import com.google.common.primitives.UnsignedLongs;
-
-import static com.google.common.base.Preconditions.checkNotNull;
-import static com.google.common.base.Preconditions.checkState;
 
 /**
- * The class representing an IPv6 address.
+ * A class representing an IPv6 address.
  * This class is immutable.
  */
-public final class Ip6Address implements Comparable<Ip6Address> {
-    private final long valueHigh;    // The higher (more significant) 64 bits
-    private final long valueLow;     // The lower (less significant) 64 bits
-
-    /** The length of the address in bytes (octets). */
-    public static final int BYTE_LENGTH = 16;
-
-    /** The length of the address in bits. */
-    public static final int BIT_LENGTH = BYTE_LENGTH * Byte.SIZE;
+public final class Ip6Address extends IpAddress {
+    public static final IpAddress.Version VERSION = IpAddress.Version.INET6;
+    public static final int BYTE_LENGTH = IpAddress.INET6_BYTE_LENGTH;
+    public static final int BIT_LENGTH = IpAddress.INET6_BIT_LENGTH;
 
     /**
-     * Default constructor.
+     * Constructor for given IP address version and address octets.
+     *
+     * @param value the IP address value stored in network byte order
+     * (i.e., the most significant byte first)
+     * @throws IllegalArgumentException if the arguments are invalid
      */
-    public Ip6Address() {
-        this.valueHigh = 0;
-        this.valueLow = 0;
+    private Ip6Address(byte[] value) {
+        super(VERSION, value);
     }
 
     /**
-     * Copy constructor.
+     * Converts a byte array into an IPv6 address.
      *
-     * @param other the object to copy from
+     * @param value the IPv6 address value stored in network byte order
+     * (i.e., the most significant byte first)
+     * @return an IPv6 address
+     * @throws IllegalArgumentException if the argument is invalid
      */
-    public Ip6Address(Ip6Address other) {
-        this.valueHigh = other.valueHigh;
-        this.valueLow = other.valueLow;
+    public static Ip6Address valueOf(byte[] value) {
+        return new Ip6Address(value);
     }
 
     /**
-     * Constructor from integer values.
-     *
-     * @param valueHigh the higher (more significant) 64 bits of the address
-     * @param valueLow  the lower (less significant) 64 bits of the address
-     */
-    public Ip6Address(long valueHigh, long valueLow) {
-        this.valueHigh = valueHigh;
-        this.valueLow = valueLow;
-    }
-
-    /**
-     * Constructor from a byte array with the IPv6 address stored in network
-     * byte order (i.e., the most significant byte first).
-     *
-     * @param value the value to use
-     */
-    public Ip6Address(byte[] value) {
-        this(value, 0);
-    }
-
-    /**
-     * Constructor from a byte array with the IPv6 address stored in network
-     * byte order (i.e., the most significant byte first), and a given offset
-     * from the beginning of the byte array.
-     *
+     * Converts a byte array and a given offset from the beginning of the
+     * array into an IPv6 address.
+     * <p>
+     * The IP address is stored in network byte order (i.e., the most
+     * significant byte first).
+     * </p>
      * @param value the value to use
      * @param offset the offset in bytes from the beginning of the byte array
+     * @return an IPv6 address
+     * @throws IllegalArgumentException if the arguments are invalid
      */
-    public Ip6Address(byte[] value, int offset) {
-        checkNotNull(value);
-
-        // Verify the arguments
-        if ((offset < 0) || (offset + BYTE_LENGTH > value.length)) {
-            String msg;
-            if (value.length < BYTE_LENGTH) {
-                msg = "Invalid IPv6 address array: array length: " +
-                    value.length + ". Must be at least " + BYTE_LENGTH;
-            } else {
-                msg = "Invalid IPv6 address array: array offset: " +
-                    offset + ". Must be in the interval [0, " +
-                    (value.length - BYTE_LENGTH) + "]";
-            }
-            throw new IllegalArgumentException(msg);
-        }
-
-        // Read the address
-        ByteBuffer bb = ByteBuffer.wrap(value);
-        bb.position(offset);
-        this.valueHigh = bb.getLong();
-        this.valueLow = bb.getLong();
+    public static Ip6Address valueOf(byte[] value, int offset) {
+        IpAddress.checkArguments(VERSION, value, offset);
+        byte[] bc = Arrays.copyOfRange(value, offset, value.length);
+        return Ip6Address.valueOf(bc);
     }
 
     /**
-     * Constructs an IPv6 address from a string representation of the address.
-     *<p>
-     * Example: "1111:2222::8888"
+     * Converts an InetAddress into an IPv6 address.
      *
-     * @param value the value to use
+     * @param inetAddress the InetAddress value to use. It must contain an IPv6
+     * address
+     * @return an IPv6 address
+     * @throws IllegalArgumentException if the argument is invalid
      */
-    public Ip6Address(String value) {
-        checkNotNull(value);
-
-        if (value.isEmpty()) {
-            final String msg = "Specified IPv6 cannot be an empty string";
+    public static Ip6Address valueOf(InetAddress inetAddress) {
+        byte[] bytes = inetAddress.getAddress();
+        if (inetAddress instanceof Inet6Address) {
+            return new Ip6Address(bytes);
+        }
+        if ((inetAddress instanceof Inet4Address) ||
+            (bytes.length == INET_BYTE_LENGTH)) {
+            final String msg = "Invalid IPv6 version address string: " +
+                inetAddress.toString();
             throw new IllegalArgumentException(msg);
         }
-        InetAddress addr = null;
+        // Use the number of bytes as a hint
+        if (bytes.length == INET6_BYTE_LENGTH) {
+            return new Ip6Address(bytes);
+        }
+        final String msg = "Unrecognized IP version address string: " +
+            inetAddress.toString();
+        throw new IllegalArgumentException(msg);
+    }
+
+    /**
+     * Converts an IPv6 string literal (e.g., "1111:2222::8888") into an IP
+     * address.
+     *
+     * @param value an IPv6 address value in string form
+     * @return an IPv6 address
+     * @throws IllegalArgumentException if the argument is invalid
+     */
+    public static Ip6Address valueOf(String value) {
+        InetAddress inetAddress = null;
         try {
-            addr = InetAddresses.forString(value);
+            inetAddress = InetAddresses.forString(value);
         } catch (IllegalArgumentException e) {
-            final String msg = "Invalid IPv6 address string: " + value;
+            final String msg = "Invalid IP address string: " + value;
             throw new IllegalArgumentException(msg);
         }
-        byte[] bytes = addr.getAddress();
-        ByteBuffer bb = ByteBuffer.wrap(bytes);
-        this.valueHigh = bb.getLong();
-        this.valueLow = bb.getLong();
-    }
-
-    /**
-     * Gets the IPv6 address as a byte array.
-     *
-     * @return a byte array with the IPv6 address stored in network byte order
-     * (i.e., the most significant byte first).
-     */
-    public byte[] toOctets() {
-        return ByteBuffer.allocate(BYTE_LENGTH)
-            .putLong(valueHigh).putLong(valueLow).array();
+        return valueOf(inetAddress);
     }
 
     /**
      * Creates an IPv6 network mask prefix.
      *
-     * @param prefixLen the length of the mask prefix. Must be in the interval
-     * [0, 128].
+     * @param prefixLength the length of the mask prefix. Must be in the
+     * interval [0, 128]
      * @return a new IPv6 address that contains a mask prefix of the
      * specified length
+     * @throws IllegalArgumentException if the arguments are invalid
      */
-    public static Ip6Address makeMaskPrefix(int prefixLen) {
-        long vh, vl;
-
-        // Verify the prefix length
-        if ((prefixLen < 0) || (prefixLen > Ip6Address.BIT_LENGTH)) {
-            final String msg = "Invalid IPv6 prefix length: " + prefixLen +
-                ". Must be in the interval [0, 128].";
-            throw new IllegalArgumentException(msg);
-        }
-
-        if (prefixLen == 0) {
-            //
-            // NOTE: Apparently, the result of "<< 64" shifting to the left
-            // results in all 1s instead of all 0s, hence we handle it as
-            // a special case.
-            //
-            vh = 0;
-            vl = 0;
-        } else if (prefixLen <= 64) {
-            vh = (0xffffffffffffffffL << (64 - prefixLen)) & 0xffffffffffffffffL;
-            vl = 0;
-        } else {
-            vh = -1L;           // All 1s
-            vl = (0xffffffffffffffffL << (128 - prefixLen)) & 0xffffffffffffffffL;
-        }
-        return new Ip6Address(vh, vl);
+    public static Ip6Address makeMaskPrefix(int prefixLength) {
+        byte[] mask = IpAddress.makeMaskPrefixArray(VERSION, prefixLength);
+        return new Ip6Address(mask);
     }
 
     /**
      * Creates an IPv6 address by masking it with a network mask of given
      * mask length.
      *
-     * @param addr the address to mask
-     * @param prefixLen the length of the mask prefix. Must be in the interval
-     * [0, 128].
+     * @param address the address to mask
+     * @param prefixLength the length of the mask prefix. Must be in the
+     * interval [0, 128]
      * @return a new IPv6 address that is masked with a mask prefix of the
      * specified length
+     * @throws IllegalArgumentException if the prefix length is invalid
      */
-    public static Ip6Address makeMaskedAddress(final Ip6Address addr,
-                                               int prefixLen) {
-        Ip6Address mask = Ip6Address.makeMaskPrefix(prefixLen);
-        long vh = addr.valueHigh & mask.valueHigh;
-        long vl = addr.valueLow & mask.valueLow;
-
-        return new Ip6Address(vh, vl);
-    }
-
-    /**
-     * Gets the value of the higher (more significant) 64 bits of the address.
-     *
-     * @return the value of the higher (more significant) 64 bits of the
-     * address
-     */
-    public long getValueHigh() {
-        return valueHigh;
-    }
-
-    /**
-     * Gets the value of the lower (less significant) 64 bits of the address.
-     *
-     * @return the value of the lower (less significant) 64 bits of the
-     * address
-     */
-    public long getValueLow() {
-        return valueLow;
-    }
-
-    /**
-     * Converts the IPv6 value to a ':' separated string.
-     *
-     * @return the IPv6 value as a ':' separated string
-     */
-    @Override
-    public String toString() {
-        ByteBuffer bb = ByteBuffer.allocate(Ip6Address.BYTE_LENGTH);
-        bb.putLong(valueHigh);
-        bb.putLong(valueLow);
-        InetAddress inetAddr = null;
-        try {
-            inetAddr = InetAddress.getByAddress(bb.array());
-        } catch (UnknownHostException e) {
-            // Should never happen
-            checkState(false, "Internal error: Ip6Address.toString()");
-            return "::";
-        }
-        return InetAddresses.toAddrString(inetAddr);
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (!(o instanceof Ip6Address)) {
-            return false;
-        }
-        Ip6Address other = (Ip6Address) o;
-        return this.valueHigh == other.valueHigh
-                && this.valueLow == other.valueLow;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(valueHigh, valueLow);
-    }
-
-    @Override
-    public int compareTo(Ip6Address o) {
-        // Compare the high-order 64-bit value
-        if (this.valueHigh != o.valueHigh) {
-            return UnsignedLongs.compare(this.valueHigh, o.valueHigh);
-        }
-        // Compare the low-order 64-bit value
-        if (this.valueLow != o.valueLow) {
-            return UnsignedLongs.compare(this.valueLow, o.valueLow);
-        }
-        return 0;
+    public static Ip6Address makeMaskedAddress(final Ip6Address address,
+                                               int prefixLength) {
+        byte[] net = makeMaskedAddressArray(address, prefixLength);
+        return Ip6Address.valueOf(net);
     }
 }
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java b/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java
index 5422ae1..ca9985e 100644
--- a/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java
@@ -15,110 +15,79 @@
  */
 package org.onlab.packet;
 
-import java.util.Objects;
-
 /**
  * The class representing an IPv6 network address.
  * This class is immutable.
  */
-public final class Ip6Prefix {
-    private final Ip6Address address;           // The IPv6 address
-    private final short prefixLen;              // The prefix length
+public final class Ip6Prefix extends IpPrefix {
+    public static final IpAddress.Version VERSION = IpAddress.Version.INET6;
+    // Maximum network mask length
+    public static final int MAX_MASK_LENGTH = IpPrefix.MAX_INET6_MASK_LENGTH;
 
     /**
-     * Default constructor.
+     * Constructor for given IPv6 address, and a prefix length.
+     *
+     * @param address the IPv6 address
+     * @param prefixLength the prefix length
+     * @throws IllegalArgumentException if the prefix length value is invalid
      */
-    public Ip6Prefix() {
-        this.address = new Ip6Address();
-        this.prefixLen = 0;
+    private Ip6Prefix(Ip6Address address, int prefixLength) {
+        super(address, prefixLength);
     }
 
     /**
-     * Copy constructor.
+     * Returns the IPv6 address value of the prefix.
      *
-     * @param other the object to copy from
+     * @return the IPv6 address value of the prefix
      */
-    public Ip6Prefix(Ip6Prefix other) {
-        this.address = new Ip6Address(other.address);
-        this.prefixLen = other.prefixLen;
+    public Ip6Address address() {
+        IpAddress a = super.address();
+        return (Ip6Address) a;
     }
 
     /**
-     * Constructor for a given address and prefix length.
+     * Converts a byte array and a prefix length into an IPv6 prefix.
      *
-     * @param address   the address to use
-     * @param prefixLen the prefix length to use
+     * @param address the IPv6 address value stored in network byte order
+     * @param prefixLength the prefix length
+     * @return an IPv6 prefix
+     * @throws IllegalArgumentException if the prefix length value is invalid
      */
-    public Ip6Prefix(Ip6Address address, short prefixLen) {
-        this.address = Ip6Address.makeMaskedAddress(address, prefixLen);
-        this.prefixLen = prefixLen;
+    public static Ip6Prefix valueOf(byte[] address, int prefixLength) {
+        return new Ip6Prefix(Ip6Address.valueOf(address), prefixLength);
     }
 
     /**
-     * Constructs an IPv6 prefix from a string representation of the
-     * prefix.
-     *<p>
-     * Example: "1111:2222::/32"
+     * Converts an IPv6 address and a prefix length into an IPv6 prefix.
      *
-     * @param value the value to use
+     * @param address the IPv6 address
+     * @param prefixLength the prefix length
+     * @return an IPv6 prefix
+     * @throws IllegalArgumentException if the prefix length value is invalid
      */
-    public Ip6Prefix(String value) {
-        String[] splits = value.split("/");
-        if (splits.length != 2) {
-            throw new IllegalArgumentException("Specified IPv6 prefix must contain an IPv6 " +
-                    "address and a prefix length separated by '/'");
+    public static Ip6Prefix valueOf(Ip6Address address, int prefixLength) {
+        return new Ip6Prefix(address, prefixLength);
+    }
+
+    /**
+     * Converts a CIDR (slash) notation string (e.g., "1111:2222::/64")
+     * into an IPv6 prefix.
+     *
+     * @param address an IP prefix in string form (e.g.,"1111:2222::/64")
+     * @return an IPv6 prefix
+     * @throws IllegalArgumentException if the arguments are invalid
+     */
+    public static Ip6Prefix valueOf(String address) {
+        final String[] parts = address.split("/");
+        if (parts.length != 2) {
+            String msg = "Malformed IPv6 prefix string: " + address + "." +
+                "Address must take form " +
+                "\"xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/y\"";
+            throw new IllegalArgumentException(msg);
         }
-        this.prefixLen = Short.decode(splits[1]);
-        this.address = Ip6Address.makeMaskedAddress(new Ip6Address(splits[0]),
-                this.prefixLen);
-    }
+        Ip6Address ipAddress = Ip6Address.valueOf(parts[0]);
+        int prefixLength = Integer.parseInt(parts[1]);
 
-    /**
-     * Gets the address value of the IPv6 prefix.
-     *
-     * @return the address value of the IPv6 prefix
-     */
-    public Ip6Address getAddress() {
-        return address;
-    }
-
-    /**
-     * Gets the prefix length value of the IPv6 prefix.
-     *
-     * @return the prefix length value of the IPv6 prefix
-     */
-    public short getPrefixLen() {
-        return prefixLen;
-    }
-
-    /**
-     * Converts the IPv6 prefix value to an "address/prefixLen" string.
-     *
-     * @return the IPv6 prefix value as an "address/prefixLen" string
-     */
-    @Override
-    public String toString() {
-        return this.address.toString() + "/" + this.prefixLen;
-    }
-
-    @Override
-    public boolean equals(Object other) {
-        if (other == this) {
-            return true;
-        }
-
-        if (!(other instanceof Ip6Prefix)) {
-            return false;
-        }
-
-        Ip6Prefix otherIp6Prefix = (Ip6Prefix) other;
-
-        return Objects.equals(this.address, otherIp6Prefix.address)
-                && this.prefixLen == otherIp6Prefix.prefixLen;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(address, prefixLen);
+        return new Ip6Prefix(ipAddress, prefixLength);
     }
 }
diff --git a/utils/misc/src/main/java/org/onlab/packet/IpAddress.java b/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
index 77ba9b0..2906798 100644
--- a/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
+++ b/utils/misc/src/main/java/org/onlab/packet/IpAddress.java
@@ -30,9 +30,9 @@
 
 /**
  * A class representing an IP address.
- * TODO: Add support for IPv6 as well.
+ * This class is immutable.
  */
-public final class IpAddress implements Comparable<IpAddress> {
+public class IpAddress implements Comparable<IpAddress> {
     // IP Versions
     public enum Version { INET, INET6 };
 
@@ -53,7 +53,7 @@
      * (i.e., the most significant byte first)
      * @throws IllegalArgumentException if the arguments are invalid
      */
-    private IpAddress(Version version, byte[] value) {
+    protected IpAddress(Version version, byte[] value) {
         checkArguments(version, value, 0);
         this.version = version;
         switch (version) {
@@ -80,6 +80,42 @@
     }
 
     /**
+     * Gets the {@link Ip4Address} view of the IP address.
+     *
+     * @return the {@link Ip4Address} view of the IP address if it is IPv4,
+     * otherwise null
+     */
+    public Ip4Address getIp4Address() {
+        if (version() != Ip4Address.VERSION) {
+            return null;
+        }
+
+        // Return this object itself if it is already instance of Ip4Address
+        if (this instanceof Ip4Address) {
+            return (Ip4Address) this;
+        }
+        return Ip4Address.valueOf(octets);
+    }
+
+    /**
+     * Gets the {@link Ip6Address} view of the IP address.
+     *
+     * @return the {@link Ip6Address} view of the IP address if it is IPv6,
+     * otherwise null
+     */
+    public Ip6Address getIp6Address() {
+        if (version() != Ip6Address.VERSION) {
+            return null;
+        }
+
+        // Return this object itself if it is already instance of Ip6Address
+        if (this instanceof Ip6Address) {
+            return (Ip6Address) this;
+        }
+        return Ip6Address.valueOf(octets);
+    }
+
+    /**
      * Returns the IP address as a byte array.
      *
      * @return a byte array
@@ -89,7 +125,7 @@
     }
 
     /**
-     * Returns the integral value of this IP address.
+     * Returns the integer value of this IP address.
      * TODO: This method should be moved to Ip4Address.
      *
      * @return the IP address's value as an integer
@@ -220,31 +256,7 @@
      * @throws IllegalArgumentException if the arguments are invalid
      */
     public static IpAddress makeMaskPrefix(Version version, int prefixLength) {
-        int addrByteLength = byteLength(version);
-        int addrBitLength = addrByteLength * Byte.SIZE;
-
-        // Verify the prefix length
-        if ((prefixLength < 0) || (prefixLength > addrBitLength)) {
-            final String msg = "Invalid IP prefix length: " + prefixLength +
-                ". Must be in the interval [0, " + addrBitLength + "].";
-            throw new IllegalArgumentException(msg);
-        }
-
-        // Number of bytes and extra bits that should be all 1s
-        int maskBytes = prefixLength / Byte.SIZE;
-        int maskBits = prefixLength % Byte.SIZE;
-        byte[] mask = new byte[addrByteLength];
-
-        // Set the bytes and extra bits to 1s
-        for (int i = 0; i < maskBytes; i++) {
-            mask[i] = (byte) 0xff;              // Set mask bytes to 1s
-        }
-        for (int i = maskBytes; i < addrByteLength; i++) {
-            mask[i] = 0;                        // Set remaining bytes to 0s
-        }
-        if (maskBits > 0) {
-            mask[maskBytes] = (byte) (0xff << (Byte.SIZE - maskBits));
-        }
+        byte[] mask = makeMaskPrefixArray(version, prefixLength);
         return new IpAddress(version, mask);
     }
 
@@ -252,24 +264,26 @@
      * Creates an IP address by masking it with a network mask of given
      * mask length.
      *
-     * @param addr the address to mask
+     * @param address the address to mask
      * @param prefixLength the length of the mask prefix. Must be in the
      * interval [0, 32] for IPv4, or [0, 128] for IPv6
      * @return a new IP address that is masked with a mask prefix of the
      * specified length
      * @throws IllegalArgumentException if the prefix length is invalid
      */
-    public static IpAddress makeMaskedAddress(final IpAddress addr,
+    public static IpAddress makeMaskedAddress(final IpAddress address,
                                               int prefixLength) {
-        IpAddress mask = IpAddress.makeMaskPrefix(addr.version(),
-                                                  prefixLength);
-        byte[] net = new byte[mask.octets.length];
-
-        // Mask each byte
-        for (int i = 0; i < net.length; i++) {
-            net[i] = (byte) (addr.octets[i] & mask.octets[i]);
+        // TODO: The code below should go away and replaced with generics
+        if (address instanceof Ip4Address) {
+            Ip4Address ip4a = (Ip4Address) address;
+            return Ip4Address.makeMaskedAddress(ip4a, prefixLength);
+        } else if (address instanceof Ip6Address) {
+            Ip6Address ip6a = (Ip6Address) address;
+            return Ip6Address.makeMaskedAddress(ip6a, prefixLength);
+        } else {
+            byte[] net = makeMaskedAddressArray(address, prefixLength);
+            return IpAddress.valueOf(address.version(), net);
         }
-        return IpAddress.valueOf(addr.version(), net);
     }
 
     @Override
@@ -353,8 +367,7 @@
      * array with the address
      * @throws IllegalArgumentException if any of the arguments is invalid
      */
-    private static void checkArguments(Version version, byte[] value,
-                                       int offset) {
+    static void checkArguments(Version version, byte[] value, int offset) {
         // Check the offset and byte array length
         int addrByteLength = byteLength(version);
         if ((offset < 0) || (offset + addrByteLength > value.length)) {
@@ -372,4 +385,67 @@
             throw new IllegalArgumentException(msg);
         }
     }
+
+    /**
+     * Creates a byte array for IP network mask prefix.
+     *
+     * @param version the IP address version
+     * @param prefixLength the length of the mask prefix. Must be in the
+     * interval [0, 32] for IPv4, or [0, 128] for IPv6
+     * @return a byte array that contains a mask prefix of the
+     * specified length
+     * @throws IllegalArgumentException if the arguments are invalid
+     */
+    static byte[] makeMaskPrefixArray(Version version, int prefixLength) {
+        int addrByteLength = byteLength(version);
+        int addrBitLength = addrByteLength * Byte.SIZE;
+
+        // Verify the prefix length
+        if ((prefixLength < 0) || (prefixLength > addrBitLength)) {
+            final String msg = "Invalid IP prefix length: " + prefixLength +
+                ". Must be in the interval [0, " + addrBitLength + "].";
+            throw new IllegalArgumentException(msg);
+        }
+
+        // Number of bytes and extra bits that should be all 1s
+        int maskBytes = prefixLength / Byte.SIZE;
+        int maskBits = prefixLength % Byte.SIZE;
+        byte[] mask = new byte[addrByteLength];
+
+        // Set the bytes and extra bits to 1s
+        for (int i = 0; i < maskBytes; i++) {
+            mask[i] = (byte) 0xff;              // Set mask bytes to 1s
+        }
+        for (int i = maskBytes; i < addrByteLength; i++) {
+            mask[i] = 0;                        // Set remaining bytes to 0s
+        }
+        if (maskBits > 0) {
+            mask[maskBytes] = (byte) (0xff << (Byte.SIZE - maskBits));
+        }
+        return mask;
+    }
+
+    /**
+     * Creates a byte array that represents an IP address masked with
+     * a network mask of given mask length.
+     *
+     * @param addr the address to mask
+     * @param prefixLength the length of the mask prefix. Must be in the
+     * interval [0, 32] for IPv4, or [0, 128] for IPv6
+     * @return a byte array that represents the IP address masked with
+     * a mask prefix of the specified length
+     * @throws IllegalArgumentException if the prefix length is invalid
+     */
+    static byte[] makeMaskedAddressArray(final IpAddress addr,
+                                         int prefixLength) {
+        byte[] mask = IpAddress.makeMaskPrefixArray(addr.version(),
+                                                    prefixLength);
+        byte[] net = new byte[mask.length];
+
+        // Mask each byte
+        for (int i = 0; i < net.length; i++) {
+            net[i] = (byte) (addr.octets[i] & mask[i]);
+        }
+        return net;
+    }
 }
diff --git a/utils/misc/src/main/java/org/onlab/packet/IpPrefix.java b/utils/misc/src/main/java/org/onlab/packet/IpPrefix.java
index 3cde79d..c2fc175 100644
--- a/utils/misc/src/main/java/org/onlab/packet/IpPrefix.java
+++ b/utils/misc/src/main/java/org/onlab/packet/IpPrefix.java
@@ -17,17 +17,16 @@
 
 import java.util.Objects;
 
-// TODO: Add support for IPv6 as well.
-
 /**
  * A class representing an IP prefix. A prefix consists of an IP address and
  * a subnet mask.
+ * This class is immutable.
  * <p>
  * NOTE: The stored IP address in the result IP prefix is masked to
  * contain zeroes in all bits after the prefix length.
  * </p>
  */
-public final class IpPrefix {
+public class IpPrefix {
     // Maximum network mask length
     public static final int MAX_INET_MASK_LENGTH = IpAddress.INET_BIT_LENGTH;
     public static final int MAX_INET6_MASK_LENGTH = IpAddress.INET6_BIT_LENGTH;
@@ -40,82 +39,12 @@
      *
      * @param address the IP address
      * @param prefixLength the prefix length
-     */
-    private IpPrefix(IpAddress address, int prefixLength) {
-        checkPrefixLength(prefixLength);
-        this.address = IpAddress.makeMaskedAddress(address, prefixLength);
-        this.prefixLength = (short) prefixLength;
-    }
-
-    /**
-     * Checks whether the prefix length is valid.
-     *
-     * @param prefixLength the prefix length value to check
      * @throws IllegalArgumentException if the prefix length value is invalid
      */
-    private static void checkPrefixLength(int prefixLength) {
-        if ((prefixLength < 0) || (prefixLength > MAX_INET_MASK_LENGTH)) {
-            String msg = "Invalid prefix length " + prefixLength + ". " +
-                "The value must be in the interval [0, " +
-                MAX_INET_MASK_LENGTH + "]";
-            throw new IllegalArgumentException(msg);
-        }
-    }
-
-    /**
-     * Converts an integer and a prefix length into an IPv4 prefix.
-     *
-     * @param address an integer representing the IPv4 address
-     * @param prefixLength the prefix length
-     * @return an IP prefix
-     */
-    public static IpPrefix valueOf(int address, int prefixLength) {
-        return new IpPrefix(IpAddress.valueOf(address), prefixLength);
-    }
-
-    /**
-     * Converts a byte array and a prefix length into an IP prefix.
-     *
-     * @param version the IP address version
-     * @param address the IP address value stored in network byte order
-     * @param prefixLength the prefix length
-     * @return an IP prefix
-     */
-    public static IpPrefix valueOf(IpAddress.Version version, byte[] address,
-                                   int prefixLength) {
-        return new IpPrefix(IpAddress.valueOf(version, address),
-                            prefixLength);
-    }
-
-    /**
-     * Converts an IP address and a prefix length into IP prefix.
-     *
-     * @param address the IP address
-     * @param prefixLength the prefix length
-     * @return an IP prefix
-     */
-    public static IpPrefix valueOf(IpAddress address, int prefixLength) {
-        return new IpPrefix(address, prefixLength);
-    }
-
-    /**
-     * Converts a CIDR (slash) notation string (e.g., "10.1.0.0/16") into an
-     * IP prefix.
-     *
-     * @param address an IP prefix in string form, e.g. "10.1.0.0/16"
-     * @return an IP prefix
-     */
-    public static IpPrefix valueOf(String address) {
-        final String[] parts = address.split("/");
-        if (parts.length != 2) {
-            String msg = "Malformed IP prefix string: " + address + "." +
-                "Address must take form \"x.x.x.x/y\"";
-            throw new IllegalArgumentException(msg);
-        }
-        IpAddress ipAddress = IpAddress.valueOf(parts[0]);
-        int prefixLength = Integer.parseInt(parts[1]);
-
-        return new IpPrefix(ipAddress, prefixLength);
+    protected IpPrefix(IpAddress address, int prefixLength) {
+        checkPrefixLength(address.version(), prefixLength);
+        this.address = IpAddress.makeMaskedAddress(address, prefixLength);
+        this.prefixLength = (short) prefixLength;
     }
 
     /**
@@ -146,6 +75,103 @@
     }
 
     /**
+     * Gets the {@link Ip4Prefix} view of the IP prefix.
+     *
+     * @return the {@link Ip4Prefix} view of the IP prefix if it is IPv4,
+     * otherwise null
+     */
+    public Ip4Prefix getIp4Prefix() {
+        if (version() != Ip4Prefix.VERSION) {
+            return null;
+        }
+
+        // Return this object itself if it is already instance of Ip4Prefix
+        if (this instanceof Ip4Prefix) {
+            return (Ip4Prefix) this;
+        }
+        return Ip4Prefix.valueOf(address.getIp4Address(), prefixLength);
+    }
+
+    /**
+     * Gets the {@link Ip6Prefix} view of the IP prefix.
+     *
+     * @return the {@link Ip6Prefix} view of the IP prefix if it is IPv6,
+     * otherwise null
+     */
+    public Ip6Prefix getIp6Prefix() {
+        if (version() != Ip6Prefix.VERSION) {
+            return null;
+        }
+
+        // Return this object itself if it is already instance of Ip6Prefix
+        if (this instanceof Ip6Prefix) {
+            return (Ip6Prefix) this;
+        }
+        return Ip6Prefix.valueOf(address.getIp6Address(), prefixLength);
+    }
+
+    /**
+     * Converts an integer and a prefix length into an IPv4 prefix.
+     *
+     * @param address an integer representing the IPv4 address
+     * @param prefixLength the prefix length
+     * @return an IP prefix
+     * @throws IllegalArgumentException if the prefix length value is invalid
+     */
+    public static IpPrefix valueOf(int address, int prefixLength) {
+        return new IpPrefix(IpAddress.valueOf(address), prefixLength);
+    }
+
+    /**
+     * Converts a byte array and a prefix length into an IP prefix.
+     *
+     * @param version the IP address version
+     * @param address the IP address value stored in network byte order
+     * @param prefixLength the prefix length
+     * @return an IP prefix
+     * @throws IllegalArgumentException if the prefix length value is invalid
+     */
+    public static IpPrefix valueOf(IpAddress.Version version, byte[] address,
+                                   int prefixLength) {
+        return new IpPrefix(IpAddress.valueOf(version, address), prefixLength);
+    }
+
+    /**
+     * Converts an IP address and a prefix length into an IP prefix.
+     *
+     * @param address the IP address
+     * @param prefixLength the prefix length
+     * @return an IP prefix
+     * @throws IllegalArgumentException if the prefix length value is invalid
+     */
+    public static IpPrefix valueOf(IpAddress address, int prefixLength) {
+        return new IpPrefix(address, prefixLength);
+    }
+
+    /**
+     * Converts a CIDR (slash) notation string (e.g., "10.1.0.0/16" or
+     * "1111:2222::/64") into an IP prefix.
+     *
+     * @param address an IP prefix in string form (e.g. "10.1.0.0/16" or
+     * "1111:2222::/64")
+     * @return an IP prefix
+     * @throws IllegalArgumentException if the arguments are invalid
+     */
+    public static IpPrefix valueOf(String address) {
+        final String[] parts = address.split("/");
+        if (parts.length != 2) {
+            String msg = "Malformed IP prefix string: " + address + "." +
+                "Address must take form \"x.x.x.x/y\" or " +
+                "\"xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx/y\"";
+            throw new IllegalArgumentException(msg);
+        }
+        IpAddress ipAddress = IpAddress.valueOf(parts[0]);
+        int prefixLength = Integer.parseInt(parts[1]);
+
+        return new IpPrefix(ipAddress, prefixLength);
+    }
+
+    /**
      * Determines whether a given IP prefix is contained within this prefix.
      *
      * @param other the IP prefix to test
@@ -217,4 +243,35 @@
         builder.append(String.format("%d", prefixLength));
         return builder.toString();
     }
+
+    /**
+     * Checks whether the prefix length is valid.
+     *
+     * @param version the IP address version
+     * @param prefixLength the prefix length value to check
+     * @throws IllegalArgumentException if the prefix length value is invalid
+     */
+    private static void checkPrefixLength(IpAddress.Version version,
+                                          int prefixLength) {
+        int maxPrefixLen = 0;
+
+        switch (version) {
+        case INET:
+            maxPrefixLen = MAX_INET_MASK_LENGTH;
+            break;
+        case INET6:
+            maxPrefixLen = MAX_INET6_MASK_LENGTH;
+            break;
+        default:
+            String msg = "Invalid IP version " + version;
+            throw new IllegalArgumentException(msg);
+        }
+
+        if ((prefixLength < 0) || (prefixLength > maxPrefixLen)) {
+            String msg = "Invalid prefix length " + prefixLength + ". " +
+                "The value must be in the interval [0, " +
+                maxPrefixLen + "]";
+            throw new IllegalArgumentException(msg);
+        }
+    }
 }
diff --git a/utils/misc/src/main/java/org/onlab/packet/LLDP.java b/utils/misc/src/main/java/org/onlab/packet/LLDP.java
index 3928068..339bc3a 100644
--- a/utils/misc/src/main/java/org/onlab/packet/LLDP.java
+++ b/utils/misc/src/main/java/org/onlab/packet/LLDP.java
@@ -48,6 +48,7 @@
     /**
      * @param chassis
      *            the chassisId to set
+     * @return this
      */
     public LLDP setChassisId(final LLDPTLV chassis) {
         this.chassisId = chassis;
@@ -64,6 +65,7 @@
     /**
      * @param portId
      *            the portId to set
+     * @return this
      */
     public LLDP setPortId(final LLDPTLV portId) {
         this.portId = portId;
@@ -80,6 +82,7 @@
     /**
      * @param ttl
      *            the ttl to set
+     * @return this
      */
     public LLDP setTtl(final LLDPTLV ttl) {
         this.ttl = ttl;
@@ -96,6 +99,7 @@
     /**
      * @param optionalTLVList
      *            the optionalTLVList to set
+     * @return this
      */
     public LLDP setOptionalTLVList(final List<LLDPTLV> optionalTLVList) {
         this.optionalTLVList = optionalTLVList;
diff --git a/utils/misc/src/main/java/org/onlab/packet/LLDPTLV.java b/utils/misc/src/main/java/org/onlab/packet/LLDPTLV.java
index 21bd55f..b5fe833 100644
--- a/utils/misc/src/main/java/org/onlab/packet/LLDPTLV.java
+++ b/utils/misc/src/main/java/org/onlab/packet/LLDPTLV.java
@@ -40,6 +40,7 @@
     /**
      * @param type
      *            the type to set
+     * @return this
      */
     public LLDPTLV setType(final byte type) {
         this.type = type;
@@ -56,6 +57,7 @@
     /**
      * @param length
      *            the length to set
+     * @return this
      */
     public LLDPTLV setLength(final short length) {
         this.length = length;
@@ -72,6 +74,7 @@
     /**
      * @param value
      *            the value to set
+     * @return this
      */
     public LLDPTLV setValue(final byte[] value) {
         this.value = value;
diff --git a/utils/misc/src/main/java/org/onlab/packet/TCP.java b/utils/misc/src/main/java/org/onlab/packet/TCP.java
index 9185cd5..5e0cfb2 100644
--- a/utils/misc/src/main/java/org/onlab/packet/TCP.java
+++ b/utils/misc/src/main/java/org/onlab/packet/TCP.java
@@ -46,6 +46,7 @@
     /**
      * @param sourcePort
      *            the sourcePort to set
+     * @return this
      */
     public TCP setSourcePort(final short sourcePort) {
         this.sourcePort = sourcePort;
@@ -62,6 +63,7 @@
     /**
      * @param destinationPort
      *            the destinationPort to set
+     * @return this
      */
     public TCP setDestinationPort(final short destinationPort) {
         this.destinationPort = destinationPort;
@@ -157,6 +159,7 @@
     /**
      * @param checksum
      *            the checksum to set
+     * @return this
      */
     public TCP setChecksum(final short checksum) {
         this.checksum = checksum;
diff --git a/utils/misc/src/main/java/org/onlab/packet/UDP.java b/utils/misc/src/main/java/org/onlab/packet/UDP.java
index 5d9c645..73a196a 100644
--- a/utils/misc/src/main/java/org/onlab/packet/UDP.java
+++ b/utils/misc/src/main/java/org/onlab/packet/UDP.java
@@ -57,6 +57,7 @@
     /**
      * @param sourcePort
      *            the sourcePort to set
+     * @return this
      */
     public UDP setSourcePort(final short sourcePort) {
         this.sourcePort = sourcePort;
@@ -73,6 +74,7 @@
     /**
      * @param destinationPort
      *            the destinationPort to set
+     * @return this
      */
     public UDP setDestinationPort(final short destinationPort) {
         this.destinationPort = destinationPort;
@@ -96,6 +98,7 @@
     /**
      * @param checksum
      *            the checksum to set
+     * @return this
      */
     public UDP setChecksum(final short checksum) {
         this.checksum = checksum;
diff --git a/utils/misc/src/main/java/org/onlab/util/HexString.java b/utils/misc/src/main/java/org/onlab/util/HexString.java
index 4e46a25..3e9ae03 100644
--- a/utils/misc/src/main/java/org/onlab/util/HexString.java
+++ b/utils/misc/src/main/java/org/onlab/util/HexString.java
@@ -24,7 +24,7 @@
     /**
      * Convert a string of bytes to a ':' separated hex string.
      *
-     * @param bytes
+     * @param bytes string of bytes to convert
      * @return "0f:ca:fe:de:ad:be:ef"
      */
     public static String toHexString(final byte[] bytes) {
diff --git a/utils/misc/src/test/java/org/onlab/graph/KshortestPathSearchTest.java b/utils/misc/src/test/java/org/onlab/graph/KshortestPathSearchTest.java
new file mode 100644
index 0000000..3ef2fcf
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/graph/KshortestPathSearchTest.java
@@ -0,0 +1,200 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.onlab.graph;
+
+import static com.google.common.collect.ImmutableSet.of;
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayOutputStream;
+//import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class KshortestPathSearchTest extends BreadthFirstSearchTest {
+
+    private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
+
+    @Test
+    public void noPath() {
+        graph = new AdjacencyListsGraph<>(of(A, B, C, D),
+                                          of(new TestEdge(A, B, 1),
+                                             new TestEdge(B, A, 1),
+                                             new TestEdge(C, D, 1),
+                                             new TestEdge(D, C, 1)));
+        KshortestPathSearch<TestVertex, TestEdge> gs = new KshortestPathSearch<TestVertex, TestEdge>(graph);
+        List<List<TestEdge>> result = gs.search(A, D, weight, 1);
+        List<Path> paths = new ArrayList<>();
+        Iterator<List<TestEdge>> itr = result.iterator();
+        while (itr.hasNext()) {
+            System.out.println(itr.next().toString());
+        }
+        assertEquals("incorrect paths count", 0, result.size());
+    }
+
+    @Test
+    public void test2Path() {
+        graph = new AdjacencyListsGraph<>(of(A, B, C, D),
+                                          of(new TestEdge(A, B, 1),
+                                             new TestEdge(B, A, 1),
+                                             new TestEdge(B, D, 1),
+                                             new TestEdge(D, B, 1),
+                                             new TestEdge(A, C, 1),
+                                             new TestEdge(C, A, 1),
+                                             new TestEdge(C, D, 1),
+                                             new TestEdge(D, C, 1)));
+        KshortestPathSearch<TestVertex, TestEdge> gs = new KshortestPathSearch<TestVertex, TestEdge>(graph);
+        List<List<TestEdge>> result = gs.search(A, D, weight, 2);
+        List<Path> paths = new ArrayList<>();
+        Iterator<List<TestEdge>> itr = result.iterator();
+        while (itr.hasNext()) {
+            System.out.println(itr.next().toString());
+        }
+        assertEquals("incorrect paths count", 2, result.size());
+        // assertEquals("printing the paths", outContent.toString());
+    }
+
+    @Test
+    public void test3Path() {
+        graph = new AdjacencyListsGraph<>(of(A, B, C, D),
+                                          of(new TestEdge(A, B, 1),
+                                             new TestEdge(B, A, 1),
+                                             new TestEdge(A, D, 1),
+                                             new TestEdge(D, A, 1),
+                                             new TestEdge(B, D, 1),
+                                             new TestEdge(D, B, 1),
+                                             new TestEdge(A, C, 1),
+                                             new TestEdge(C, A, 1),
+                                             new TestEdge(C, D, 1),
+                                             new TestEdge(D, C, 1)));
+        KshortestPathSearch<TestVertex, TestEdge> gs = new KshortestPathSearch<TestVertex, TestEdge>(graph);
+        List<List<TestEdge>> result = gs.search(A, D, weight, 3);
+        List<Path> paths = new ArrayList<>();
+        Iterator<List<TestEdge>> itr = result.iterator();
+        while (itr.hasNext()) {
+            System.out.println(itr.next().toString());
+        }
+        assertEquals("incorrect paths count", 3, result.size());
+        // assertEquals("printing the paths", outContent.toString());
+    }
+
+    @Test
+    public void test4Path() {
+        graph = new AdjacencyListsGraph<>(of(A, B, C, D, E, F),
+                                          of(new TestEdge(A, B, 1),
+                                             new TestEdge(B, A, 1),
+                                             new TestEdge(A, C, 1),
+                                             new TestEdge(C, A, 1),
+                                             new TestEdge(B, D, 1),
+                                             new TestEdge(D, B, 1),
+                                             new TestEdge(C, E, 1),
+                                             new TestEdge(E, C, 1),
+                                             new TestEdge(D, F, 1),
+                                             new TestEdge(F, D, 1),
+                                             new TestEdge(F, E, 1),
+                                             new TestEdge(E, F, 1),
+                                             new TestEdge(C, D, 1),
+                                             new TestEdge(D, C, 1)));
+        KshortestPathSearch<TestVertex, TestEdge> gs = new KshortestPathSearch<TestVertex, TestEdge>(graph);
+        List<List<TestEdge>> result = gs.search(A, F, weight, 4);
+        List<Path> paths = new ArrayList<>();
+        Iterator<List<TestEdge>> itr = result.iterator();
+        while (itr.hasNext()) {
+            System.out.println(itr.next().toString());
+        }
+        assertEquals("incorrect paths count", 4, result.size());
+        // assertEquals("printing the paths", outContent.toString());
+    }
+
+    @Test
+    public void test6Path() {
+        graph = new AdjacencyListsGraph<>(of(A, B, C, D, E, F),
+                                          of(new TestEdge(A, B, 1),
+                                             new TestEdge(B, A, 1),
+                                             new TestEdge(A, C, 1),
+                                             new TestEdge(C, A, 1),
+                                             new TestEdge(B, D, 1),
+                                             new TestEdge(D, B, 1),
+                                             new TestEdge(B, C, 1),
+                                             new TestEdge(C, B, 1),
+                                             new TestEdge(D, E, 1),
+                                             new TestEdge(E, D, 1),
+                                             new TestEdge(C, E, 1),
+                                             new TestEdge(E, C, 1),
+                                             new TestEdge(D, F, 1),
+                                             new TestEdge(F, D, 1),
+                                             new TestEdge(E, F, 1),
+                                             new TestEdge(F, E, 1)));
+        KshortestPathSearch<TestVertex, TestEdge> gs = new KshortestPathSearch<TestVertex, TestEdge>(graph);
+        List<List<TestEdge>> result = gs.search(A, F, weight, 6);
+        List<Path> paths = new ArrayList<>();
+        Iterator<List<TestEdge>> itr = result.iterator();
+        while (itr.hasNext()) {
+            System.out.println(itr.next().toString());
+        }
+        assertEquals("incorrect paths count", 6, result.size());
+        // assertEquals("printing the paths", outContent.toString());
+    }
+
+    @Test
+    public void dualEdgePath() {
+        graph = new AdjacencyListsGraph<>(of(A, B, C, D, E, F, G, H),
+                                          of(new TestEdge(A, B, 1), new TestEdge(A, C, 3),
+                                             new TestEdge(B, D, 2), new TestEdge(B, C, 1),
+                                             new TestEdge(B, E, 4), new TestEdge(C, E, 1),
+                                             new TestEdge(D, H, 5), new TestEdge(D, E, 1),
+                                             new TestEdge(E, F, 1), new TestEdge(F, D, 1),
+                                             new TestEdge(F, G, 1), new TestEdge(F, H, 1),
+                                             new TestEdge(A, E, 3), new TestEdge(B, D, 1)));
+        KshortestPathSearch<TestVertex, TestEdge> gs = new KshortestPathSearch<TestVertex, TestEdge>(graph);
+        List<List<TestEdge>> result = gs.search(A, G, weight, 6);
+        List<Path> paths = new ArrayList<>();
+        Iterator<List<TestEdge>> itr = result.iterator();
+        while (itr.hasNext()) {
+            System.out.println(itr.next().toString());
+        }
+        assertEquals("incorrect paths count", 6, result.size());
+        // assertEquals("printing the paths", outContent.toString());
+    }
+
+    @BeforeClass
+    public static void setUpBeforeClass() throws Exception {
+    }
+
+    @AfterClass
+    public static void tearDownAfterClass() throws Exception {
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        // System.setOut(new PrintStream(outContent));
+    }
+
+    @After
+    public void tearDown() throws Exception {
+         // System.setOut(null);
+    }
+
+}
diff --git a/utils/misc/src/test/java/org/onlab/packet/Ip4AddressTest.java b/utils/misc/src/test/java/org/onlab/packet/Ip4AddressTest.java
index 36463cf..3bbf000 100644
--- a/utils/misc/src/test/java/org/onlab/packet/Ip4AddressTest.java
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip4AddressTest.java
@@ -15,10 +15,13 @@
  */
 package org.onlab.packet;
 
+import com.google.common.net.InetAddresses;
+import com.google.common.testing.EqualsTester;
 import org.junit.Test;
 
+import java.net.InetAddress;
+
 import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
@@ -36,10 +39,18 @@
     }
 
     /**
+     * Tests the IPv4 address version constant.
+     */
+    @Test
+    public void testAddressVersion() {
+        assertThat(Ip4Address.VERSION, is(IpAddress.Version.INET));
+    }
+
+    /**
      * Tests the length of the address in bytes (octets).
      */
     @Test
-    public void testAddrBytelen() {
+    public void testAddrByteLength() {
         assertThat(Ip4Address.BYTE_LENGTH, is(4));
     }
 
@@ -47,299 +58,375 @@
      * Tests the length of the address in bits.
      */
     @Test
-    public void testAddrBitlen() {
+    public void testAddrBitLength() {
         assertThat(Ip4Address.BIT_LENGTH, is(32));
     }
 
     /**
-     * Tests default class constructor.
+     * Tests returning the IP address version.
      */
     @Test
-    public void testDefaultConstructor() {
-        Ip4Address ip4Address = new Ip4Address();
-        assertThat(ip4Address.toString(), is("0.0.0.0"));
+    public void testVersion() {
+        Ip4Address ipAddress;
+
+        // IPv4
+        ipAddress = Ip4Address.valueOf("0.0.0.0");
+        assertThat(ipAddress.version(), is(IpAddress.Version.INET));
     }
 
     /**
-     * Tests valid class copy constructor.
+     * Tests returning an IPv4 address as a byte array.
      */
     @Test
-    public void testCopyConstructor() {
-        Ip4Address fromAddr = new Ip4Address("1.2.3.4");
-        Ip4Address ip4Address = new Ip4Address(fromAddr);
-        assertThat(ip4Address.toString(), is("1.2.3.4"));
+    public void testAddressToOctetsIPv4() {
+        Ip4Address ipAddress;
+        byte[] value;
 
-        fromAddr = new Ip4Address("0.0.0.0");
-        ip4Address = new Ip4Address(fromAddr);
-        assertThat(ip4Address.toString(), is("0.0.0.0"));
+        value = new byte[] {1, 2, 3, 4};
+        ipAddress = Ip4Address.valueOf("1.2.3.4");
+        assertThat(ipAddress.toOctets(), is(value));
 
-        fromAddr = new Ip4Address("255.255.255.255");
-        ip4Address = new Ip4Address(fromAddr);
-        assertThat(ip4Address.toString(), is("255.255.255.255"));
+        value = new byte[] {0, 0, 0, 0};
+        ipAddress = Ip4Address.valueOf("0.0.0.0");
+        assertThat(ipAddress.toOctets(), is(value));
+
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
+        ipAddress = Ip4Address.valueOf("255.255.255.255");
+        assertThat(ipAddress.toOctets(), is(value));
     }
 
     /**
-     * Tests invalid class copy constructor for a null object to copy from.
+     * Tests returning an IPv4 address as an integer.
+     */
+    @Test
+    public void testToInt() {
+        Ip4Address ipAddress;
+
+        ipAddress = Ip4Address.valueOf("1.2.3.4");
+        assertThat(ipAddress.toInt(), is(0x01020304));
+
+        ipAddress = Ip4Address.valueOf("0.0.0.0");
+        assertThat(ipAddress.toInt(), is(0));
+
+        ipAddress = Ip4Address.valueOf("255.255.255.255");
+        assertThat(ipAddress.toInt(), is(-1));
+    }
+
+    /**
+     * Tests valueOf() converter for IPv4 integer value.
+     */
+    @Test
+    public void testValueOfForIntegerIPv4() {
+        Ip4Address ipAddress;
+
+        ipAddress = Ip4Address.valueOf(0x01020304);
+        assertThat(ipAddress.toString(), is("1.2.3.4"));
+
+        ipAddress = Ip4Address.valueOf(0);
+        assertThat(ipAddress.toString(), is("0.0.0.0"));
+
+        ipAddress = Ip4Address.valueOf(0xffffffff);
+        assertThat(ipAddress.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests valueOf() converter for IPv4 byte array.
+     */
+    @Test
+    public void testValueOfByteArrayIPv4() {
+        Ip4Address ipAddress;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3, 4};
+        ipAddress = Ip4Address.valueOf(value);
+        assertThat(ipAddress.toString(), is("1.2.3.4"));
+
+        value = new byte[] {0, 0, 0, 0};
+        ipAddress = Ip4Address.valueOf(value);
+        assertThat(ipAddress.toString(), is("0.0.0.0"));
+
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
+        ipAddress = Ip4Address.valueOf(value);
+        assertThat(ipAddress.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a null array for IPv4.
      */
     @Test(expected = NullPointerException.class)
-    public void testInvalidConstructorNullObject() {
-        Ip4Address fromAddr = null;
-        Ip4Address ip4Address = new Ip4Address(fromAddr);
+    public void testInvalidValueOfNullArrayIPv4() {
+        Ip4Address ipAddress;
+        byte[] value;
+
+        value = null;
+        ipAddress = Ip4Address.valueOf(value);
     }
 
     /**
-     * Tests valid class constructor for an integer value.
-     */
-    @Test
-    public void testConstructorForInteger() {
-        Ip4Address ip4Address = new Ip4Address(0x01020304);
-        assertThat(ip4Address.toString(), is("1.2.3.4"));
-
-        ip4Address = new Ip4Address(0);
-        assertThat(ip4Address.toString(), is("0.0.0.0"));
-
-        ip4Address = new Ip4Address(0xffffffff);
-        assertThat(ip4Address.toString(), is("255.255.255.255"));
-    }
-
-    /**
-     * Tests valid class constructor for an array value.
-     */
-    @Test
-    public void testConstructorForArray() {
-        final byte[] value1 = new byte[] {1, 2, 3, 4};
-        Ip4Address ip4Address = new Ip4Address(value1);
-        assertThat(ip4Address.toString(), is("1.2.3.4"));
-
-        final byte[] value2 = new byte[] {0, 0, 0, 0};
-        ip4Address = new Ip4Address(value2);
-        assertThat(ip4Address.toString(), is("0.0.0.0"));
-
-        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff};
-        ip4Address = new Ip4Address(value3);
-        assertThat(ip4Address.toString(), is("255.255.255.255"));
-    }
-
-    /**
-     * Tests valid class constructor for an array value and an offset.
-     */
-    @Test
-    public void testConstructorForArrayAndOffset() {
-        final byte[] value1 = new byte[] {11, 22, 33,   // Preamble
-                                          1, 2, 3, 4,
-                                          44, 55};      // Extra bytes
-        Ip4Address ip4Address = new Ip4Address(value1, 3);
-        assertThat(ip4Address.toString(), is("1.2.3.4"));
-
-        final byte[] value2 = new byte[] {11, 22,       // Preamble
-                                          0, 0, 0, 0,
-                                          33};          // Extra bytes
-        ip4Address = new Ip4Address(value2, 2);
-        assertThat(ip4Address.toString(), is("0.0.0.0"));
-
-        final byte[] value3 = new byte[] {11, 22,       // Preamble
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          33};          // Extra bytes
-        ip4Address = new Ip4Address(value3, 2);
-        assertThat(ip4Address.toString(), is("255.255.255.255"));
-    }
-
-    /**
-     * Tests invalid class constructor for a null array.
-     */
-    @Test(expected = NullPointerException.class)
-    public void testInvalidConstructorNullArray() {
-        final byte[] fromArray = null;
-        Ip4Address ip4Address = new Ip4Address(fromArray);
-    }
-
-    /**
-     * Tests invalid class constructor for an array that is too short.
+     * Tests invalid valueOf() converger for an array that is too short for
+     * IPv4.
      */
     @Test(expected = IllegalArgumentException.class)
-    public void testInvalidConstructorShortArray() {
-        final byte[] fromArray = new byte[] {1, 2, 3};
-        Ip4Address ip4Address = new Ip4Address(fromArray);
+    public void testInvalidValueOfShortArrayIPv4() {
+        Ip4Address ipAddress;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3};
+        ipAddress = Ip4Address.valueOf(value);
     }
 
     /**
-     * Tests invalid class constructor for an array and an invalid offset.
-     */
-    @Test(expected = IllegalArgumentException.class)
-    public void testInvalidConstructorArrayInvalidOffset() {
-        final byte[] value1 = new byte[] {11, 22, 33,   // Preamble
-                                          1, 2, 3, 4,
-                                          44, 55};      // Extra bytes
-        Ip4Address ip4Address = new Ip4Address(value1, 6);
-    }
-
-    /**
-     * Tests valid class constructor for a string.
+     * Tests valueOf() converter for IPv4 byte array and an offset.
      */
     @Test
-    public void testConstructorForString() {
-        Ip4Address ip4Address = new Ip4Address("1.2.3.4");
-        assertThat(ip4Address.toString(), is("1.2.3.4"));
+    public void testValueOfByteArrayOffsetIPv4() {
+        Ip4Address ipAddress;
+        byte[] value;
 
-        ip4Address = new Ip4Address("0.0.0.0");
-        assertThat(ip4Address.toString(), is("0.0.0.0"));
+        value = new byte[] {11, 22, 33,                 // Preamble
+                            1, 2, 3, 4,
+                            44, 55};                    // Extra bytes
+        ipAddress = Ip4Address.valueOf(value, 3);
+        assertThat(ipAddress.toString(), is("1.2.3.4"));
 
-        ip4Address = new Ip4Address("255.255.255.255");
-        assertThat(ip4Address.toString(), is("255.255.255.255"));
+        value = new byte[] {11, 22,                     // Preamble
+                            0, 0, 0, 0,
+                            33};                        // Extra bytes
+        ipAddress = Ip4Address.valueOf(value, 2);
+        assertThat(ipAddress.toString(), is("0.0.0.0"));
+
+        value = new byte[] {11, 22,                     // Preamble
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            33};                        // Extra bytes
+        ipAddress = Ip4Address.valueOf(value, 2);
+        assertThat(ipAddress.toString(), is("255.255.255.255"));
     }
 
     /**
-     * Tests invalid class constructor for a null string.
+     * Tests invalid valueOf() converger for an array and an invalid offset
+     * for IPv4.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfArrayInvalidOffsetIPv4() {
+        Ip4Address ipAddress;
+        byte[] value;
+
+        value = new byte[] {11, 22, 33,                 // Preamble
+                            1, 2, 3, 4,
+                            44, 55};                    // Extra bytes
+        ipAddress = Ip4Address.valueOf(value, 6);
+    }
+
+    /**
+     * Tests valueOf() converter for IPv4 InetAddress.
+     */
+    @Test
+    public void testValueOfInetAddressIPv4() {
+        Ip4Address ipAddress;
+        InetAddress inetAddress;
+
+        inetAddress = InetAddresses.forString("1.2.3.4");
+        ipAddress = Ip4Address.valueOf(inetAddress);
+        assertThat(ipAddress.toString(), is("1.2.3.4"));
+
+        inetAddress = InetAddresses.forString("0.0.0.0");
+        ipAddress = Ip4Address.valueOf(inetAddress);
+        assertThat(ipAddress.toString(), is("0.0.0.0"));
+
+        inetAddress = InetAddresses.forString("255.255.255.255");
+        ipAddress = Ip4Address.valueOf(inetAddress);
+        assertThat(ipAddress.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests valueOf() converter for IPv4 string.
+     */
+    @Test
+    public void testValueOfStringIPv4() {
+        Ip4Address ipAddress;
+
+        ipAddress = Ip4Address.valueOf("1.2.3.4");
+        assertThat(ipAddress.toString(), is("1.2.3.4"));
+
+        ipAddress = Ip4Address.valueOf("0.0.0.0");
+        assertThat(ipAddress.toString(), is("0.0.0.0"));
+
+        ipAddress = Ip4Address.valueOf("255.255.255.255");
+        assertThat(ipAddress.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a null string.
      */
     @Test(expected = NullPointerException.class)
-    public void testInvalidConstructorNullString() {
+    public void testInvalidValueOfNullString() {
+        Ip4Address ipAddress;
+
         String fromString = null;
-        Ip4Address ip4Address = new Ip4Address(fromString);
+        ipAddress = Ip4Address.valueOf(fromString);
     }
 
     /**
-     * Tests invalid class constructor for an empty string.
+     * Tests invalid valueOf() converter for an empty string.
      */
     @Test(expected = IllegalArgumentException.class)
-    public void testInvalidConstructors() {
-        // Check constructor for invalid ID: empty string
-        Ip4Address ip4Address = new Ip4Address("");
+    public void testInvalidValueOfEmptyString() {
+        Ip4Address ipAddress;
+
+        String fromString = "";
+        ipAddress = Ip4Address.valueOf(fromString);
     }
 
     /**
-     * Tests returning the address as a byte array.
+     * Tests invalid valueOf() converter for an incorrect string.
      */
-    @Test
-    public void testAddressToOctets() {
-        final byte[] value1 = new byte[] {1, 2, 3, 4};
-        Ip4Address ip4Address = new Ip4Address("1.2.3.4");
-        assertThat(ip4Address.toOctets(), is(value1));
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfIncorrectString() {
+        Ip4Address ipAddress;
 
-        final byte[] value2 = new byte[] {0, 0, 0, 0};
-        ip4Address = new Ip4Address("0.0.0.0");
-        assertThat(ip4Address.toOctets(), is(value2));
-
-        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff};
-        ip4Address = new Ip4Address("255.255.255.255");
-        assertThat(ip4Address.toOctets(), is(value3));
+        String fromString = "NoSuchIpAddress";
+        ipAddress = Ip4Address.valueOf(fromString);
     }
 
     /**
-     * Tests making a mask prefix for a given prefix length.
+     * Tests making a mask prefix for a given prefix length for IPv4.
      */
     @Test
-    public void testMakeMaskPrefix() {
-        Ip4Address ip4Address = Ip4Address.makeMaskPrefix(25);
-        assertThat(ip4Address.toString(), is("255.255.255.128"));
+    public void testMakeMaskPrefixIPv4() {
+        Ip4Address ipAddress;
 
-        ip4Address = Ip4Address.makeMaskPrefix(0);
-        assertThat(ip4Address.toString(), is("0.0.0.0"));
+        ipAddress = Ip4Address.makeMaskPrefix(25);
+        assertThat(ipAddress.toString(), is("255.255.255.128"));
 
-        ip4Address = Ip4Address.makeMaskPrefix(32);
-        assertThat(ip4Address.toString(), is("255.255.255.255"));
+        ipAddress = Ip4Address.makeMaskPrefix(0);
+        assertThat(ipAddress.toString(), is("0.0.0.0"));
+
+        ipAddress = Ip4Address.makeMaskPrefix(32);
+        assertThat(ipAddress.toString(), is("255.255.255.255"));
     }
 
     /**
-     * Tests making of a masked address.
+     * Tests making a mask prefix for an invalid prefix length for IPv4:
+     * negative prefix length.
      */
-    @Test
-    public void testMakeMaskedAddress() {
-        Ip4Address ip4Address = new Ip4Address("1.2.3.5");
-        Ip4Address ip4AddressMasked =
-            Ip4Address.makeMaskedAddress(ip4Address, 24);
-        assertThat(ip4AddressMasked.toString(), is("1.2.3.0"));
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidMakeNegativeMaskPrefixIPv4() {
+        Ip4Address ipAddress;
 
-        ip4AddressMasked = Ip4Address.makeMaskedAddress(ip4Address, 0);
-        assertThat(ip4AddressMasked.toString(), is("0.0.0.0"));
-
-        ip4AddressMasked = Ip4Address.makeMaskedAddress(ip4Address, 32);
-        assertThat(ip4AddressMasked.toString(), is("1.2.3.5"));
+        ipAddress = Ip4Address.makeMaskPrefix(-1);
     }
 
     /**
-     * Tests getting the value of an address.
+     * Tests making a mask prefix for an invalid prefix length for IPv4:
+     * too long prefix length.
      */
-    @Test
-    public void testGetValue() {
-        Ip4Address ip4Address = new Ip4Address("1.2.3.4");
-        assertThat(ip4Address.getValue(), is(0x01020304));
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidMakeTooLongMaskPrefixIPv4() {
+        Ip4Address ipAddress;
 
-        ip4Address = new Ip4Address("0.0.0.0");
-        assertThat(ip4Address.getValue(), is(0));
-
-        ip4Address = new Ip4Address("255.255.255.255");
-        assertThat(ip4Address.getValue(), is(-1));
+        ipAddress = Ip4Address.makeMaskPrefix(33);
     }
 
     /**
-     * Tests equality of {@link Ip4Address}.
+     * Tests making of a masked address for IPv4.
      */
     @Test
-    public void testEquality() {
-        Ip4Address addr1 = new Ip4Address("1.2.3.4");
-        Ip4Address addr2 = new Ip4Address("1.2.3.4");
-        assertThat(addr1, is(addr2));
+    public void testMakeMaskedAddressIPv4() {
+        Ip4Address ipAddress = Ip4Address.valueOf("1.2.3.5");
+        Ip4Address ipAddressMasked;
 
-        addr1 = new Ip4Address("0.0.0.0");
-        addr2 = new Ip4Address("0.0.0.0");
-        assertThat(addr1, is(addr2));
+        ipAddressMasked = Ip4Address.makeMaskedAddress(ipAddress, 24);
+        assertThat(ipAddressMasked.toString(), is("1.2.3.0"));
 
-        addr1 = new Ip4Address("255.255.255.255");
-        addr2 = new Ip4Address("255.255.255.255");
-        assertThat(addr1, is(addr2));
+        ipAddressMasked = Ip4Address.makeMaskedAddress(ipAddress, 0);
+        assertThat(ipAddressMasked.toString(), is("0.0.0.0"));
+
+        ipAddressMasked = Ip4Address.makeMaskedAddress(ipAddress, 32);
+        assertThat(ipAddressMasked.toString(), is("1.2.3.5"));
     }
 
     /**
-     * Tests non-equality of {@link Ip4Address}.
+     * Tests making of a masked address for invalid prefix length for IPv4:
+     * negative prefix length.
      */
-    @Test
-    public void testNonEquality() {
-        Ip4Address addr1 = new Ip4Address("1.2.3.4");
-        Ip4Address addr2 = new Ip4Address("1.2.3.5");
-        Ip4Address addr3 = new Ip4Address("0.0.0.0");
-        Ip4Address addr4 = new Ip4Address("255.255.255.255");
-        assertThat(addr1, is(not(addr2)));
-        assertThat(addr3, is(not(addr2)));
-        assertThat(addr4, is(not(addr2)));
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidMakeNegativeMaskedAddressIPv4() {
+        Ip4Address ipAddress = Ip4Address.valueOf("1.2.3.5");
+        Ip4Address ipAddressMasked;
+
+        ipAddressMasked = Ip4Address.makeMaskedAddress(ipAddress, -1);
     }
 
     /**
-     * Tests comparison of {@link Ip4Address}.
+     * Tests making of a masked address for an invalid prefix length for IPv4:
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidMakeTooLongMaskedAddressIPv4() {
+        Ip4Address ipAddress = Ip4Address.valueOf("1.2.3.5");
+        Ip4Address ipAddressMasked;
+
+        ipAddressMasked = Ip4Address.makeMaskedAddress(ipAddress, 33);
+    }
+
+    /**
+     * Tests comparison of {@link Ip4Address} for IPv4.
      */
     @Test
-    public void testComparison() {
-        Ip4Address addr1 = new Ip4Address("1.2.3.4");
-        Ip4Address addr2 = new Ip4Address("1.2.3.4");
-        Ip4Address addr3 = new Ip4Address("1.2.3.3");
-        Ip4Address addr4 = new Ip4Address("1.2.3.5");
+    public void testComparisonIPv4() {
+        Ip4Address addr1, addr2, addr3, addr4;
+
+        addr1 = Ip4Address.valueOf("1.2.3.4");
+        addr2 = Ip4Address.valueOf("1.2.3.4");
+        addr3 = Ip4Address.valueOf("1.2.3.3");
+        addr4 = Ip4Address.valueOf("1.2.3.5");
         assertTrue(addr1.compareTo(addr2) == 0);
         assertTrue(addr1.compareTo(addr3) > 0);
         assertTrue(addr1.compareTo(addr4) < 0);
 
-        addr1 = new Ip4Address("255.2.3.4");
-        addr2 = new Ip4Address("255.2.3.4");
-        addr3 = new Ip4Address("255.2.3.3");
-        addr4 = new Ip4Address("255.2.3.5");
+        addr1 = Ip4Address.valueOf("255.2.3.4");
+        addr2 = Ip4Address.valueOf("255.2.3.4");
+        addr3 = Ip4Address.valueOf("255.2.3.3");
+        addr4 = Ip4Address.valueOf("255.2.3.5");
         assertTrue(addr1.compareTo(addr2) == 0);
         assertTrue(addr1.compareTo(addr3) > 0);
         assertTrue(addr1.compareTo(addr4) < 0);
     }
 
     /**
-     * Tests object string representation.
+     * Tests equality of {@link Ip4Address} for IPv4.
      */
     @Test
-    public void testToString() {
-        Ip4Address ip4Address = new Ip4Address("1.2.3.4");
-        assertThat(ip4Address.toString(), is("1.2.3.4"));
+    public void testEqualityIPv4() {
+        new EqualsTester()
+            .addEqualityGroup(Ip4Address.valueOf("1.2.3.4"),
+                              Ip4Address.valueOf("1.2.3.4"))
+            .addEqualityGroup(Ip4Address.valueOf("1.2.3.5"),
+                              Ip4Address.valueOf("1.2.3.5"))
+            .addEqualityGroup(Ip4Address.valueOf("0.0.0.0"),
+                              Ip4Address.valueOf("0.0.0.0"))
+            .addEqualityGroup(Ip4Address.valueOf("255.255.255.255"),
+                              Ip4Address.valueOf("255.255.255.255"))
+            .testEquals();
+    }
 
-        ip4Address = new Ip4Address("0.0.0.0");
-        assertThat(ip4Address.toString(), is("0.0.0.0"));
+    /**
+     * Tests object string representation for IPv4.
+     */
+    @Test
+    public void testToStringIPv4() {
+        Ip4Address ipAddress;
 
-        ip4Address = new Ip4Address("255.255.255.255");
-        assertThat(ip4Address.toString(), is("255.255.255.255"));
+        ipAddress = Ip4Address.valueOf("1.2.3.4");
+        assertThat(ipAddress.toString(), is("1.2.3.4"));
+
+        ipAddress = Ip4Address.valueOf("0.0.0.0");
+        assertThat(ipAddress.toString(), is("0.0.0.0"));
+
+        ipAddress = Ip4Address.valueOf("255.255.255.255");
+        assertThat(ipAddress.toString(), is("255.255.255.255"));
     }
 }
diff --git a/utils/misc/src/test/java/org/onlab/packet/Ip4PrefixTest.java b/utils/misc/src/test/java/org/onlab/packet/Ip4PrefixTest.java
index 84ef0b3..c731ed6 100644
--- a/utils/misc/src/test/java/org/onlab/packet/Ip4PrefixTest.java
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip4PrefixTest.java
@@ -15,12 +15,14 @@
  */
 package org.onlab.packet;
 
+import com.google.common.testing.EqualsTester;
 import org.junit.Test;
 
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 
 /**
@@ -36,175 +38,497 @@
     }
 
     /**
-     * Tests default class constructor.
+     * Tests the IPv4 prefix address version constant.
      */
     @Test
-    public void testDefaultConstructor() {
-        Ip4Prefix ip4prefix = new Ip4Prefix();
-        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
+    public void testAddressVersion() {
+        assertThat(Ip4Prefix.VERSION, is(IpAddress.Version.INET));
     }
 
     /**
-     * Tests valid class copy constructor.
+     * Tests the maximum mask length.
      */
     @Test
-    public void testCopyConstructor() {
-        Ip4Prefix fromAddr = new Ip4Prefix("1.2.3.0/24");
-        Ip4Prefix ip4prefix = new Ip4Prefix(fromAddr);
-        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
-
-        fromAddr = new Ip4Prefix("0.0.0.0/0");
-        ip4prefix = new Ip4Prefix(fromAddr);
-        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
-
-        fromAddr = new Ip4Prefix("255.255.255.255/32");
-        ip4prefix = new Ip4Prefix(fromAddr);
-        assertThat(ip4prefix.toString(), is("255.255.255.255/32"));
+    public void testMaxMaskLength() {
+        assertThat(Ip4Prefix.MAX_MASK_LENGTH, is(32));
     }
 
     /**
-     * Tests invalid class copy constructor for a null object to copy from.
-     */
-    @Test(expected = NullPointerException.class)
-    public void testInvalidConstructorNullObject() {
-        Ip4Prefix fromAddr = null;
-        Ip4Prefix ip4prefix = new Ip4Prefix(fromAddr);
-    }
-
-    /**
-     * Tests valid class constructor for an address and prefix length.
+     * Tests returning the IP version of the prefix.
      */
     @Test
-    public void testConstructorForAddressAndPrefixLength() {
-        Ip4Prefix ip4prefix =
-            new Ip4Prefix(new Ip4Address("1.2.3.0"), (short) 24);
-        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+    public void testVersion() {
+        Ip4Prefix ipPrefix;
 
-        ip4prefix = new Ip4Prefix(new Ip4Address("1.2.3.4"), (short) 24);
-        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
-
-        ip4prefix = new Ip4Prefix(new Ip4Address("1.2.3.5"), (short) 32);
-        assertThat(ip4prefix.toString(), is("1.2.3.5/32"));
-
-        ip4prefix = new Ip4Prefix(new Ip4Address("0.0.0.0"), (short) 0);
-        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
-
-        ip4prefix =
-            new Ip4Prefix(new Ip4Address("255.255.255.255"), (short) 32);
-        assertThat(ip4prefix.toString(), is("255.255.255.255/32"));
+        // IPv4
+        ipPrefix = Ip4Prefix.valueOf("0.0.0.0/0");
+        assertThat(ipPrefix.version(), is(IpAddress.Version.INET));
     }
 
     /**
-     * Tests valid class constructor for a string.
+     * Tests returning the IP address value and IP address prefix length of
+     * an IPv4 prefix.
      */
     @Test
-    public void testConstructorForString() {
-        Ip4Prefix ip4prefix = new Ip4Prefix("1.2.3.0/24");
-        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+    public void testAddressAndPrefixLengthIPv4() {
+        Ip4Prefix ipPrefix;
 
-        ip4prefix = new Ip4Prefix("1.2.3.4/24");
-        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.0/24");
+        assertThat(ipPrefix.address(), equalTo(Ip4Address.valueOf("1.2.3.0")));
+        assertThat(ipPrefix.prefixLength(), is(24));
 
-        ip4prefix = new Ip4Prefix("1.2.3.5/32");
-        assertThat(ip4prefix.toString(), is("1.2.3.5/32"));
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.4/24");
+        assertThat(ipPrefix.address(), equalTo(Ip4Address.valueOf("1.2.3.0")));
+        assertThat(ipPrefix.prefixLength(), is(24));
 
-        ip4prefix = new Ip4Prefix("0.0.0.0/0");
-        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.4/32");
+        assertThat(ipPrefix.address(), equalTo(Ip4Address.valueOf("1.2.3.4")));
+        assertThat(ipPrefix.prefixLength(), is(32));
 
-        ip4prefix = new Ip4Prefix("255.255.255.255/32");
-        assertThat(ip4prefix.toString(), is("255.255.255.255/32"));
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.5/32");
+        assertThat(ipPrefix.address(), equalTo(Ip4Address.valueOf("1.2.3.5")));
+        assertThat(ipPrefix.prefixLength(), is(32));
+
+        ipPrefix = Ip4Prefix.valueOf("0.0.0.0/0");
+        assertThat(ipPrefix.address(), equalTo(Ip4Address.valueOf("0.0.0.0")));
+        assertThat(ipPrefix.prefixLength(), is(0));
+
+        ipPrefix = Ip4Prefix.valueOf("255.255.255.255/32");
+        assertThat(ipPrefix.address(),
+                   equalTo(Ip4Address.valueOf("255.255.255.255")));
+        assertThat(ipPrefix.prefixLength(), is(32));
     }
 
     /**
-     * Tests invalid class constructor for a null string.
+     * Tests valueOf() converter for IPv4 integer value.
      */
-    @Test(expected = NullPointerException.class)
-    public void testInvalidConstructorNullString() {
-        String fromString = null;
-        Ip4Prefix ip4prefix = new Ip4Prefix(fromString);
+    @Test
+    public void testValueOfForIntegerIPv4() {
+        Ip4Prefix ipPrefix;
+
+        ipPrefix = Ip4Prefix.valueOf(0x01020304, 24);
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
+
+        ipPrefix = Ip4Prefix.valueOf(0x01020304, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.4/32"));
+
+        ipPrefix = Ip4Prefix.valueOf(0x01020305, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.5/32"));
+
+        ipPrefix = Ip4Prefix.valueOf(0, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = Ip4Prefix.valueOf(0, 32);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/32"));
+
+        ipPrefix = Ip4Prefix.valueOf(0xffffffff, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = Ip4Prefix.valueOf(0xffffffff, 16);
+        assertThat(ipPrefix.toString(), is("255.255.0.0/16"));
+
+        ipPrefix = Ip4Prefix.valueOf(0xffffffff, 32);
+        assertThat(ipPrefix.toString(), is("255.255.255.255/32"));
     }
 
     /**
-     * Tests invalid class constructor for an empty string.
+     * Tests invalid valueOf() converter for IPv4 integer value and
+     * negative prefix length.
      */
     @Test(expected = IllegalArgumentException.class)
-    public void testInvalidConstructors() {
-        // Check constructor for invalid ID: empty string
-        Ip4Prefix ip4prefix = new Ip4Prefix("");
+    public void testInvalidValueOfIntegerNegativePrefixLengthIPv4() {
+        Ip4Prefix ipPrefix;
+
+        ipPrefix = Ip4Prefix.valueOf(0x01020304, -1);
     }
 
     /**
-     * Tests getting the value of an address.
+     * Tests invalid valueOf() converter for IPv4 integer value and
+     * too long prefix length.
      */
-    @Test
-    public void testGetValue() {
-        Ip4Prefix ip4prefix = new Ip4Prefix("1.2.3.0/24");
-        assertThat(ip4prefix.getAddress(), equalTo(new Ip4Address("1.2.3.0")));
-        assertThat(ip4prefix.getPrefixLen(), is((short) 24));
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfIntegerTooLongPrefixLengthIPv4() {
+        Ip4Prefix ipPrefix;
 
-        ip4prefix = new Ip4Prefix("0.0.0.0/0");
-        assertThat(ip4prefix.getAddress(), equalTo(new Ip4Address("0.0.0.0")));
-        assertThat(ip4prefix.getPrefixLen(), is((short) 0));
-
-        ip4prefix = new Ip4Prefix("255.255.255.255/32");
-        assertThat(ip4prefix.getAddress(),
-                   equalTo(new Ip4Address("255.255.255.255")));
-        assertThat(ip4prefix.getPrefixLen(), is((short) 32));
+        ipPrefix = Ip4Prefix.valueOf(0x01020304, 33);
     }
 
     /**
-     * Tests equality of {@link Ip4Address}.
+     * Tests valueOf() converter for IPv4 byte array.
      */
     @Test
-    public void testEquality() {
-        Ip4Prefix addr1net = new Ip4Prefix("1.2.3.0/24");
-        Ip4Prefix addr2net = new Ip4Prefix("1.2.3.0/24");
-        assertThat(addr1net, is(addr2net));
+    public void testValueOfByteArrayIPv4() {
+        Ip4Prefix ipPrefix;
+        byte[] value;
 
-        addr1net = new Ip4Prefix("1.2.3.0/24");
-        addr2net = new Ip4Prefix("1.2.3.4/24");
-        assertThat(addr1net, is(addr2net));
+        value = new byte[] {1, 2, 3, 4};
+        ipPrefix = Ip4Prefix.valueOf(value, 24);
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
 
-        addr1net = new Ip4Prefix("0.0.0.0/0");
-        addr2net = new Ip4Prefix("0.0.0.0/0");
-        assertThat(addr1net, is(addr2net));
+        ipPrefix = Ip4Prefix.valueOf(value, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.4/32"));
 
-        addr1net = new Ip4Prefix("255.255.255.255/32");
-        addr2net = new Ip4Prefix("255.255.255.255/32");
-        assertThat(addr1net, is(addr2net));
+        value = new byte[] {1, 2, 3, 5};
+        ipPrefix = Ip4Prefix.valueOf(value, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.5/32"));
+
+        value = new byte[] {0, 0, 0, 0};
+        ipPrefix = Ip4Prefix.valueOf(value, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = Ip4Prefix.valueOf(value, 32);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/32"));
+
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
+        ipPrefix = Ip4Prefix.valueOf(value, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = Ip4Prefix.valueOf(value, 16);
+        assertThat(ipPrefix.toString(), is("255.255.0.0/16"));
+
+        ipPrefix = Ip4Prefix.valueOf(value, 32);
+        assertThat(ipPrefix.toString(), is("255.255.255.255/32"));
     }
 
     /**
-     * Tests non-equality of {@link Ip4Address}.
+     * Tests invalid valueOf() converter for a null array for IPv4.
      */
-    @Test
-    public void testNonEquality() {
-        Ip4Prefix addr1net = new Ip4Prefix("1.2.0.0/16");
-        Ip4Prefix addr2net = new Ip4Prefix("1.3.0.0/16");
-        Ip4Prefix addr3net = new Ip4Prefix("1.3.0.0/24");
-        Ip4Prefix addr4net = new Ip4Prefix("0.0.0.0/0");
-        Ip4Prefix addr5net = new Ip4Prefix("255.255.255.255/32");
-        assertThat(addr1net, is(not(addr2net)));
-        assertThat(addr3net, is(not(addr2net)));
-        assertThat(addr4net, is(not(addr2net)));
-        assertThat(addr5net, is(not(addr2net)));
+    @Test(expected = NullPointerException.class)
+    public void testInvalidValueOfNullArrayIPv4() {
+        Ip4Prefix ipPrefix;
+        byte[] value;
+
+        value = null;
+        ipPrefix = Ip4Prefix.valueOf(value, 24);
     }
 
     /**
-     * Tests object string representation.
+     * Tests invalid valueOf() converter for a short array for IPv4.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfShortArrayIPv4() {
+        Ip4Prefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3};
+        ipPrefix = Ip4Prefix.valueOf(value, 24);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 byte array and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfByteArrayNegativePrefixLengthIPv4() {
+        Ip4Prefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3, 4};
+        ipPrefix = Ip4Prefix.valueOf(value, -1);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 byte array and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfByteArrayTooLongPrefixLengthIPv4() {
+        Ip4Prefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3, 4};
+        ipPrefix = Ip4Prefix.valueOf(value, 33);
+    }
+
+    /**
+     * Tests valueOf() converter for IPv4 address.
      */
     @Test
-    public void testToString() {
-        Ip4Prefix ip4prefix = new Ip4Prefix("1.2.3.0/24");
-        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+    public void testValueOfAddressIPv4() {
+        Ip4Address ipAddress;
+        Ip4Prefix ipPrefix;
 
-        ip4prefix = new Ip4Prefix("1.2.3.4/24");
-        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+        ipAddress = Ip4Address.valueOf("1.2.3.4");
+        ipPrefix = Ip4Prefix.valueOf(ipAddress, 24);
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
 
-        ip4prefix = new Ip4Prefix("0.0.0.0/0");
-        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
+        ipPrefix = Ip4Prefix.valueOf(ipAddress, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.4/32"));
 
-        ip4prefix = new Ip4Prefix("255.255.255.255/32");
-        assertThat(ip4prefix.toString(), is("255.255.255.255/32"));
+        ipAddress = Ip4Address.valueOf("1.2.3.5");
+        ipPrefix = Ip4Prefix.valueOf(ipAddress, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.5/32"));
+
+        ipAddress = Ip4Address.valueOf("0.0.0.0");
+        ipPrefix = Ip4Prefix.valueOf(ipAddress, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = Ip4Prefix.valueOf(ipAddress, 32);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/32"));
+
+        ipAddress = Ip4Address.valueOf("255.255.255.255");
+        ipPrefix = Ip4Prefix.valueOf(ipAddress, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = Ip4Prefix.valueOf(ipAddress, 16);
+        assertThat(ipPrefix.toString(), is("255.255.0.0/16"));
+
+        ipPrefix = Ip4Prefix.valueOf(ipAddress, 32);
+        assertThat(ipPrefix.toString(), is("255.255.255.255/32"));
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a null IP address.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidValueOfNullAddress() {
+        Ip4Address ipAddress;
+        Ip4Prefix ipPrefix;
+
+        ipAddress = null;
+        ipPrefix = Ip4Prefix.valueOf(ipAddress, 24);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 address and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfAddressNegativePrefixLengthIPv4() {
+        Ip4Address ipAddress;
+        Ip4Prefix ipPrefix;
+
+        ipAddress = Ip4Address.valueOf("1.2.3.4");
+        ipPrefix = Ip4Prefix.valueOf(ipAddress, -1);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 address and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfAddressTooLongPrefixLengthIPv4() {
+        Ip4Address ipAddress;
+        Ip4Prefix ipPrefix;
+
+        ipAddress = Ip4Address.valueOf("1.2.3.4");
+        ipPrefix = Ip4Prefix.valueOf(ipAddress, 33);
+    }
+
+    /**
+     * Tests valueOf() converter for IPv4 string.
+     */
+    @Test
+    public void testValueOfStringIPv4() {
+        Ip4Prefix ipPrefix;
+
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.4/24");
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
+
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.4/32");
+        assertThat(ipPrefix.toString(), is("1.2.3.4/32"));
+
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.5/32");
+        assertThat(ipPrefix.toString(), is("1.2.3.5/32"));
+
+        ipPrefix = Ip4Prefix.valueOf("0.0.0.0/0");
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = Ip4Prefix.valueOf("0.0.0.0/32");
+        assertThat(ipPrefix.toString(), is("0.0.0.0/32"));
+
+        ipPrefix = Ip4Prefix.valueOf("255.255.255.255/0");
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = Ip4Prefix.valueOf("255.255.255.255/16");
+        assertThat(ipPrefix.toString(), is("255.255.0.0/16"));
+
+        ipPrefix = Ip4Prefix.valueOf("255.255.255.255/32");
+        assertThat(ipPrefix.toString(), is("255.255.255.255/32"));
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a null string.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidValueOfNullString() {
+        Ip4Prefix ipPrefix;
+        String fromString;
+
+        fromString = null;
+        ipPrefix = Ip4Prefix.valueOf(fromString);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for an empty string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfEmptyString() {
+        Ip4Prefix ipPrefix;
+        String fromString;
+
+        fromString = "";
+        ipPrefix = Ip4Prefix.valueOf(fromString);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for an incorrect string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfIncorrectString() {
+        Ip4Prefix ipPrefix;
+        String fromString;
+
+        fromString = "NoSuchIpPrefix";
+        ipPrefix = Ip4Prefix.valueOf(fromString);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 string and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfStringNegativePrefixLengthIPv4() {
+        Ip4Prefix ipPrefix;
+
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.4/-1");
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 string and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfStringTooLongPrefixLengthIPv4() {
+        Ip4Prefix ipPrefix;
+
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.4/33");
+    }
+
+    /**
+     * Tests IP prefix contains another IP prefix for IPv4.
+     */
+    @Test
+    public void testContainsIpPrefixIPv4() {
+        Ip4Prefix ipPrefix;
+
+        ipPrefix = Ip4Prefix.valueOf("1.2.0.0/24");
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/24")));
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/32")));
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.4/32")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/16")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("1.3.0.0/24")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("0.0.0.0/16")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("0.0.0.0/0")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("255.255.255.255/32")));
+
+        ipPrefix = Ip4Prefix.valueOf("1.2.0.0/32");
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/24")));
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/32")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.4/32")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/16")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("1.3.0.0/24")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("0.0.0.0/16")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("0.0.0.0/0")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("255.255.255.255/32")));
+
+        ipPrefix = Ip4Prefix.valueOf("0.0.0.0/0");
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/24")));
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/32")));
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.4/32")));
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/16")));
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("1.3.0.0/24")));
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("0.0.0.0/16")));
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("0.0.0.0/0")));
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("255.255.255.255/32")));
+
+        ipPrefix = Ip4Prefix.valueOf("255.255.255.255/32");
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/24")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/32")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.4/32")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("1.2.0.0/16")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("1.3.0.0/24")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("0.0.0.0/16")));
+        assertFalse(ipPrefix.contains(Ip4Prefix.valueOf("0.0.0.0/0")));
+        assertTrue(ipPrefix.contains(Ip4Prefix.valueOf("255.255.255.255/32")));
+    }
+
+    /**
+     * Tests IP prefix contains IP address for IPv4.
+     */
+    @Test
+    public void testContainsIpAddressIPv4() {
+        Ip4Prefix ipPrefix;
+
+        ipPrefix = Ip4Prefix.valueOf("1.2.0.0/24");
+        assertTrue(ipPrefix.contains(Ip4Address.valueOf("1.2.0.0")));
+        assertTrue(ipPrefix.contains(Ip4Address.valueOf("1.2.0.4")));
+        assertFalse(ipPrefix.contains(Ip4Address.valueOf("1.3.0.0")));
+        assertFalse(ipPrefix.contains(Ip4Address.valueOf("0.0.0.0")));
+        assertFalse(ipPrefix.contains(Ip4Address.valueOf("255.255.255.255")));
+
+        ipPrefix = Ip4Prefix.valueOf("1.2.0.0/32");
+        assertTrue(ipPrefix.contains(Ip4Address.valueOf("1.2.0.0")));
+        assertFalse(ipPrefix.contains(Ip4Address.valueOf("1.2.0.4")));
+        assertFalse(ipPrefix.contains(Ip4Address.valueOf("1.3.0.0")));
+        assertFalse(ipPrefix.contains(Ip4Address.valueOf("0.0.0.0")));
+        assertFalse(ipPrefix.contains(Ip4Address.valueOf("255.255.255.255")));
+
+        ipPrefix = Ip4Prefix.valueOf("0.0.0.0/0");
+        assertTrue(ipPrefix.contains(Ip4Address.valueOf("1.2.0.0")));
+        assertTrue(ipPrefix.contains(Ip4Address.valueOf("1.2.0.4")));
+        assertTrue(ipPrefix.contains(Ip4Address.valueOf("1.3.0.0")));
+        assertTrue(ipPrefix.contains(Ip4Address.valueOf("0.0.0.0")));
+        assertTrue(ipPrefix.contains(Ip4Address.valueOf("255.255.255.255")));
+
+        ipPrefix = Ip4Prefix.valueOf("255.255.255.255/32");
+        assertFalse(ipPrefix.contains(Ip4Address.valueOf("1.2.0.0")));
+        assertFalse(ipPrefix.contains(Ip4Address.valueOf("1.2.0.4")));
+        assertFalse(ipPrefix.contains(Ip4Address.valueOf("1.3.0.0")));
+        assertFalse(ipPrefix.contains(Ip4Address.valueOf("0.0.0.0")));
+        assertTrue(ipPrefix.contains(Ip4Address.valueOf("255.255.255.255")));
+    }
+
+    /**
+     * Tests equality of {@link Ip4Prefix} for IPv4.
+     */
+    @Test
+    public void testEqualityIPv4() {
+        new EqualsTester()
+            .addEqualityGroup(Ip4Prefix.valueOf("1.2.0.0/24"),
+                              Ip4Prefix.valueOf("1.2.0.0/24"),
+                              Ip4Prefix.valueOf("1.2.0.4/24"))
+            .addEqualityGroup(Ip4Prefix.valueOf("1.2.0.0/16"),
+                              Ip4Prefix.valueOf("1.2.0.0/16"))
+            .addEqualityGroup(Ip4Prefix.valueOf("1.2.0.0/32"),
+                              Ip4Prefix.valueOf("1.2.0.0/32"))
+            .addEqualityGroup(Ip4Prefix.valueOf("1.3.0.0/24"),
+                              Ip4Prefix.valueOf("1.3.0.0/24"))
+            .addEqualityGroup(Ip4Prefix.valueOf("0.0.0.0/0"),
+                              Ip4Prefix.valueOf("0.0.0.0/0"))
+            .addEqualityGroup(Ip4Prefix.valueOf("255.255.255.255/32"),
+                              Ip4Prefix.valueOf("255.255.255.255/32"))
+            .testEquals();
+    }
+
+    /**
+     * Tests object string representation for IPv4.
+     */
+    @Test
+    public void testToStringIPv4() {
+        Ip4Prefix ipPrefix;
+
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.0/24");
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
+
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.4/24");
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
+
+        ipPrefix = Ip4Prefix.valueOf("0.0.0.0/0");
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = Ip4Prefix.valueOf("255.255.255.255/32");
+        assertThat(ipPrefix.toString(), is("255.255.255.255/32"));
     }
 }
diff --git a/utils/misc/src/test/java/org/onlab/packet/Ip6AddressTest.java b/utils/misc/src/test/java/org/onlab/packet/Ip6AddressTest.java
index 9170461..e7d017d 100644
--- a/utils/misc/src/test/java/org/onlab/packet/Ip6AddressTest.java
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip6AddressTest.java
@@ -15,10 +15,13 @@
  */
 package org.onlab.packet;
 
+import com.google.common.net.InetAddresses;
+import com.google.common.testing.EqualsTester;
 import org.junit.Test;
 
+import java.net.InetAddress;
+
 import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
@@ -36,10 +39,18 @@
     }
 
     /**
+     * Tests the IPv4 address version constant.
+     */
+    @Test
+    public void testAddressVersion() {
+        assertThat(Ip6Address.VERSION, is(IpAddress.Version.INET6));
+    }
+
+    /**
      * Tests the length of the address in bytes (octets).
      */
     @Test
-    public void testAddrBytelen() {
+    public void testAddrByteLength() {
         assertThat(Ip6Address.BYTE_LENGTH, is(16));
     }
 
@@ -47,406 +58,442 @@
      * Tests the length of the address in bits.
      */
     @Test
-    public void testAddrBitlen() {
+    public void testAddrBitLength() {
         assertThat(Ip6Address.BIT_LENGTH, is(128));
     }
 
     /**
-     * Tests default class constructor.
+     * Tests returning the IP address version.
      */
     @Test
-    public void testDefaultConstructor() {
-        Ip6Address ip6Address = new Ip6Address();
-        assertThat(ip6Address.toString(), is("::"));
+    public void testVersion() {
+        IpAddress ipAddress;
+
+        // IPv6
+        ipAddress = IpAddress.valueOf("::");
+        assertThat(ipAddress.version(), is(IpAddress.Version.INET6));
     }
 
     /**
-     * Tests valid class copy constructor.
+     * Tests returning an IPv6 address as a byte array.
      */
     @Test
-    public void testCopyConstructor() {
-        Ip6Address fromAddr =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
-        Ip6Address ip6Address = new Ip6Address(fromAddr);
-        assertThat(ip6Address.toString(),
+    public void testAddressToOctetsIPv6() {
+        Ip6Address ipAddress;
+        byte[] value;
+
+        value = new byte[] {0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77,
+                            (byte) 0x88, (byte) 0x88};
+        ipAddress =
+            Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
+        assertThat(ipAddress.toOctets(), is(value));
+
+        value = new byte[] {0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00};
+        ipAddress = Ip6Address.valueOf("::");
+        assertThat(ipAddress.toOctets(), is(value));
+
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
+        ipAddress =
+            Ip6Address.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        assertThat(ipAddress.toOctets(), is(value));
+    }
+
+    /**
+     * Tests valueOf() converter for IPv6 byte array.
+     */
+    @Test
+    public void testValueOfByteArrayIPv6() {
+        Ip6Address ipAddress;
+        byte[] value;
+
+        value = new byte[] {0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77,
+                            (byte) 0x88, (byte) 0x88};
+        ipAddress = Ip6Address.valueOf(value);
+        assertThat(ipAddress.toString(),
                    is("1111:2222:3333:4444:5555:6666:7777:8888"));
 
-        fromAddr = new Ip6Address("::");
-        ip6Address = new Ip6Address(fromAddr);
-        assertThat(ip6Address.toString(), is("::"));
+        value = new byte[] {0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00};
+        ipAddress = Ip6Address.valueOf(value);
+        assertThat(ipAddress.toString(), is("::"));
 
-        fromAddr = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
-        ip6Address = new Ip6Address(fromAddr);
-        assertThat(ip6Address.toString(),
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
+        ipAddress = Ip6Address.valueOf(value);
+        assertThat(ipAddress.toString(),
                    is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
     }
 
     /**
-     * Tests invalid class copy constructor for a null object to copy from.
+     * Tests invalid valueOf() converter for a null array for IPv6.
      */
     @Test(expected = NullPointerException.class)
-    public void testInvalidConstructorNullObject() {
-        Ip6Address fromAddr = null;
-        Ip6Address ip6Address = new Ip6Address(fromAddr);
+    public void testInvalidValueOfNullArrayIPv6() {
+        Ip6Address ipAddress;
+        byte[] value;
+
+        value = null;
+        ipAddress = Ip6Address.valueOf(value);
     }
 
     /**
-     * Tests valid class constructor for integer values.
-     */
-    @Test
-    public void testConstructorForInteger() {
-        Ip6Address ip6Address =
-            new Ip6Address(0x1111222233334444L, 0x5555666677778888L);
-        assertThat(ip6Address.toString(),
-                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
-
-        ip6Address = new Ip6Address(0L, 0L);
-        assertThat(ip6Address.toString(), is("::"));
-
-        ip6Address = new Ip6Address(-1L, -1L);
-        assertThat(ip6Address.toString(),
-                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
-    }
-
-    /**
-     * Tests valid class constructor for an array value.
-     */
-    @Test
-    public void testConstructorForArray() {
-        final byte[] value1 = new byte[] {0x11, 0x11, 0x22, 0x22,
-                                          0x33, 0x33, 0x44, 0x44,
-                                          0x55, 0x55, 0x66, 0x66,
-                                          0x77, 0x77,
-                                          (byte) 0x88, (byte) 0x88};
-        Ip6Address ip6Address = new Ip6Address(value1);
-        assertThat(ip6Address.toString(),
-                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
-
-        final byte[] value2 = new byte[] {0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00};
-        ip6Address = new Ip6Address(value2);
-        assertThat(ip6Address.toString(), is("::"));
-
-        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff};
-        ip6Address = new Ip6Address(value3);
-        assertThat(ip6Address.toString(),
-                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
-    }
-
-    /**
-     * Tests valid class constructor for an array value and an offset.
-     */
-    @Test
-    public void testConstructorForArrayAndOffset() {
-        final byte[] value1 = new byte[] {11, 22, 33,           // Preamble
-                                          0x11, 0x11, 0x22, 0x22,
-                                          0x33, 0x33, 0x44, 0x44,
-                                          0x55, 0x55, 0x66, 0x66,
-                                          0x77, 0x77,
-                                          (byte) 0x88, (byte) 0x88,
-                                          44, 55};              // Extra bytes
-        Ip6Address ip6Address = new Ip6Address(value1, 3);
-        assertThat(ip6Address.toString(),
-                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
-
-        final byte[] value2 = new byte[] {11, 22,               // Preamble
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          33};                  // Extra bytes
-        ip6Address = new Ip6Address(value2, 2);
-        assertThat(ip6Address.toString(), is("::"));
-
-        final byte[] value3 = new byte[] {11, 22,               // Preamble
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          33};                  // Extra bytes
-        ip6Address = new Ip6Address(value3, 2);
-        assertThat(ip6Address.toString(),
-                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
-    }
-
-    /**
-     * Tests invalid class constructor for a null array.
-     */
-    @Test(expected = NullPointerException.class)
-    public void testInvalidConstructorNullArray() {
-        final byte[] fromArray = null;
-        Ip6Address ip6Address = new Ip6Address(fromArray);
-    }
-
-    /**
-     * Tests invalid class constructor for an array that is too short.
+     * Tests invalid valueOf() converger for an array that is too short for
+     * IPv6.
      */
     @Test(expected = IllegalArgumentException.class)
-    public void testInvalidConstructorShortArray() {
-        final byte[] fromArray = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
-        Ip6Address ip6Address = new Ip6Address(fromArray);
+    public void testInvalidValueOfShortArrayIPv6() {
+        Ip6Address ipAddress;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
+        ipAddress = Ip6Address.valueOf(value);
     }
 
     /**
-     * Tests invalid class constructor for an array and an invalid offset.
-     */
-    @Test(expected = IllegalArgumentException.class)
-    public void testInvalidConstructorArrayInvalidOffset() {
-        final byte[] value1 = new byte[] {11, 22, 33,           // Preamble
-                                          0x11, 0x11, 0x22, 0x22,
-                                          0x33, 0x33, 0x44, 0x44,
-                                          0x55, 0x55, 0x66, 0x66,
-                                          0x77, 0x77,
-                                          (byte) 0x88, (byte) 0x88,
-                                          44, 55};              // Extra bytes
-        Ip6Address ip6Address = new Ip6Address(value1, 6);
-    }
-
-    /**
-     * Tests valid class constructor for a string.
+     * Tests valueOf() converter for IPv6 byte array and an offset.
      */
     @Test
-    public void testConstructorForString() {
-        Ip6Address ip6Address =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
-        assertThat(ip6Address.toString(),
+    public void testValueOfByteArrayOffsetIPv6() {
+        Ip6Address ipAddress;
+        byte[] value;
+
+        value = new byte[] {11, 22, 33,                         // Preamble
+                            0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77,
+                            (byte) 0x88, (byte) 0x88,
+                            44, 55};                            // Extra bytes
+        ipAddress = Ip6Address.valueOf(value, 3);
+        assertThat(ipAddress.toString(),
                    is("1111:2222:3333:4444:5555:6666:7777:8888"));
 
-        ip6Address = new Ip6Address("::");
-        assertThat(ip6Address.toString(), is("::"));
+        value = new byte[] {11, 22,                             // Preamble
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            33};                                // Extra bytes
+        ipAddress = Ip6Address.valueOf(value, 2);
+        assertThat(ipAddress.toString(), is("::"));
 
-        ip6Address = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
-        assertThat(ip6Address.toString(),
+        value = new byte[] {11, 22,                             // Preamble
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            33};                                // Extra bytes
+        ipAddress = Ip6Address.valueOf(value, 2);
+        assertThat(ipAddress.toString(),
                    is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
     }
 
     /**
-     * Tests invalid class constructor for a null string.
+     * Tests invalid valueOf() converger for an array and an invalid offset
+     * for IPv6.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfArrayInvalidOffsetIPv6() {
+        Ip6Address ipAddress;
+        byte[] value;
+
+        value = new byte[] {11, 22, 33,                         // Preamble
+                            0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77,
+                            (byte) 0x88, (byte) 0x88,
+                            44, 55};                            // Extra bytes
+        ipAddress = Ip6Address.valueOf(value, 6);
+    }
+
+    /**
+     * Tests valueOf() converter for IPv6 InetAddress.
+     */
+    @Test
+    public void testValueOfInetAddressIPv6() {
+        Ip6Address ipAddress;
+        InetAddress inetAddress;
+
+        inetAddress =
+            InetAddresses.forString("1111:2222:3333:4444:5555:6666:7777:8888");
+        ipAddress = Ip6Address.valueOf(inetAddress);
+        assertThat(ipAddress.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
+
+        inetAddress = InetAddresses.forString("::");
+        ipAddress = Ip6Address.valueOf(inetAddress);
+        assertThat(ipAddress.toString(), is("::"));
+
+        inetAddress =
+            InetAddresses.forString("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        ipAddress = Ip6Address.valueOf(inetAddress);
+        assertThat(ipAddress.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+    }
+
+    /**
+     * Tests valueOf() converter for IPv6 string.
+     */
+    @Test
+    public void testValueOfStringIPv6() {
+        Ip6Address ipAddress;
+
+        ipAddress =
+            Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
+        assertThat(ipAddress.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
+
+        ipAddress = Ip6Address.valueOf("::");
+        assertThat(ipAddress.toString(), is("::"));
+
+        ipAddress =
+            Ip6Address.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        assertThat(ipAddress.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a null string.
      */
     @Test(expected = NullPointerException.class)
-    public void testInvalidConstructorNullString() {
+    public void testInvalidValueOfNullString() {
+        Ip6Address ipAddress;
+
         String fromString = null;
-        Ip6Address ip6Address = new Ip6Address(fromString);
+        ipAddress = Ip6Address.valueOf(fromString);
     }
 
     /**
-     * Tests invalid class constructor for an empty string.
+     * Tests invalid valueOf() converter for an empty string.
      */
     @Test(expected = IllegalArgumentException.class)
-    public void testInvalidConstructors() {
-        // Check constructor for invalid ID: empty string
-        Ip6Address ip6Address = new Ip6Address("");
+    public void testInvalidValueOfEmptyString() {
+        Ip6Address ipAddress;
+
+        String fromString = "";
+        ipAddress = Ip6Address.valueOf(fromString);
     }
 
     /**
-     * Tests returning the address as a byte array.
+     * Tests invalid valueOf() converter for an incorrect string.
      */
-    @Test
-    public void testAddressToOctets() {
-        final byte[] value1 = new byte[] {0x11, 0x11, 0x22, 0x22,
-                                          0x33, 0x33, 0x44, 0x44,
-                                          0x55, 0x55, 0x66, 0x66,
-                                          0x77, 0x77,
-                                          (byte) 0x88, (byte) 0x88};
-        Ip6Address ip6Address =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
-        assertThat(ip6Address.toOctets(), is(value1));
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfIncorrectString() {
+        Ip6Address ipAddress;
 
-        final byte[] value2 = new byte[] {0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00};
-        ip6Address = new Ip6Address("::");
-        assertThat(ip6Address.toOctets(), is(value2));
-
-        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff};
-        ip6Address = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
-        assertThat(ip6Address.toOctets(), is(value3));
+        String fromString = "NoSuchIpAddress";
+        ipAddress = Ip6Address.valueOf(fromString);
     }
 
     /**
-     * Tests making a mask prefix for a given prefix length.
+     * Tests making a mask prefix for a given prefix length for IPv6.
      */
     @Test
-    public void testMakeMaskPrefix() {
-        Ip6Address ip6Address = Ip6Address.makeMaskPrefix(8);
-        assertThat(ip6Address.toString(), is("ff00::"));
+    public void testMakeMaskPrefixIPv6() {
+        Ip6Address ipAddress;
 
-        ip6Address = Ip6Address.makeMaskPrefix(120);
-        assertThat(ip6Address.toString(),
+        ipAddress = Ip6Address.makeMaskPrefix(8);
+        assertThat(ipAddress.toString(), is("ff00::"));
+
+        ipAddress = Ip6Address.makeMaskPrefix(120);
+        assertThat(ipAddress.toString(),
                    is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00"));
 
-        ip6Address = Ip6Address.makeMaskPrefix(0);
-        assertThat(ip6Address.toString(), is("::"));
+        ipAddress = Ip6Address.makeMaskPrefix(0);
+        assertThat(ipAddress.toString(), is("::"));
 
-        ip6Address = Ip6Address.makeMaskPrefix(128);
-        assertThat(ip6Address.toString(),
+        ipAddress = Ip6Address.makeMaskPrefix(128);
+        assertThat(ipAddress.toString(),
                    is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
 
-        ip6Address = Ip6Address.makeMaskPrefix(64);
-        assertThat(ip6Address.toString(), is("ffff:ffff:ffff:ffff::"));
+        ipAddress = Ip6Address.makeMaskPrefix(64);
+        assertThat(ipAddress.toString(), is("ffff:ffff:ffff:ffff::"));
     }
 
     /**
-     * Tests making of a masked address.
+     * Tests making a mask prefix for an invalid prefix length for IPv6:
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidMakeNegativeMaskPrefixIPv6() {
+        Ip6Address ipAddress;
+
+        ipAddress = Ip6Address.makeMaskPrefix(-1);
+    }
+
+    /**
+     * Tests making a mask prefix for an invalid prefix length for IPv6:
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidMakeTooLongMaskPrefixIPv6() {
+        Ip6Address ipAddress;
+
+        ipAddress = Ip6Address.makeMaskPrefix(129);
+    }
+
+    /**
+     * Tests making of a masked address for IPv6.
      */
     @Test
-    public void testMakeMaskedAddress() {
-        Ip6Address ip6Address =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8885");
-        Ip6Address ip6AddressMasked =
-            Ip6Address.makeMaskedAddress(ip6Address, 8);
-        assertThat(ip6AddressMasked.toString(), is("1100::"));
+    public void testMakeMaskedAddressIPv6() {
+        Ip6Address ipAddress =
+            Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8885");
+        Ip6Address ipAddressMasked;
 
-        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 120);
-        assertThat(ip6AddressMasked.toString(),
+        ipAddressMasked = Ip6Address.makeMaskedAddress(ipAddress, 8);
+        assertThat(ipAddressMasked.toString(), is("1100::"));
+
+        ipAddressMasked = Ip6Address.makeMaskedAddress(ipAddress, 120);
+        assertThat(ipAddressMasked.toString(),
                    is("1111:2222:3333:4444:5555:6666:7777:8800"));
 
-        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 0);
-        assertThat(ip6AddressMasked.toString(), is("::"));
+        ipAddressMasked = Ip6Address.makeMaskedAddress(ipAddress, 0);
+        assertThat(ipAddressMasked.toString(), is("::"));
 
-        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 128);
-        assertThat(ip6AddressMasked.toString(),
+        ipAddressMasked = Ip6Address.makeMaskedAddress(ipAddress, 128);
+        assertThat(ipAddressMasked.toString(),
                    is("1111:2222:3333:4444:5555:6666:7777:8885"));
 
-        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 64);
-        assertThat(ip6AddressMasked.toString(), is("1111:2222:3333:4444::"));
+        ipAddressMasked = Ip6Address.makeMaskedAddress(ipAddress, 64);
+        assertThat(ipAddressMasked.toString(), is("1111:2222:3333:4444::"));
     }
 
     /**
-     * Tests getting the value of an address.
+     * Tests making of a masked address for invalid prefix length for IPv6:
+     * negative prefix length.
      */
-    @Test
-    public void testGetValue() {
-        Ip6Address ip6Address =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
-        assertThat(ip6Address.getValueHigh(), is(0x1111222233334444L));
-        assertThat(ip6Address.getValueLow(), is(0x5555666677778888L));
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidMakeNegativeMaskedAddressIPv6() {
+        Ip6Address ipAddress =
+            Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8885");
+        Ip6Address ipAddressMasked;
 
-        ip6Address = new Ip6Address(0, 0);
-        assertThat(ip6Address.getValueHigh(), is(0L));
-        assertThat(ip6Address.getValueLow(), is(0L));
-
-        ip6Address = new Ip6Address(-1L, -1L);
-        assertThat(ip6Address.getValueHigh(), is(-1L));
-        assertThat(ip6Address.getValueLow(), is(-1L));
+        ipAddressMasked = Ip6Address.makeMaskedAddress(ipAddress, -1);
     }
 
     /**
-     * Tests equality of {@link Ip6Address}.
+     * Tests making of a masked address for an invalid prefix length for IPv6:
+     * too long prefix length.
      */
-    @Test
-    public void testEquality() {
-        Ip6Address addr1 =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
-        Ip6Address addr2 =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
-        assertThat(addr1, is(addr2));
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidMakeTooLongMaskedAddressIPv6() {
+        Ip6Address ipAddress =
+            Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8885");
+        Ip6Address ipAddressMasked;
 
-        addr1 = new Ip6Address("::");
-        addr2 = new Ip6Address("::");
-        assertThat(addr1, is(addr2));
-
-        addr1 = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
-        addr2 = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
-        assertThat(addr1, is(addr2));
+        ipAddressMasked = Ip6Address.makeMaskedAddress(ipAddress, 129);
     }
 
     /**
-     * Tests non-equality of {@link Ip6Address}.
+     * Tests comparison of {@link Ip6Address} for IPv6.
      */
     @Test
-    public void testNonEquality() {
-        Ip6Address addr1 =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
-        Ip6Address addr2 =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:888A");
-        Ip6Address addr3 = new Ip6Address("::");
-        Ip6Address addr4 =
-            new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
-        assertThat(addr1, is(not(addr2)));
-        assertThat(addr3, is(not(addr2)));
-        assertThat(addr4, is(not(addr2)));
-    }
+    public void testComparisonIPv6() {
+        Ip6Address addr1, addr2, addr3, addr4;
 
-    /**
-     * Tests comparison of {@link Ip6Address}.
-     */
-    @Test
-    public void testComparison() {
-        Ip6Address addr1 =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
-        Ip6Address addr2 =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
-        Ip6Address addr3 =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8887");
-        Ip6Address addr4 =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8889");
+        addr1 = Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
+        addr2 = Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
+        addr3 = Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8887");
+        addr4 = Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8889");
         assertTrue(addr1.compareTo(addr2) == 0);
         assertTrue(addr1.compareTo(addr3) > 0);
         assertTrue(addr1.compareTo(addr4) < 0);
 
-        addr1 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8888");
-        addr2 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8888");
-        addr3 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8887");
-        addr4 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8889");
+        addr1 = Ip6Address.valueOf("ffff:2222:3333:4444:5555:6666:7777:8888");
+        addr2 = Ip6Address.valueOf("ffff:2222:3333:4444:5555:6666:7777:8888");
+        addr3 = Ip6Address.valueOf("ffff:2222:3333:4444:5555:6666:7777:8887");
+        addr4 = Ip6Address.valueOf("ffff:2222:3333:4444:5555:6666:7777:8889");
         assertTrue(addr1.compareTo(addr2) == 0);
         assertTrue(addr1.compareTo(addr3) > 0);
         assertTrue(addr1.compareTo(addr4) < 0);
 
-        addr1 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8888");
-        addr2 = new Ip6Address("ffff:2222:3333:4444:5555:6666:7777:8888");
-        addr3 = new Ip6Address("ffff:2222:3333:4443:5555:6666:7777:8888");
-        addr4 = new Ip6Address("ffff:2222:3333:4445:5555:6666:7777:8888");
+        addr1 = Ip6Address.valueOf("ffff:2222:3333:4444:5555:6666:7777:8888");
+        addr2 = Ip6Address.valueOf("ffff:2222:3333:4444:5555:6666:7777:8888");
+        addr3 = Ip6Address.valueOf("ffff:2222:3333:4443:5555:6666:7777:8888");
+        addr4 = Ip6Address.valueOf("ffff:2222:3333:4445:5555:6666:7777:8888");
         assertTrue(addr1.compareTo(addr2) == 0);
         assertTrue(addr1.compareTo(addr3) > 0);
         assertTrue(addr1.compareTo(addr4) < 0);
     }
 
     /**
-     * Tests object string representation.
+     * Tests equality of {@link Ip6Address} for IPv6.
      */
     @Test
-    public void testToString() {
-        Ip6Address ip6Address =
-            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
-        assertThat(ip6Address.toString(),
+    public void testEqualityIPv6() {
+        new EqualsTester()
+            .addEqualityGroup(
+                Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8888"),
+                Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8888"))
+            .addEqualityGroup(
+                Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:888a"),
+                Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:888a"))
+            .addEqualityGroup(
+                Ip6Address.valueOf("::"),
+                Ip6Address.valueOf("::"))
+            .addEqualityGroup(
+                Ip6Address.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"),
+                Ip6Address.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"))
+            .testEquals();
+    }
+
+    /**
+     * Tests object string representation for IPv6.
+     */
+    @Test
+    public void testToStringIPv6() {
+        Ip6Address ipAddress;
+
+        ipAddress =
+            Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
+        assertThat(ipAddress.toString(),
                    is("1111:2222:3333:4444:5555:6666:7777:8888"));
 
-        ip6Address = new Ip6Address("1111::8888");
-        assertThat(ip6Address.toString(), is("1111::8888"));
+        ipAddress = Ip6Address.valueOf("1111::8888");
+        assertThat(ipAddress.toString(), is("1111::8888"));
 
-        ip6Address = new Ip6Address("1111::");
-        assertThat(ip6Address.toString(), is("1111::"));
+        ipAddress = Ip6Address.valueOf("1111::");
+        assertThat(ipAddress.toString(), is("1111::"));
 
-        ip6Address = new Ip6Address("::8888");
-        assertThat(ip6Address.toString(), is("::8888"));
+        ipAddress = Ip6Address.valueOf("::8888");
+        assertThat(ipAddress.toString(), is("::8888"));
 
-        ip6Address = new Ip6Address("::");
-        assertThat(ip6Address.toString(), is("::"));
+        ipAddress = Ip6Address.valueOf("::");
+        assertThat(ipAddress.toString(), is("::"));
 
-        ip6Address = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
-        assertThat(ip6Address.toString(),
+        ipAddress =
+            Ip6Address.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        assertThat(ipAddress.toString(),
                    is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
     }
 }
diff --git a/utils/misc/src/test/java/org/onlab/packet/Ip6PrefixTest.java b/utils/misc/src/test/java/org/onlab/packet/Ip6PrefixTest.java
index 59b19f6..dceeb84 100644
--- a/utils/misc/src/test/java/org/onlab/packet/Ip6PrefixTest.java
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip6PrefixTest.java
@@ -15,12 +15,14 @@
  */
 package org.onlab.packet;
 
+import com.google.common.testing.EqualsTester;
 import org.junit.Test;
 
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
-import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 
 /**
@@ -36,235 +38,531 @@
     }
 
     /**
-     * Tests default class constructor.
+     * Tests the IPv4 prefix address version constant.
      */
     @Test
-    public void testDefaultConstructor() {
-        Ip6Prefix ip6prefix = new Ip6Prefix();
-        assertThat(ip6prefix.toString(), is("::/0"));
+    public void testAddressVersion() {
+        assertThat(Ip6Prefix.VERSION, is(IpAddress.Version.INET6));
     }
 
     /**
-     * Tests valid class copy constructor.
+     * Tests the maximum mask length.
      */
     @Test
-    public void testCopyConstructor() {
-        Ip6Prefix fromAddr = new Ip6Prefix("1100::/8");
-        Ip6Prefix ip6prefix = new Ip6Prefix(fromAddr);
-        assertThat(ip6prefix.toString(), is("1100::/8"));
-
-        fromAddr = new Ip6Prefix("::/0");
-        ip6prefix = new Ip6Prefix(fromAddr);
-        assertThat(ip6prefix.toString(), is("::/0"));
-
-        fromAddr =
-            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
-        ip6prefix = new Ip6Prefix(fromAddr);
-        assertThat(ip6prefix.toString(),
-                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
+    public void testMaxMaskLength() {
+        assertThat(Ip6Prefix.MAX_MASK_LENGTH, is(128));
     }
 
     /**
-     * Tests invalid class copy constructor for a null object to copy from.
+     * Tests returning the IP version of the prefix.
      */
-    @Test(expected = NullPointerException.class)
-    public void testInvalidConstructorNullObject() {
-        Ip6Prefix fromAddr = null;
-        Ip6Prefix ip6prefix = new Ip6Prefix(fromAddr);
+    @Test
+    public void testVersion() {
+        Ip6Prefix ipPrefix;
+
+        // IPv6
+        ipPrefix = Ip6Prefix.valueOf("::/0");
+        assertThat(ipPrefix.version(), is(IpAddress.Version.INET6));
     }
 
     /**
-     * Tests valid class constructor for an address and prefix length.
+     * Tests returning the IP address value and IP address prefix length of
+     * an IPv6 prefix.
      */
     @Test
-    public void testConstructorForAddressAndPrefixLength() {
-        Ip6Prefix ip6prefix =
-            new Ip6Prefix(new Ip6Address("1100::"), (short) 8);
-        assertThat(ip6prefix.toString(), is("1100::/8"));
+    public void testAddressAndPrefixLengthIPv6() {
+        Ip6Prefix ipPrefix;
 
-        ip6prefix =
-            new Ip6Prefix(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8885"),
-                        (short) 8);
-        assertThat(ip6prefix.toString(), is("1100::/8"));
+        ipPrefix = Ip6Prefix.valueOf("1100::/8");
+        assertThat(ipPrefix.address(), equalTo(Ip6Address.valueOf("1100::")));
+        assertThat(ipPrefix.prefixLength(), is(8));
 
-        ip6prefix =
-            new Ip6Prefix(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8800"),
-                        (short) 120);
-        assertThat(ip6prefix.toString(),
+        ipPrefix =
+            Ip6Prefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8885/8");
+        assertThat(ipPrefix.address(), equalTo(Ip6Address.valueOf("1100::")));
+        assertThat(ipPrefix.prefixLength(), is(8));
+
+        ipPrefix =
+            Ip6Prefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8800/120");
+        assertThat(ipPrefix.address(),
+                   equalTo(Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8800")));
+        assertThat(ipPrefix.prefixLength(), is(120));
+
+        ipPrefix =
+            Ip6Prefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8885/128");
+        assertThat(ipPrefix.address(),
+                   equalTo(Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8885")));
+        assertThat(ipPrefix.prefixLength(), is(128));
+
+        ipPrefix = Ip6Prefix.valueOf("::/0");
+        assertThat(ipPrefix.address(), equalTo(Ip6Address.valueOf("::")));
+        assertThat(ipPrefix.prefixLength(), is(0));
+
+        ipPrefix =
+            Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertThat(ipPrefix.address(),
+                   equalTo(Ip6Address.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+        assertThat(ipPrefix.prefixLength(), is(128));
+
+        ipPrefix =
+            Ip6Prefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8885/64");
+        assertThat(ipPrefix.address(),
+                   equalTo(Ip6Address.valueOf("1111:2222:3333:4444::")));
+        assertThat(ipPrefix.prefixLength(), is(64));
+    }
+
+    /**
+     * Tests valueOf() converter for IPv6 byte array.
+     */
+    @Test
+    public void testValueOfByteArrayIPv6() {
+        Ip6Prefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77, (byte) 0x88, (byte) 0x88};
+        ipPrefix = Ip6Prefix.valueOf(value, 120);
+        assertThat(ipPrefix.toString(),
                    is("1111:2222:3333:4444:5555:6666:7777:8800/120"));
 
-        ip6prefix = new Ip6Prefix(new Ip6Address("::"), (short) 0);
-        assertThat(ip6prefix.toString(), is("::/0"));
+        ipPrefix = Ip6Prefix.valueOf(value, 128);
+        assertThat(ipPrefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888/128"));
 
-        ip6prefix =
-            new Ip6Prefix(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8885"),
-                        (short) 128);
-        assertThat(ip6prefix.toString(),
-                   is("1111:2222:3333:4444:5555:6666:7777:8885/128"));
+        value = new byte[] {0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00};
+        ipPrefix = Ip6Prefix.valueOf(value, 0);
+        assertThat(ipPrefix.toString(), is("::/0"));
 
-        ip6prefix =
-            new Ip6Prefix(new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"),
-                        (short) 128);
-        assertThat(ip6prefix.toString(),
+        ipPrefix = Ip6Prefix.valueOf(value, 128);
+        assertThat(ipPrefix.toString(), is("::/128"));
+
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
+        ipPrefix = Ip6Prefix.valueOf(value, 0);
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix = Ip6Prefix.valueOf(value, 64);
+        assertThat(ipPrefix.toString(), is("ffff:ffff:ffff:ffff::/64"));
+
+        ipPrefix = Ip6Prefix.valueOf(value, 128);
+        assertThat(ipPrefix.toString(),
                    is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
-
-        ip6prefix =
-            new Ip6Prefix(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8885"),
-                        (short) 64);
-        assertThat(ip6prefix.toString(), is("1111:2222:3333:4444::/64"));
     }
 
     /**
-     * Tests valid class constructor for a string.
-     */
-    @Test
-    public void testConstructorForString() {
-        Ip6Prefix ip6prefix = new Ip6Prefix("1100::/8");
-        assertThat(ip6prefix.toString(), is("1100::/8"));
-
-        ip6prefix = new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/8");
-        assertThat(ip6prefix.toString(), is("1100::/8"));
-
-        ip6prefix =
-            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8800/120");
-        assertThat(ip6prefix.toString(),
-                   is("1111:2222:3333:4444:5555:6666:7777:8800/120"));
-
-        ip6prefix = new Ip6Prefix("::/0");
-        assertThat(ip6prefix.toString(), is("::/0"));
-
-        ip6prefix =
-            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/128");
-        assertThat(ip6prefix.toString(),
-                   is("1111:2222:3333:4444:5555:6666:7777:8885/128"));
-
-        ip6prefix = new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
-        assertThat(ip6prefix.toString(),
-                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
-
-        ip6prefix =
-            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/64");
-        assertThat(ip6prefix.toString(), is("1111:2222:3333:4444::/64"));
-    }
-
-    /**
-     * Tests invalid class constructor for a null string.
+     * Tests invalid valueOf() converter for a null array for IPv6.
      */
     @Test(expected = NullPointerException.class)
-    public void testInvalidConstructorNullString() {
-        String fromString = null;
-        Ip6Prefix ip6prefix = new Ip6Prefix(fromString);
+    public void testInvalidValueOfNullArrayIPv6() {
+        Ip6Prefix ipPrefix;
+        byte[] value;
+
+        value = null;
+        ipPrefix = Ip6Prefix.valueOf(value, 120);
     }
 
     /**
-     * Tests invalid class constructor for an empty string.
+     * Tests invalid valueOf() converter for a short array for IPv6.
      */
     @Test(expected = IllegalArgumentException.class)
-    public void testInvalidConstructors() {
-        // Check constructor for invalid ID: empty string
-        Ip6Prefix ip6prefix = new Ip6Prefix("");
+    public void testInvalidValueOfShortArrayIPv6() {
+        Ip6Prefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
+        ipPrefix = Ip6Prefix.valueOf(value, 120);
     }
 
     /**
-     * Tests getting the value of an address.
+     * Tests invalid valueOf() converter for IPv6 byte array and
+     * negative prefix length.
      */
-    @Test
-    public void testGetValue() {
-        Ip6Prefix ip6prefix = new Ip6Prefix("1100::/8");
-        assertThat(ip6prefix.getAddress(), equalTo(new Ip6Address("1100::")));
-        assertThat(ip6prefix.getPrefixLen(), is((short) 8));
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfByteArrayNegativePrefixLengthIPv6() {
+        Ip6Prefix ipPrefix;
+        byte[] value;
 
-        ip6prefix = new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/8");
-        assertThat(ip6prefix.getAddress(), equalTo(new Ip6Address("1100::")));
-        assertThat(ip6prefix.getPrefixLen(), is((short) 8));
-
-        ip6prefix =
-            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8800/120");
-        assertThat(ip6prefix.getAddress(),
-                   equalTo(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8800")));
-        assertThat(ip6prefix.getPrefixLen(), is((short) 120));
-
-        ip6prefix = new Ip6Prefix("::/0");
-        assertThat(ip6prefix.getAddress(), equalTo(new Ip6Address("::")));
-        assertThat(ip6prefix.getPrefixLen(), is((short) 0));
-
-        ip6prefix =
-            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/128");
-        assertThat(ip6prefix.getAddress(),
-                   equalTo(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8885")));
-        assertThat(ip6prefix.getPrefixLen(), is((short) 128));
-
-        ip6prefix =
-            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
-        assertThat(ip6prefix.getAddress(),
-                   equalTo(new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
-        assertThat(ip6prefix.getPrefixLen(), is((short) 128));
-
-        ip6prefix =
-            new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/64");
-        assertThat(ip6prefix.getAddress(),
-                   equalTo(new Ip6Address("1111:2222:3333:4444::")));
-        assertThat(ip6prefix.getPrefixLen(), is((short) 64));
+        value = new byte[] {0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77, (byte) 0x88, (byte) 0x88};
+        ipPrefix = Ip6Prefix.valueOf(value, -1);
     }
 
     /**
-     * Tests equality of {@link Ip6Address}.
+     * Tests invalid valueOf() converter for IPv6 byte array and
+     * too long prefix length.
      */
-    @Test
-    public void testEquality() {
-        Ip6Prefix addr1net = new Ip6Prefix("1100::/8");
-        Ip6Prefix addr2net = new Ip6Prefix("1100::/8");
-        assertThat(addr1net, is(addr2net));
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfByteArrayTooLongPrefixLengthIPv6() {
+        Ip6Prefix ipPrefix;
+        byte[] value;
 
-        addr1net = new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/8");
-        addr2net = new Ip6Prefix("1100::/8");
-        assertThat(addr1net, is(addr2net));
-
-        addr1net = new Ip6Prefix("::/0");
-        addr2net = new Ip6Prefix("::/0");
-        assertThat(addr1net, is(addr2net));
-
-        addr1net =
-            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
-        addr2net =
-            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
-        assertThat(addr1net, is(addr2net));
+        value = new byte[] {0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77, (byte) 0x88, (byte) 0x88};
+        ipPrefix = Ip6Prefix.valueOf(value, 129);
     }
 
     /**
-     * Tests non-equality of {@link Ip6Address}.
+     * Tests valueOf() converter for IPv6 address.
      */
     @Test
-    public void testNonEquality() {
-        Ip6Prefix addr1net = new Ip6Prefix("1100::/8");
-        Ip6Prefix addr2net = new Ip6Prefix("1200::/8");
-        Ip6Prefix addr3net = new Ip6Prefix("1200::/12");
-        Ip6Prefix addr4net = new Ip6Prefix("::/0");
-        Ip6Prefix addr5net =
-            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
-        assertThat(addr1net, is(not(addr2net)));
-        assertThat(addr3net, is(not(addr2net)));
-        assertThat(addr4net, is(not(addr2net)));
-        assertThat(addr5net, is(not(addr2net)));
+    public void testValueOfAddressIPv6() {
+        Ip6Address ipAddress;
+        Ip6Prefix ipPrefix;
+
+        ipAddress =
+            Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
+        ipPrefix = Ip6Prefix.valueOf(ipAddress, 120);
+        assertThat(ipPrefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8800/120"));
+
+        ipPrefix = Ip6Prefix.valueOf(ipAddress, 128);
+        assertThat(ipPrefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888/128"));
+
+        ipAddress = Ip6Address.valueOf("::");
+        ipPrefix = Ip6Prefix.valueOf(ipAddress, 0);
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix = Ip6Prefix.valueOf(ipAddress, 128);
+        assertThat(ipPrefix.toString(), is("::/128"));
+
+        ipAddress =
+            Ip6Address.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        ipPrefix = Ip6Prefix.valueOf(ipAddress, 0);
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix = Ip6Prefix.valueOf(ipAddress, 64);
+        assertThat(ipPrefix.toString(), is("ffff:ffff:ffff:ffff::/64"));
+
+        ipPrefix = Ip6Prefix.valueOf(ipAddress, 128);
+        assertThat(ipPrefix.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
     }
 
     /**
-     * Tests object string representation.
+     * Tests invalid valueOf() converter for a null IP address.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidValueOfNullAddress() {
+        Ip6Address ipAddress;
+        Ip6Prefix ipPrefix;
+
+        ipAddress = null;
+        ipPrefix = Ip6Prefix.valueOf(ipAddress, 24);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv6 address and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfAddressNegativePrefixLengthIPv6() {
+        Ip6Address ipAddress;
+        Ip6Prefix ipPrefix;
+
+        ipAddress =
+            Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
+        ipPrefix = Ip6Prefix.valueOf(ipAddress, -1);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv6 address and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfAddressTooLongPrefixLengthIPv6() {
+        Ip6Address ipAddress;
+        Ip6Prefix ipPrefix;
+
+        ipAddress =
+            Ip6Address.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
+        ipPrefix = Ip6Prefix.valueOf(ipAddress, 129);
+    }
+
+    /**
+     * Tests valueOf() converter for IPv6 string.
      */
     @Test
-    public void testToString() {
-        Ip6Prefix ip6prefix = new Ip6Prefix("1100::/8");
-        assertThat(ip6prefix.toString(), is("1100::/8"));
+    public void testValueOfStringIPv6() {
+        Ip6Prefix ipPrefix;
 
-        ip6prefix = new Ip6Prefix("1111:2222:3333:4444:5555:6666:7777:8885/8");
-        assertThat(ip6prefix.toString(), is("1100::/8"));
+        ipPrefix =
+            Ip6Prefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8888/120");
+        assertThat(ipPrefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8800/120"));
 
-        ip6prefix = new Ip6Prefix("::/0");
-        assertThat(ip6prefix.toString(), is("::/0"));
+        ipPrefix =
+            Ip6Prefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8888/128");
+        assertThat(ipPrefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888/128"));
 
-        ip6prefix =
-            new Ip6Prefix("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
-        assertThat(ip6prefix.toString(),
+        ipPrefix = Ip6Prefix.valueOf("::/0");
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix = Ip6Prefix.valueOf("::/128");
+        assertThat(ipPrefix.toString(), is("::/128"));
+
+        ipPrefix =
+            Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/0");
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix =
+            Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/64");
+        assertThat(ipPrefix.toString(), is("ffff:ffff:ffff:ffff::/64"));
+
+        ipPrefix =
+            Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertThat(ipPrefix.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a null string.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidValueOfNullString() {
+        Ip6Prefix ipPrefix;
+        String fromString;
+
+        fromString = null;
+        ipPrefix = Ip6Prefix.valueOf(fromString);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for an empty string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfEmptyString() {
+        Ip6Prefix ipPrefix;
+        String fromString;
+
+        fromString = "";
+        ipPrefix = Ip6Prefix.valueOf(fromString);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for an incorrect string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfIncorrectString() {
+        Ip6Prefix ipPrefix;
+        String fromString;
+
+        fromString = "NoSuchIpPrefix";
+        ipPrefix = Ip6Prefix.valueOf(fromString);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv6 string and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfStringNegativePrefixLengthIPv6() {
+        Ip6Prefix ipPrefix;
+
+        ipPrefix =
+            Ip6Prefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8888/-1");
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv6 string and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfStringTooLongPrefixLengthIPv6() {
+        Ip6Prefix ipPrefix;
+
+        ipPrefix =
+            Ip6Prefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8888/129");
+    }
+
+    /**
+     * Tests IP prefix contains another IP prefix for IPv6.
+     */
+    @Test
+    public void testContainsIpPrefixIPv6() {
+        Ip6Prefix ipPrefix;
+
+        ipPrefix = Ip6Prefix.valueOf("1111:2222:3333:4444::/120");
+        assertTrue(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/120")));
+        assertTrue(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/128")));
+        assertTrue(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::1/128")));
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/64")));
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4445::/120")));
+        assertFalse(ipPrefix.contains(Ip6Prefix.valueOf("::/64")));
+        assertFalse(ipPrefix.contains(Ip6Prefix.valueOf("::/0")));
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128")));
+
+        ipPrefix = Ip6Prefix.valueOf("1111:2222:3333:4444::/128");
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/120")));
+        assertTrue(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/128")));
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::1/128")));
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/64")));
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4445::/120")));
+        assertFalse(ipPrefix.contains(Ip6Prefix.valueOf("::/64")));
+        assertFalse(ipPrefix.contains(Ip6Prefix.valueOf("::/0")));
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128")));
+
+        ipPrefix = Ip6Prefix.valueOf("::/0");
+        assertTrue(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/120")));
+        assertTrue(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/128")));
+        assertTrue(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::1/128")));
+        assertTrue(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/64")));
+        assertTrue(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4445::/120")));
+        assertTrue(ipPrefix.contains(Ip6Prefix.valueOf("::/64")));
+        assertTrue(ipPrefix.contains(Ip6Prefix.valueOf("::/0")));
+        assertTrue(ipPrefix.contains(
+                Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128")));
+
+        ipPrefix =
+            Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/120")));
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/128")));
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::1/128")));
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/64")));
+        assertFalse(ipPrefix.contains(
+                Ip6Prefix.valueOf("1111:2222:3333:4445::/120")));
+        assertFalse(ipPrefix.contains(Ip6Prefix.valueOf("::/64")));
+        assertFalse(ipPrefix.contains(Ip6Prefix.valueOf("::/0")));
+        assertTrue(ipPrefix.contains(
+                Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128")));
+    }
+
+    /**
+     * Tests IP prefix contains IP address for IPv6.
+     */
+    @Test
+    public void testContainsIpAddressIPv6() {
+        Ip6Prefix ipPrefix;
+
+        ipPrefix = Ip6Prefix.valueOf("1111:2222:3333:4444::/120");
+        assertTrue(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4444::")));
+        assertTrue(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4444::1")));
+        assertFalse(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4445::")));
+        assertFalse(ipPrefix.contains(Ip6Address.valueOf("::")));
+        assertFalse(ipPrefix.contains(
+                Ip6Address.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+
+        ipPrefix = Ip6Prefix.valueOf("1111:2222:3333:4444::/128");
+        assertTrue(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4444::")));
+        assertFalse(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4444::1")));
+        assertFalse(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4445::")));
+        assertFalse(ipPrefix.contains(Ip6Address.valueOf("::")));
+        assertFalse(ipPrefix.contains(
+                Ip6Address.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+
+        ipPrefix = Ip6Prefix.valueOf("::/0");
+        assertTrue(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4444::")));
+        assertTrue(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4444::1")));
+        assertTrue(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4445::")));
+        assertTrue(ipPrefix.contains(Ip6Address.valueOf("::")));
+        assertTrue(ipPrefix.contains(
+                Ip6Address.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+
+        ipPrefix =
+            Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertFalse(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4444::")));
+        assertFalse(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4444::1")));
+        assertFalse(ipPrefix.contains(
+                Ip6Address.valueOf("1111:2222:3333:4445::")));
+        assertFalse(ipPrefix.contains(Ip6Address.valueOf("::")));
+        assertTrue(ipPrefix.contains(
+                Ip6Address.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+    }
+
+    /**
+     * Tests equality of {@link Ip6Prefix} for IPv6.
+     */
+    @Test
+    public void testEqualityIPv6() {
+        new EqualsTester()
+            .addEqualityGroup(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/120"),
+                Ip6Prefix.valueOf("1111:2222:3333:4444::1/120"),
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/120"))
+            .addEqualityGroup(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/64"),
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/64"))
+            .addEqualityGroup(
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/128"),
+                Ip6Prefix.valueOf("1111:2222:3333:4444::/128"))
+            .addEqualityGroup(
+                Ip6Prefix.valueOf("1111:2222:3333:4445::/64"),
+                Ip6Prefix.valueOf("1111:2222:3333:4445::/64"))
+            .addEqualityGroup(
+                Ip6Prefix.valueOf("::/0"),
+                Ip6Prefix.valueOf("::/0"))
+            .addEqualityGroup(
+                Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"),
+                Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"))
+            .testEquals();
+    }
+
+    /**
+     * Tests object string representation for IPv6.
+     */
+    @Test
+    public void testToStringIPv6() {
+        Ip6Prefix ipPrefix;
+
+        ipPrefix = Ip6Prefix.valueOf("1100::/8");
+        assertThat(ipPrefix.toString(), is("1100::/8"));
+
+        ipPrefix = Ip6Prefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8885/8");
+        assertThat(ipPrefix.toString(), is("1100::/8"));
+
+        ipPrefix = Ip6Prefix.valueOf("::/0");
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix = Ip6Prefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertThat(ipPrefix.toString(),
                    is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
     }
 }
diff --git a/utils/misc/src/test/java/org/onlab/packet/IpAddressTest.java b/utils/misc/src/test/java/org/onlab/packet/IpAddressTest.java
index 4237829..ad3ca4b 100644
--- a/utils/misc/src/test/java/org/onlab/packet/IpAddressTest.java
+++ b/utils/misc/src/test/java/org/onlab/packet/IpAddressTest.java
@@ -17,11 +17,13 @@
 
 import com.google.common.net.InetAddresses;
 import com.google.common.testing.EqualsTester;
+import org.junit.Ignore;
 import org.junit.Test;
 
 import java.net.InetAddress;
 
 import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
@@ -33,6 +35,7 @@
     /**
      * Tests the immutability of {@link IpAddress}.
      */
+    @Ignore("The class is not pure immutable, because it is not 'final'")
     @Test
     public void testImmutable() {
         assertThatClassIsImmutable(IpAddress.class);
@@ -75,24 +78,63 @@
     }
 
     /**
+     * Tests getting the Ip4Address and Ip6Address view of the IP address.
+     */
+    @Test
+    public void testGetIp4AndIp6AddressView() {
+        IpAddress ipAddress;
+        Ip4Address ip4Address;
+        Ip6Address ip6Address;
+
+        // Pure IPv4 IpAddress
+        ipAddress = IpAddress.valueOf("1.2.3.4");
+        ip4Address = ipAddress.getIp4Address();
+        ip6Address = ipAddress.getIp6Address();
+        assertThat(ip4Address.toString(), is("1.2.3.4"));
+        assertNull(ip6Address);
+
+        // IPv4 IpAddress that is Ip4Address
+        ipAddress = Ip4Address.valueOf("1.2.3.4");
+        ip4Address = ipAddress.getIp4Address();
+        ip6Address = ipAddress.getIp6Address();
+        assertThat(ip4Address.toString(), is("1.2.3.4"));
+        assertNull(ip6Address);
+
+        // Pure IPv6 IpAddress
+        ipAddress = IpAddress.valueOf("1111:2222::");
+        ip4Address = ipAddress.getIp4Address();
+        ip6Address = ipAddress.getIp6Address();
+        assertNull(ip4Address);
+        assertThat(ip6Address.toString(), is("1111:2222::"));
+
+        // IPv6 IpAddress that is Ip6Address
+        ipAddress = Ip6Address.valueOf("1111:2222::");
+        ip4Address = ipAddress.getIp4Address();
+        ip6Address = ipAddress.getIp6Address();
+        assertNull(ip4Address);
+        assertThat(ip6Address.toString(), is("1111:2222::"));
+    }
+
+    /**
      * Tests returning an IPv4 address as a byte array.
      */
     @Test
     public void testAddressToOctetsIPv4() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] value1 = new byte[] {1, 2, 3, 4};
+        value = new byte[] {1, 2, 3, 4};
         ipAddress = IpAddress.valueOf("1.2.3.4");
-        assertThat(ipAddress.toOctets(), is(value1));
+        assertThat(ipAddress.toOctets(), is(value));
 
-        final byte[] value2 = new byte[] {0, 0, 0, 0};
+        value = new byte[] {0, 0, 0, 0};
         ipAddress = IpAddress.valueOf("0.0.0.0");
-        assertThat(ipAddress.toOctets(), is(value2));
+        assertThat(ipAddress.toOctets(), is(value));
 
-        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff};
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
         ipAddress = IpAddress.valueOf("255.255.255.255");
-        assertThat(ipAddress.toOctets(), is(value3));
+        assertThat(ipAddress.toOctets(), is(value));
     }
 
     /**
@@ -101,41 +143,42 @@
     @Test
     public void testAddressToOctetsIPv6() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] value1 = new byte[] {0x11, 0x11, 0x22, 0x22,
-                                          0x33, 0x33, 0x44, 0x44,
-                                          0x55, 0x55, 0x66, 0x66,
-                                          0x77, 0x77,
-                                          (byte) 0x88, (byte) 0x88};
+        value = new byte[] {0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77,
+                            (byte) 0x88, (byte) 0x88};
         ipAddress =
             IpAddress.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
-        assertThat(ipAddress.toOctets(), is(value1));
+        assertThat(ipAddress.toOctets(), is(value));
 
-        final byte[] value2 = new byte[] {0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00};
+        value = new byte[] {0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00};
         ipAddress = IpAddress.valueOf("::");
-        assertThat(ipAddress.toOctets(), is(value2));
+        assertThat(ipAddress.toOctets(), is(value));
 
-        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff};
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
         ipAddress =
             IpAddress.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
-        assertThat(ipAddress.toOctets(), is(value3));
+        assertThat(ipAddress.toOctets(), is(value));
     }
 
     /**
-     * Tests returning an IPv4 address asn an integer.
+     * Tests returning an IPv4 address as an integer.
      */
     @Test
-    public void testToint() {
+    public void testToInt() {
         IpAddress ipAddress;
 
         ipAddress = IpAddress.valueOf("1.2.3.4");
@@ -149,10 +192,10 @@
     }
 
     /**
-     * Tests valueOf() converter for an integer value.
+     * Tests valueOf() converter for IPv4 integer value.
      */
     @Test
-    public void testValueOfForInteger() {
+    public void testValueOfForIntegerIPv4() {
         IpAddress ipAddress;
 
         ipAddress = IpAddress.valueOf(0x01020304);
@@ -171,18 +214,19 @@
     @Test
     public void testValueOfByteArrayIPv4() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] value1 = new byte[] {1, 2, 3, 4};
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value1);
+        value = new byte[] {1, 2, 3, 4};
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value);
         assertThat(ipAddress.toString(), is("1.2.3.4"));
 
-        final byte[] value2 = new byte[] {0, 0, 0, 0};
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value2);
+        value = new byte[] {0, 0, 0, 0};
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value);
         assertThat(ipAddress.toString(), is("0.0.0.0"));
 
-        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff};
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value3);
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value);
         assertThat(ipAddress.toString(), is("255.255.255.255"));
     }
 
@@ -192,32 +236,33 @@
     @Test
     public void testValueOfByteArrayIPv6() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] value1 = new byte[] {0x11, 0x11, 0x22, 0x22,
-                                          0x33, 0x33, 0x44, 0x44,
-                                          0x55, 0x55, 0x66, 0x66,
-                                          0x77, 0x77,
-                                          (byte) 0x88, (byte) 0x88};
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value1);
+        value = new byte[] {0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77,
+                            (byte) 0x88, (byte) 0x88};
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value);
         assertThat(ipAddress.toString(),
                    is("1111:2222:3333:4444:5555:6666:7777:8888"));
 
-        final byte[] value2 = new byte[] {0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00};
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value2);
+        value = new byte[] {0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00};
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value);
         assertThat(ipAddress.toString(), is("::"));
 
-        final byte[] value3 = new byte[] {(byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff};
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value3);
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value);
         assertThat(ipAddress.toString(),
                    is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
     }
@@ -228,9 +273,10 @@
     @Test(expected = NullPointerException.class)
     public void testInvalidValueOfNullArrayIPv4() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] fromArray = null;
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, fromArray);
+        value = null;
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value);
     }
 
     /**
@@ -239,9 +285,10 @@
     @Test(expected = NullPointerException.class)
     public void testInvalidValueOfNullArrayIPv6() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] fromArray = null;
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, fromArray);
+        value = null;
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value);
     }
 
     /**
@@ -251,9 +298,10 @@
     @Test(expected = IllegalArgumentException.class)
     public void testInvalidValueOfShortArrayIPv4() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] fromArray = new byte[] {1, 2, 3};
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, fromArray);
+        value = new byte[] {1, 2, 3};
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value);
     }
 
     /**
@@ -263,9 +311,10 @@
     @Test(expected = IllegalArgumentException.class)
     public void testInvalidValueOfShortArrayIPv6() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] fromArray = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, fromArray);
+        value = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value);
     }
 
     /**
@@ -274,24 +323,25 @@
     @Test
     public void testValueOfByteArrayOffsetIPv4() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] value1 = new byte[] {11, 22, 33,   // Preamble
-                                          1, 2, 3, 4,
-                                          44, 55};      // Extra bytes
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value1, 3);
+        value = new byte[] {11, 22, 33,                 // Preamble
+                            1, 2, 3, 4,
+                            44, 55};                    // Extra bytes
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value, 3);
         assertThat(ipAddress.toString(), is("1.2.3.4"));
 
-        final byte[] value2 = new byte[] {11, 22,       // Preamble
-                                          0, 0, 0, 0,
-                                          33};          // Extra bytes
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value2, 2);
+        value = new byte[] {11, 22,                     // Preamble
+                            0, 0, 0, 0,
+                            33};                        // Extra bytes
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value, 2);
         assertThat(ipAddress.toString(), is("0.0.0.0"));
 
-        final byte[] value3 = new byte[] {11, 22,       // Preamble
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          33};          // Extra bytes
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value3, 2);
+        value = new byte[] {11, 22,                     // Preamble
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            33};                        // Extra bytes
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value, 2);
         assertThat(ipAddress.toString(), is("255.255.255.255"));
     }
 
@@ -301,38 +351,39 @@
     @Test
     public void testValueOfByteArrayOffsetIPv6() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] value1 = new byte[] {11, 22, 33,           // Preamble
-                                          0x11, 0x11, 0x22, 0x22,
-                                          0x33, 0x33, 0x44, 0x44,
-                                          0x55, 0x55, 0x66, 0x66,
-                                          0x77, 0x77,
-                                          (byte) 0x88, (byte) 0x88,
-                                          44, 55};              // Extra bytes
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value1, 3);
+        value = new byte[] {11, 22, 33,                 // Preamble
+                            0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77,
+                            (byte) 0x88, (byte) 0x88,
+                            44, 55};                    // Extra bytes
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value, 3);
         assertThat(ipAddress.toString(),
                    is("1111:2222:3333:4444:5555:6666:7777:8888"));
 
-        final byte[] value2 = new byte[] {11, 22,               // Preamble
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          0x00, 0x00, 0x00, 0x00,
-                                          33};                  // Extra bytes
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value2, 2);
+        value = new byte[] {11, 22,                     // Preamble
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            33};                        // Extra bytes
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value, 2);
         assertThat(ipAddress.toString(), is("::"));
 
-        final byte[] value3 = new byte[] {11, 22,               // Preamble
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          (byte) 0xff, (byte) 0xff,
-                                          33};                  // Extra bytes
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value3, 2);
+        value = new byte[] {11, 22,                     // Preamble
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            33};                        // Extra bytes
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value, 2);
         assertThat(ipAddress.toString(),
                    is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
     }
@@ -344,11 +395,12 @@
     @Test(expected = IllegalArgumentException.class)
     public void testInvalidValueOfArrayInvalidOffsetIPv4() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] value1 = new byte[] {11, 22, 33,   // Preamble
-                                          1, 2, 3, 4,
-                                          44, 55};      // Extra bytes
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value1, 6);
+        value = new byte[] {11, 22, 33,                 // Preamble
+                            1, 2, 3, 4,
+                            44, 55};                    // Extra bytes
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET, value, 6);
     }
 
     /**
@@ -358,15 +410,16 @@
     @Test(expected = IllegalArgumentException.class)
     public void testInvalidValueOfArrayInvalidOffsetIPv6() {
         IpAddress ipAddress;
+        byte[] value;
 
-        final byte[] value1 = new byte[] {11, 22, 33,           // Preamble
-                                          0x11, 0x11, 0x22, 0x22,
-                                          0x33, 0x33, 0x44, 0x44,
-                                          0x55, 0x55, 0x66, 0x66,
-                                          0x77, 0x77,
-                                          (byte) 0x88, (byte) 0x88,
-                                          44, 55};              // Extra bytes
-        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value1, 6);
+        value = new byte[] {11, 22, 33,                 // Preamble
+                            0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77,
+                            (byte) 0x88, (byte) 0x88,
+                            44, 55};                    // Extra bytes
+        ipAddress = IpAddress.valueOf(IpAddress.Version.INET6, value, 6);
     }
 
     /**
diff --git a/utils/misc/src/test/java/org/onlab/packet/IpPrefixTest.java b/utils/misc/src/test/java/org/onlab/packet/IpPrefixTest.java
index f5ad88b..91a4d49 100644
--- a/utils/misc/src/test/java/org/onlab/packet/IpPrefixTest.java
+++ b/utils/misc/src/test/java/org/onlab/packet/IpPrefixTest.java
@@ -15,123 +15,1011 @@
  */
 package org.onlab.packet;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import java.util.Arrays;
-
-import org.junit.Test;
-import org.onlab.packet.IpAddress.Version;
-
 import com.google.common.testing.EqualsTester;
+import org.junit.Ignore;
+import org.junit.Test;
 
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+
+/**
+ * Tests for class {@link IpPrefix}.
+ */
 public class IpPrefixTest {
-
-    private static final byte [] BYTES1 = new byte [] {0xa, 0x0, 0x0, 0xa};
-    private static final byte [] BYTES2 = new byte [] {0xa, 0x0, 0x0, 0xb};
-    private static final int INTVAL0 = 0x0a000000;
-    private static final int INTVAL1 = 0x0a00000a;
-    private static final int INTVAL2 = 0x0a00000b;
-    private static final String STRVAL = "10.0.0.12/16";
-    private static final int MASK_LENGTH = 16;
-
+    /**
+     * Tests the immutability of {@link IpPrefix}.
+     */
+    @Ignore("The class is not pure immutable, because it is not 'final'")
     @Test
-    public void testEquality() {
-        IpPrefix ip1 = IpPrefix.valueOf(IpAddress.Version.INET,
-                                        BYTES1, IpPrefix.MAX_INET_MASK_LENGTH);
-        IpPrefix ip2 = IpPrefix.valueOf(INTVAL1, IpPrefix.MAX_INET_MASK_LENGTH);
-        IpPrefix ip3 = IpPrefix.valueOf(IpAddress.Version.INET,
-                                        BYTES2, IpPrefix.MAX_INET_MASK_LENGTH);
-        IpPrefix ip4 = IpPrefix.valueOf(INTVAL2, IpPrefix.MAX_INET_MASK_LENGTH);
-        IpPrefix ip5 = IpPrefix.valueOf(STRVAL);
-
-        new EqualsTester().addEqualityGroup(ip1, ip2)
-        .addEqualityGroup(ip3, ip4)
-        .addEqualityGroup(ip5)
-        .testEquals();
-
-        // string conversions
-        IpPrefix ip6 = IpPrefix.valueOf(IpAddress.Version.INET,
-                                        BYTES1, MASK_LENGTH);
-        IpPrefix ip7 = IpPrefix.valueOf("10.0.0.10/16");
-        IpPrefix ip8 = IpPrefix.valueOf(IpAddress.Version.INET,
-                                        new byte [] {0xa, 0x0, 0x0, 0xc}, 16);
-        assertEquals("incorrect address conversion", ip6, ip7);
-        assertEquals("incorrect address conversion", ip5, ip8);
+    public void testImmutable() {
+        assertThatClassIsImmutable(IpPrefix.class);
     }
 
+    /**
+     * Tests the maximum mask length.
+     */
     @Test
-    public void basics() {
-        IpPrefix ip1 = IpPrefix.valueOf(IpAddress.Version.INET,
-                                        BYTES1, MASK_LENGTH);
-        final byte [] bytes = new byte [] {0xa, 0x0, 0x0, 0x0};
-
-        // check fields
-        assertEquals("incorrect IP Version", Version.INET, ip1.version());
-        assertEquals("incorrect netmask", 16, ip1.prefixLength());
-        assertTrue("faulty toOctets()",
-                   Arrays.equals(bytes, ip1.address().toOctets()));
-        assertEquals("faulty toInt()", INTVAL0, ip1.address().toInt());
-        assertEquals("faulty toString()", "10.0.0.0/16", ip1.toString());
+    public void testMaxMaskLength() {
+        assertThat(IpPrefix.MAX_INET_MASK_LENGTH, is(32));
+        assertThat(IpPrefix.MAX_INET6_MASK_LENGTH, is(128));
     }
 
+    /**
+     * Tests returning the IP version of the prefix.
+     */
     @Test
-    public void netmasks() {
-        // masked
-        IpPrefix ip1 = IpPrefix.valueOf(IpAddress.Version.INET,
-                                        BYTES1, MASK_LENGTH);
-        IpPrefix ip2 = IpPrefix.valueOf("10.0.0.10/16");
-        IpPrefix ip3 = IpPrefix.valueOf("10.0.0.0/16");
-        assertEquals("incorrect binary masked address",
-                     ip1.toString(), "10.0.0.0/16");
-        assertEquals("incorrect string masked address",
-                     ip2.toString(), "10.0.0.0/16");
-        assertEquals("incorrect network address",
-                     ip2.toString(), "10.0.0.0/16");
+    public void testVersion() {
+        IpPrefix ipPrefix;
+
+        // IPv4
+        ipPrefix = IpPrefix.valueOf("0.0.0.0/0");
+        assertThat(ipPrefix.version(), is(IpAddress.Version.INET));
+
+        // IPv6
+        ipPrefix = IpPrefix.valueOf("::/0");
+        assertThat(ipPrefix.version(), is(IpAddress.Version.INET6));
     }
 
+    /**
+     * Tests returning the IP address value and IP address prefix length of
+     * an IPv4 prefix.
+     */
     @Test
-    public void testContainsIpPrefix() {
-        IpPrefix slash31 = IpPrefix.valueOf(IpAddress.Version.INET,
-                                            BYTES1, 31);
-        IpPrefix slash32 = IpPrefix.valueOf(IpAddress.Version.INET,
-                                            BYTES1, 32);
-        IpPrefix differentSlash32 = IpPrefix.valueOf(IpAddress.Version.INET,
-                                                     BYTES2, 32);
+    public void testAddressAndPrefixLengthIPv4() {
+        IpPrefix ipPrefix;
 
-        assertTrue(slash31.contains(differentSlash32));
-        assertFalse(differentSlash32.contains(slash31));
+        ipPrefix = IpPrefix.valueOf("1.2.3.0/24");
+        assertThat(ipPrefix.address(), equalTo(IpAddress.valueOf("1.2.3.0")));
+        assertThat(ipPrefix.prefixLength(), is(24));
 
-        assertTrue(slash31.contains(slash32));
-        assertFalse(slash32.contains(differentSlash32));
-        assertFalse(differentSlash32.contains(slash32));
+        ipPrefix = IpPrefix.valueOf("1.2.3.4/24");
+        assertThat(ipPrefix.address(), equalTo(IpAddress.valueOf("1.2.3.0")));
+        assertThat(ipPrefix.prefixLength(), is(24));
 
-        IpPrefix zero = IpPrefix.valueOf("0.0.0.0/0");
-        assertTrue(zero.contains(differentSlash32));
-        assertFalse(differentSlash32.contains(zero));
+        ipPrefix = IpPrefix.valueOf("1.2.3.4/32");
+        assertThat(ipPrefix.address(), equalTo(IpAddress.valueOf("1.2.3.4")));
+        assertThat(ipPrefix.prefixLength(), is(32));
 
-        IpPrefix slash8 = IpPrefix.valueOf("10.0.0.0/8");
-        assertTrue(slash8.contains(slash31));
-        assertFalse(slash31.contains(slash8));
+        ipPrefix = IpPrefix.valueOf("1.2.3.5/32");
+        assertThat(ipPrefix.address(), equalTo(IpAddress.valueOf("1.2.3.5")));
+        assertThat(ipPrefix.prefixLength(), is(32));
+
+        ipPrefix = IpPrefix.valueOf("0.0.0.0/0");
+        assertThat(ipPrefix.address(), equalTo(IpAddress.valueOf("0.0.0.0")));
+        assertThat(ipPrefix.prefixLength(), is(0));
+
+        ipPrefix = IpPrefix.valueOf("255.255.255.255/32");
+        assertThat(ipPrefix.address(),
+                   equalTo(IpAddress.valueOf("255.255.255.255")));
+        assertThat(ipPrefix.prefixLength(), is(32));
     }
 
+    /**
+     * Tests returning the IP address value and IP address prefix length of
+     * an IPv6 prefix.
+     */
     @Test
-    public void testContainsIpAddress() {
-        IpPrefix slash31 = IpPrefix.valueOf(IpAddress.Version.INET,
-                                            BYTES1, 31);
-        IpAddress addr32 = IpAddress.valueOf(IpAddress.Version.INET, BYTES1);
+    public void testAddressAndPrefixLengthIPv6() {
+        IpPrefix ipPrefix;
 
-        assertTrue(slash31.contains(addr32));
+        ipPrefix = IpPrefix.valueOf("1100::/8");
+        assertThat(ipPrefix.address(), equalTo(IpAddress.valueOf("1100::")));
+        assertThat(ipPrefix.prefixLength(), is(8));
 
-        IpPrefix intf = IpPrefix.valueOf("192.168.10.101/24");
-        IpAddress addr = IpAddress.valueOf("192.168.10.1");
+        ipPrefix =
+            IpPrefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8885/8");
+        assertThat(ipPrefix.address(), equalTo(IpAddress.valueOf("1100::")));
+        assertThat(ipPrefix.prefixLength(), is(8));
 
-        assertTrue(intf.contains(addr));
+        ipPrefix =
+            IpPrefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8800/120");
+        assertThat(ipPrefix.address(),
+                   equalTo(IpAddress.valueOf("1111:2222:3333:4444:5555:6666:7777:8800")));
+        assertThat(ipPrefix.prefixLength(), is(120));
 
-        IpPrefix intf1 = IpPrefix.valueOf("10.0.0.101/24");
-        IpAddress addr1 = IpAddress.valueOf("10.0.0.4");
+        ipPrefix =
+            IpPrefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8885/128");
+        assertThat(ipPrefix.address(),
+                   equalTo(IpAddress.valueOf("1111:2222:3333:4444:5555:6666:7777:8885")));
+        assertThat(ipPrefix.prefixLength(), is(128));
 
-        assertTrue(intf1.contains(addr1));
+        ipPrefix = IpPrefix.valueOf("::/0");
+        assertThat(ipPrefix.address(), equalTo(IpAddress.valueOf("::")));
+        assertThat(ipPrefix.prefixLength(), is(0));
+
+        ipPrefix =
+            IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertThat(ipPrefix.address(),
+                   equalTo(IpAddress.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+        assertThat(ipPrefix.prefixLength(), is(128));
+
+        ipPrefix =
+            IpPrefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8885/64");
+        assertThat(ipPrefix.address(),
+                   equalTo(IpAddress.valueOf("1111:2222:3333:4444::")));
+        assertThat(ipPrefix.prefixLength(), is(64));
+    }
+
+    /**
+     * Tests getting the Ip4Prefix and Ip6Prefix view of the IP prefix.
+     */
+    @Test
+    public void testGetIp4AndIp6PrefixView() {
+        IpPrefix ipPrefix;
+        Ip4Prefix ip4Prefix;
+        Ip6Prefix ip6Prefix;
+
+        // Pure IPv4 IpPrefix
+        ipPrefix = IpPrefix.valueOf("1.2.3.0/24");
+        ip4Prefix = ipPrefix.getIp4Prefix();
+        ip6Prefix = ipPrefix.getIp6Prefix();
+        assertThat(ip4Prefix.toString(), is("1.2.3.0/24"));
+        assertNull(ip6Prefix);
+
+        // IPv4 IpPrefix that is Ip4Prefix
+        ipPrefix = Ip4Prefix.valueOf("1.2.3.0/24");
+        ip4Prefix = ipPrefix.getIp4Prefix();
+        ip6Prefix = ipPrefix.getIp6Prefix();
+        assertThat(ip4Prefix.toString(), is("1.2.3.0/24"));
+        assertNull(ip6Prefix);
+
+        // Pure IPv6 IpPrefix
+        ipPrefix = IpPrefix.valueOf("1111:2222::/64");
+        ip4Prefix = ipPrefix.getIp4Prefix();
+        ip6Prefix = ipPrefix.getIp6Prefix();
+        assertNull(ip4Prefix);
+        assertThat(ip6Prefix.toString(), is("1111:2222::/64"));
+
+        // IPv6 IpPrefix that is Ip6Prefix
+        ipPrefix = Ip6Prefix.valueOf("1111:2222::/64");
+        ip4Prefix = ipPrefix.getIp4Prefix();
+        ip6Prefix = ipPrefix.getIp6Prefix();
+        assertNull(ip4Prefix);
+        assertThat(ip6Prefix.toString(), is("1111:2222::/64"));
+    }
+
+    /**
+     * Tests valueOf() converter for IPv4 integer value.
+     */
+    @Test
+    public void testValueOfForIntegerIPv4() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf(0x01020304, 24);
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
+
+        ipPrefix = IpPrefix.valueOf(0x01020304, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.4/32"));
+
+        ipPrefix = IpPrefix.valueOf(0x01020305, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.5/32"));
+
+        ipPrefix = IpPrefix.valueOf(0, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = IpPrefix.valueOf(0, 32);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/32"));
+
+        ipPrefix = IpPrefix.valueOf(0xffffffff, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = IpPrefix.valueOf(0xffffffff, 16);
+        assertThat(ipPrefix.toString(), is("255.255.0.0/16"));
+
+        ipPrefix = IpPrefix.valueOf(0xffffffff, 32);
+        assertThat(ipPrefix.toString(), is("255.255.255.255/32"));
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 integer value and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfIntegerNegativePrefixLengthIPv4() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf(0x01020304, -1);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 integer value and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfIntegerTooLongPrefixLengthIPv4() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf(0x01020304, 33);
+    }
+
+    /**
+     * Tests valueOf() converter for IPv4 byte array.
+     */
+    @Test
+    public void testValueOfByteArrayIPv4() {
+        IpPrefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3, 4};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, 24);
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
+
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.4/32"));
+
+        value = new byte[] {1, 2, 3, 5};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.5/32"));
+
+        value = new byte[] {0, 0, 0, 0};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, 32);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/32"));
+
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, 16);
+        assertThat(ipPrefix.toString(), is("255.255.0.0/16"));
+
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, 32);
+        assertThat(ipPrefix.toString(), is("255.255.255.255/32"));
+    }
+
+    /**
+     * Tests valueOf() converter for IPv6 byte array.
+     */
+    @Test
+    public void testValueOfByteArrayIPv6() {
+        IpPrefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77, (byte) 0x88, (byte) 0x88};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET6, value, 120);
+        assertThat(ipPrefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8800/120"));
+
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET6, value, 128);
+        assertThat(ipPrefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888/128"));
+
+        value = new byte[] {0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00,
+                            0x00, 0x00, 0x00, 0x00};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET6, value, 0);
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET6, value, 128);
+        assertThat(ipPrefix.toString(), is("::/128"));
+
+        value = new byte[] {(byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff,
+                            (byte) 0xff, (byte) 0xff};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET6, value, 0);
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET6, value, 64);
+        assertThat(ipPrefix.toString(), is("ffff:ffff:ffff:ffff::/64"));
+
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET6, value, 128);
+        assertThat(ipPrefix.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a null array for IPv4.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidValueOfNullArrayIPv4() {
+        IpPrefix ipPrefix;
+        byte[] value;
+
+        value = null;
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, 24);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a null array for IPv6.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidValueOfNullArrayIPv6() {
+        IpPrefix ipPrefix;
+        byte[] value;
+
+        value = null;
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET6, value, 120);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a short array for IPv4.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfShortArrayIPv4() {
+        IpPrefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, 24);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a short array for IPv6.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfShortArrayIPv6() {
+        IpPrefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET6, value, 120);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 byte array and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfByteArrayNegativePrefixLengthIPv4() {
+        IpPrefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3, 4};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, -1);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv6 byte array and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfByteArrayNegativePrefixLengthIPv6() {
+        IpPrefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77, (byte) 0x88, (byte) 0x88};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET6, value, -1);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 byte array and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfByteArrayTooLongPrefixLengthIPv4() {
+        IpPrefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {1, 2, 3, 4};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET, value, 33);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv6 byte array and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfByteArrayTooLongPrefixLengthIPv6() {
+        IpPrefix ipPrefix;
+        byte[] value;
+
+        value = new byte[] {0x11, 0x11, 0x22, 0x22,
+                            0x33, 0x33, 0x44, 0x44,
+                            0x55, 0x55, 0x66, 0x66,
+                            0x77, 0x77, (byte) 0x88, (byte) 0x88};
+        ipPrefix = IpPrefix.valueOf(IpAddress.Version.INET6, value, 129);
+    }
+
+    /**
+     * Tests valueOf() converter for IPv4 address.
+     */
+    @Test
+    public void testValueOfAddressIPv4() {
+        IpAddress ipAddress;
+        IpPrefix ipPrefix;
+
+        ipAddress = IpAddress.valueOf("1.2.3.4");
+        ipPrefix = IpPrefix.valueOf(ipAddress, 24);
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
+
+        ipPrefix = IpPrefix.valueOf(ipAddress, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.4/32"));
+
+        ipAddress = IpAddress.valueOf("1.2.3.5");
+        ipPrefix = IpPrefix.valueOf(ipAddress, 32);
+        assertThat(ipPrefix.toString(), is("1.2.3.5/32"));
+
+        ipAddress = IpAddress.valueOf("0.0.0.0");
+        ipPrefix = IpPrefix.valueOf(ipAddress, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = IpPrefix.valueOf(ipAddress, 32);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/32"));
+
+        ipAddress = IpAddress.valueOf("255.255.255.255");
+        ipPrefix = IpPrefix.valueOf(ipAddress, 0);
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = IpPrefix.valueOf(ipAddress, 16);
+        assertThat(ipPrefix.toString(), is("255.255.0.0/16"));
+
+        ipPrefix = IpPrefix.valueOf(ipAddress, 32);
+        assertThat(ipPrefix.toString(), is("255.255.255.255/32"));
+    }
+
+    /**
+     * Tests valueOf() converter for IPv6 address.
+     */
+    @Test
+    public void testValueOfAddressIPv6() {
+        IpAddress ipAddress;
+        IpPrefix ipPrefix;
+
+        ipAddress =
+            IpAddress.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
+        ipPrefix = IpPrefix.valueOf(ipAddress, 120);
+        assertThat(ipPrefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8800/120"));
+
+        ipPrefix = IpPrefix.valueOf(ipAddress, 128);
+        assertThat(ipPrefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888/128"));
+
+        ipAddress = IpAddress.valueOf("::");
+        ipPrefix = IpPrefix.valueOf(ipAddress, 0);
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix = IpPrefix.valueOf(ipAddress, 128);
+        assertThat(ipPrefix.toString(), is("::/128"));
+
+        ipAddress =
+            IpAddress.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        ipPrefix = IpPrefix.valueOf(ipAddress, 0);
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix = IpPrefix.valueOf(ipAddress, 64);
+        assertThat(ipPrefix.toString(), is("ffff:ffff:ffff:ffff::/64"));
+
+        ipPrefix = IpPrefix.valueOf(ipAddress, 128);
+        assertThat(ipPrefix.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a null IP address.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidValueOfNullAddress() {
+        IpAddress ipAddress;
+        IpPrefix ipPrefix;
+
+        ipAddress = null;
+        ipPrefix = IpPrefix.valueOf(ipAddress, 24);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 address and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfAddressNegativePrefixLengthIPv4() {
+        IpAddress ipAddress;
+        IpPrefix ipPrefix;
+
+        ipAddress = IpAddress.valueOf("1.2.3.4");
+        ipPrefix = IpPrefix.valueOf(ipAddress, -1);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv6 address and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfAddressNegativePrefixLengthIPv6() {
+        IpAddress ipAddress;
+        IpPrefix ipPrefix;
+
+        ipAddress =
+            IpAddress.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
+        ipPrefix = IpPrefix.valueOf(ipAddress, -1);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 address and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfAddressTooLongPrefixLengthIPv4() {
+        IpAddress ipAddress;
+        IpPrefix ipPrefix;
+
+        ipAddress = IpAddress.valueOf("1.2.3.4");
+        ipPrefix = IpPrefix.valueOf(ipAddress, 33);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv6 address and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfAddressTooLongPrefixLengthIPv6() {
+        IpAddress ipAddress;
+        IpPrefix ipPrefix;
+
+        ipAddress =
+            IpAddress.valueOf("1111:2222:3333:4444:5555:6666:7777:8888");
+        ipPrefix = IpPrefix.valueOf(ipAddress, 129);
+    }
+
+    /**
+     * Tests valueOf() converter for IPv4 string.
+     */
+    @Test
+    public void testValueOfStringIPv4() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf("1.2.3.4/24");
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
+
+        ipPrefix = IpPrefix.valueOf("1.2.3.4/32");
+        assertThat(ipPrefix.toString(), is("1.2.3.4/32"));
+
+        ipPrefix = IpPrefix.valueOf("1.2.3.5/32");
+        assertThat(ipPrefix.toString(), is("1.2.3.5/32"));
+
+        ipPrefix = IpPrefix.valueOf("0.0.0.0/0");
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = IpPrefix.valueOf("0.0.0.0/32");
+        assertThat(ipPrefix.toString(), is("0.0.0.0/32"));
+
+        ipPrefix = IpPrefix.valueOf("255.255.255.255/0");
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = IpPrefix.valueOf("255.255.255.255/16");
+        assertThat(ipPrefix.toString(), is("255.255.0.0/16"));
+
+        ipPrefix = IpPrefix.valueOf("255.255.255.255/32");
+        assertThat(ipPrefix.toString(), is("255.255.255.255/32"));
+    }
+
+    /**
+     * Tests valueOf() converter for IPv6 string.
+     */
+    @Test
+    public void testValueOfStringIPv6() {
+        IpPrefix ipPrefix;
+
+        ipPrefix =
+            IpPrefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8888/120");
+        assertThat(ipPrefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8800/120"));
+
+        ipPrefix =
+            IpPrefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8888/128");
+        assertThat(ipPrefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888/128"));
+
+        ipPrefix = IpPrefix.valueOf("::/0");
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix = IpPrefix.valueOf("::/128");
+        assertThat(ipPrefix.toString(), is("::/128"));
+
+        ipPrefix =
+            IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/0");
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix =
+            IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/64");
+        assertThat(ipPrefix.toString(), is("ffff:ffff:ffff:ffff::/64"));
+
+        ipPrefix =
+            IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertThat(ipPrefix.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
+    }
+
+    /**
+     * Tests invalid valueOf() converter for a null string.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidValueOfNullString() {
+        IpPrefix ipPrefix;
+        String fromString;
+
+        fromString = null;
+        ipPrefix = IpPrefix.valueOf(fromString);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for an empty string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfEmptyString() {
+        IpPrefix ipPrefix;
+        String fromString;
+
+        fromString = "";
+        ipPrefix = IpPrefix.valueOf(fromString);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for an incorrect string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfIncorrectString() {
+        IpPrefix ipPrefix;
+        String fromString;
+
+        fromString = "NoSuchIpPrefix";
+        ipPrefix = IpPrefix.valueOf(fromString);
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 string and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfStringNegativePrefixLengthIPv4() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf("1.2.3.4/-1");
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv6 string and
+     * negative prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfStringNegativePrefixLengthIPv6() {
+        IpPrefix ipPrefix;
+
+        ipPrefix =
+            IpPrefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8888/-1");
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv4 string and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfStringTooLongPrefixLengthIPv4() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf("1.2.3.4/33");
+    }
+
+    /**
+     * Tests invalid valueOf() converter for IPv6 string and
+     * too long prefix length.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidValueOfStringTooLongPrefixLengthIPv6() {
+        IpPrefix ipPrefix;
+
+        ipPrefix =
+            IpPrefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8888/129");
+    }
+
+    /**
+     * Tests IP prefix contains another IP prefix for IPv4.
+     */
+    @Test
+    public void testContainsIpPrefixIPv4() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf("1.2.0.0/24");
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/24")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/32")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("1.2.0.4/32")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/16")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("1.3.0.0/24")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("0.0.0.0/16")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("0.0.0.0/0")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("255.255.255.255/32")));
+
+        ipPrefix = IpPrefix.valueOf("1.2.0.0/32");
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/24")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/32")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("1.2.0.4/32")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/16")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("1.3.0.0/24")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("0.0.0.0/16")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("0.0.0.0/0")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("255.255.255.255/32")));
+
+        ipPrefix = IpPrefix.valueOf("0.0.0.0/0");
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/24")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/32")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("1.2.0.4/32")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/16")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("1.3.0.0/24")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("0.0.0.0/16")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("0.0.0.0/0")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("255.255.255.255/32")));
+
+        ipPrefix = IpPrefix.valueOf("255.255.255.255/32");
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/24")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/32")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("1.2.0.4/32")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("1.2.0.0/16")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("1.3.0.0/24")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("0.0.0.0/16")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("0.0.0.0/0")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("255.255.255.255/32")));
+    }
+
+    /**
+     * Tests IP prefix contains another IP prefix for IPv6.
+     */
+    @Test
+    public void testContainsIpPrefixIPv6() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf("1111:2222:3333:4444::/120");
+        assertTrue(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/120")));
+        assertTrue(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/128")));
+        assertTrue(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::1/128")));
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/64")));
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4445::/120")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("::/64")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("::/0")));
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128")));
+
+        ipPrefix = IpPrefix.valueOf("1111:2222:3333:4444::/128");
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/120")));
+        assertTrue(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/128")));
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::1/128")));
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/64")));
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4445::/120")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("::/64")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("::/0")));
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128")));
+
+        ipPrefix = IpPrefix.valueOf("::/0");
+        assertTrue(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/120")));
+        assertTrue(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/128")));
+        assertTrue(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::1/128")));
+        assertTrue(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/64")));
+        assertTrue(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4445::/120")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("::/64")));
+        assertTrue(ipPrefix.contains(IpPrefix.valueOf("::/0")));
+        assertTrue(ipPrefix.contains(
+                IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128")));
+
+        ipPrefix =
+            IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/120")));
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/128")));
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::1/128")));
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4444::/64")));
+        assertFalse(ipPrefix.contains(
+                IpPrefix.valueOf("1111:2222:3333:4445::/120")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("::/64")));
+        assertFalse(ipPrefix.contains(IpPrefix.valueOf("::/0")));
+        assertTrue(ipPrefix.contains(
+                IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128")));
+    }
+
+    /**
+     * Tests IP prefix contains IP address for IPv4.
+     */
+    @Test
+    public void testContainsIpAddressIPv4() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf("1.2.0.0/24");
+        assertTrue(ipPrefix.contains(IpAddress.valueOf("1.2.0.0")));
+        assertTrue(ipPrefix.contains(IpAddress.valueOf("1.2.0.4")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("1.3.0.0")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("0.0.0.0")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("255.255.255.255")));
+
+        ipPrefix = IpPrefix.valueOf("1.2.0.0/32");
+        assertTrue(ipPrefix.contains(IpAddress.valueOf("1.2.0.0")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("1.2.0.4")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("1.3.0.0")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("0.0.0.0")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("255.255.255.255")));
+
+        ipPrefix = IpPrefix.valueOf("0.0.0.0/0");
+        assertTrue(ipPrefix.contains(IpAddress.valueOf("1.2.0.0")));
+        assertTrue(ipPrefix.contains(IpAddress.valueOf("1.2.0.4")));
+        assertTrue(ipPrefix.contains(IpAddress.valueOf("1.3.0.0")));
+        assertTrue(ipPrefix.contains(IpAddress.valueOf("0.0.0.0")));
+        assertTrue(ipPrefix.contains(IpAddress.valueOf("255.255.255.255")));
+
+        ipPrefix = IpPrefix.valueOf("255.255.255.255/32");
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("1.2.0.0")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("1.2.0.4")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("1.3.0.0")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("0.0.0.0")));
+        assertTrue(ipPrefix.contains(IpAddress.valueOf("255.255.255.255")));
+    }
+
+    /**
+     * Tests IP prefix contains IP address for IPv6.
+     */
+    @Test
+    public void testContainsIpAddressIPv6() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf("1111:2222:3333:4444::/120");
+        assertTrue(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4444::")));
+        assertTrue(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4444::1")));
+        assertFalse(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4445::")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("::")));
+        assertFalse(ipPrefix.contains(
+                IpAddress.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+
+        ipPrefix = IpPrefix.valueOf("1111:2222:3333:4444::/128");
+        assertTrue(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4444::")));
+        assertFalse(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4444::1")));
+        assertFalse(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4445::")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("::")));
+        assertFalse(ipPrefix.contains(
+                IpAddress.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+
+        ipPrefix = IpPrefix.valueOf("::/0");
+        assertTrue(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4444::")));
+        assertTrue(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4444::1")));
+        assertTrue(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4445::")));
+        assertTrue(ipPrefix.contains(IpAddress.valueOf("::")));
+        assertTrue(ipPrefix.contains(
+                IpAddress.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+
+        ipPrefix =
+            IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertFalse(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4444::")));
+        assertFalse(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4444::1")));
+        assertFalse(ipPrefix.contains(
+                IpAddress.valueOf("1111:2222:3333:4445::")));
+        assertFalse(ipPrefix.contains(IpAddress.valueOf("::")));
+        assertTrue(ipPrefix.contains(
+                IpAddress.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")));
+    }
+
+    /**
+     * Tests equality of {@link IpPrefix} for IPv4.
+     */
+    @Test
+    public void testEqualityIPv4() {
+        new EqualsTester()
+            .addEqualityGroup(IpPrefix.valueOf("1.2.0.0/24"),
+                              IpPrefix.valueOf("1.2.0.0/24"),
+                              IpPrefix.valueOf("1.2.0.4/24"))
+            .addEqualityGroup(IpPrefix.valueOf("1.2.0.0/16"),
+                              IpPrefix.valueOf("1.2.0.0/16"))
+            .addEqualityGroup(IpPrefix.valueOf("1.2.0.0/32"),
+                              IpPrefix.valueOf("1.2.0.0/32"))
+            .addEqualityGroup(IpPrefix.valueOf("1.3.0.0/24"),
+                              IpPrefix.valueOf("1.3.0.0/24"))
+            .addEqualityGroup(IpPrefix.valueOf("0.0.0.0/0"),
+                              IpPrefix.valueOf("0.0.0.0/0"))
+            .addEqualityGroup(IpPrefix.valueOf("255.255.255.255/32"),
+                              IpPrefix.valueOf("255.255.255.255/32"))
+            .testEquals();
+    }
+
+    /**
+     * Tests equality of {@link IpPrefix} for IPv6.
+     */
+    @Test
+    public void testEqualityIPv6() {
+        new EqualsTester()
+            .addEqualityGroup(
+                IpPrefix.valueOf("1111:2222:3333:4444::/120"),
+                IpPrefix.valueOf("1111:2222:3333:4444::1/120"),
+                IpPrefix.valueOf("1111:2222:3333:4444::/120"))
+            .addEqualityGroup(
+                IpPrefix.valueOf("1111:2222:3333:4444::/64"),
+                IpPrefix.valueOf("1111:2222:3333:4444::/64"))
+            .addEqualityGroup(
+                IpPrefix.valueOf("1111:2222:3333:4444::/128"),
+                IpPrefix.valueOf("1111:2222:3333:4444::/128"))
+            .addEqualityGroup(
+                IpPrefix.valueOf("1111:2222:3333:4445::/64"),
+                IpPrefix.valueOf("1111:2222:3333:4445::/64"))
+            .addEqualityGroup(
+                IpPrefix.valueOf("::/0"),
+                IpPrefix.valueOf("::/0"))
+            .addEqualityGroup(
+                IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"),
+                IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"))
+            .testEquals();
+    }
+
+    /**
+     * Tests object string representation for IPv4.
+     */
+    @Test
+    public void testToStringIPv4() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf("1.2.3.0/24");
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
+
+        ipPrefix = IpPrefix.valueOf("1.2.3.4/24");
+        assertThat(ipPrefix.toString(), is("1.2.3.0/24"));
+
+        ipPrefix = IpPrefix.valueOf("0.0.0.0/0");
+        assertThat(ipPrefix.toString(), is("0.0.0.0/0"));
+
+        ipPrefix = IpPrefix.valueOf("255.255.255.255/32");
+        assertThat(ipPrefix.toString(), is("255.255.255.255/32"));
+    }
+
+    /**
+     * Tests object string representation for IPv6.
+     */
+    @Test
+    public void testToStringIPv6() {
+        IpPrefix ipPrefix;
+
+        ipPrefix = IpPrefix.valueOf("1100::/8");
+        assertThat(ipPrefix.toString(), is("1100::/8"));
+
+        ipPrefix = IpPrefix.valueOf("1111:2222:3333:4444:5555:6666:7777:8885/8");
+        assertThat(ipPrefix.toString(), is("1100::/8"));
+
+        ipPrefix = IpPrefix.valueOf("::/0");
+        assertThat(ipPrefix.toString(), is("::/0"));
+
+        ipPrefix = IpPrefix.valueOf("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128");
+        assertThat(ipPrefix.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"));
     }
 }
diff --git a/utils/netty/pom.xml b/utils/netty/pom.xml
index fd0e8e0..436214f 100644
--- a/utils/netty/pom.xml
+++ b/utils/netty/pom.xml
@@ -73,6 +73,7 @@
         <dependency>
           <groupId>io.netty</groupId>
           <artifactId>netty-transport-native-epoll</artifactId>
+          <version>${netty4.version}</version>
         </dependency>
     </dependencies>
 
diff --git a/utils/netty/src/main/java/org/onlab/netty/MessageDecoder.java b/utils/netty/src/main/java/org/onlab/netty/MessageDecoder.java
index c957153..51f8dd3 100644
--- a/utils/netty/src/main/java/org/onlab/netty/MessageDecoder.java
+++ b/utils/netty/src/main/java/org/onlab/netty/MessageDecoder.java
@@ -50,7 +50,7 @@
             ByteBuf buffer,
             List<Object> out) throws Exception {
 
-        switch(state()) {
+        switch (state()) {
         case READ_HEADER_VERSION:
             int headerVersion = buffer.readInt();
             checkState(headerVersion == MessageEncoder.HEADER_VERSION, "Unexpected header version");
diff --git a/utils/netty/src/main/java/org/onlab/netty/MessageEncoder.java b/utils/netty/src/main/java/org/onlab/netty/MessageEncoder.java
index d4b82cf..fe89039 100644
--- a/utils/netty/src/main/java/org/onlab/netty/MessageEncoder.java
+++ b/utils/netty/src/main/java/org/onlab/netty/MessageEncoder.java
@@ -15,6 +15,8 @@
  */
 package org.onlab.netty;
 
+import java.nio.charset.StandardCharsets;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -32,7 +34,7 @@
     private final Logger log = LoggerFactory.getLogger(getClass());
 
     // onosiscool in ascii
-    public static final byte[] PREAMBLE = "onosiscool".getBytes();
+    static final byte[] PREAMBLE = "onosiscool".getBytes(StandardCharsets.US_ASCII);
     public static final int HEADER_VERSION = 1;
     public static final int SERIALIZER_VERSION = 1;
 
diff --git a/utils/netty/src/main/java/org/onlab/netty/MessagingService.java b/utils/netty/src/main/java/org/onlab/netty/MessagingService.java
index 8c4f4ee..9277448 100644
--- a/utils/netty/src/main/java/org/onlab/netty/MessagingService.java
+++ b/utils/netty/src/main/java/org/onlab/netty/MessagingService.java
@@ -29,7 +29,7 @@
      * @param ep end point to send the message to.
      * @param type type of message.
      * @param payload message payload bytes.
-     * @throws IOException
+     * @throws IOException when I/O exception of some sort has occurred
      */
     public void sendAsync(Endpoint ep, String type, byte[] payload) throws IOException;
 
@@ -39,7 +39,7 @@
      * @param type type of message.
      * @param payload message payload.
      * @return a response future
-     * @throws IOException
+     * @throws IOException when I/O exception of some sort has occurred
      */
     public ListenableFuture<byte[]> sendAndReceive(Endpoint ep, String type, byte[] payload) throws IOException;
 
diff --git a/utils/nio/src/main/java/org/onlab/nio/IOLoop.java b/utils/nio/src/main/java/org/onlab/nio/IOLoop.java
index 9d417ac..106df7b 100644
--- a/utils/nio/src/main/java/org/onlab/nio/IOLoop.java
+++ b/utils/nio/src/main/java/org/onlab/nio/IOLoop.java
@@ -107,6 +107,7 @@
      * Completes connection request pending on the given selection key.
      *
      * @param key selection key holding the pending connect operation.
+     * @throws IOException when I/O exception of some sort has occurred
      */
     protected void connect(SelectionKey key) throws IOException {
         SocketChannel ch = (SocketChannel) key.channel();
diff --git a/utils/nio/src/main/java/org/onlab/nio/SelectorLoop.java b/utils/nio/src/main/java/org/onlab/nio/SelectorLoop.java
index 4f3220d..605e0a9 100644
--- a/utils/nio/src/main/java/org/onlab/nio/SelectorLoop.java
+++ b/utils/nio/src/main/java/org/onlab/nio/SelectorLoop.java
@@ -74,6 +74,7 @@
 
     /**
      * Indicates that the loop is marked to run.
+     * @return true if the loop is marked to run
      */
     protected boolean isRunning() {
         return state == State.STARTED || state == State.STARTING;
diff --git a/utils/thirdparty/src/main/java/org/onlab/thirdparty/OnlabThirdparty.java b/utils/thirdparty/src/main/java/org/onlab/thirdparty/OnlabThirdparty.java
index 9c9b03f..7405558 100644
--- a/utils/thirdparty/src/main/java/org/onlab/thirdparty/OnlabThirdparty.java
+++ b/utils/thirdparty/src/main/java/org/onlab/thirdparty/OnlabThirdparty.java
@@ -18,7 +18,7 @@
 
 /**
  * Empty class required to get the onlab-thirdparty module to build properly.
- * <p/>
+ *
  * TODO Figure out how to remove this.
  */
 public class OnlabThirdparty {
diff --git a/web/gui/src/main/java/org/onlab/onos/gui/TopologyResource.java b/web/gui/src/main/java/org/onlab/onos/gui/TopologyResource.java
index 14795b2..b63e3e0 100644
--- a/web/gui/src/main/java/org/onlab/onos/gui/TopologyResource.java
+++ b/web/gui/src/main/java/org/onlab/onos/gui/TopologyResource.java
@@ -101,7 +101,7 @@
                             new Prop("Vendor", device.manufacturer()),
                             new Prop("H/W Version", device.hwVersion()),
                             new Prop("S/W Version", device.swVersion()),
-                            new Prop("S/W Version", device.serialNumber()),
+                            new Prop("Serial Number", device.serialNumber()),
                             new Separator(),
                             new Prop("Latitude", annot.value("latitude")),
                             new Prop("Longitude", annot.value("longitude")),
diff --git a/web/gui/src/main/webapp/geometry2.js b/web/gui/src/main/webapp/geometry2.js
new file mode 100644
index 0000000..5ede643
--- /dev/null
+++ b/web/gui/src/main/webapp/geometry2.js
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ Geometry library - based on work by Mike Bostock.
+ */
+
+(function() {
+
+    if (typeof geo == 'undefined') {
+        geo = {};
+    }
+
+    var tolerance = 1e-10;
+
+    function eq(a, b) {
+        return (Math.abs(a - b) < tolerance);
+    }
+
+    function gt(a, b) {
+        return (a - b > -tolerance);
+    }
+
+    function lt(a, b) {
+        return gt(b, a);
+    }
+
+    geo.eq = eq;
+    geo.gt = gt;
+    geo.lt = lt;
+
+    geo.LineSegment = function(x1, y1, x2, y2) {
+        this.x1 = x1;
+        this.y1 = y1;
+        this.x2 = x2;
+        this.y2 = y2;
+
+        // Ax + By = C
+        this.a = y2 - y1;
+        this.b = x1 - x2;
+        this.c = x1 * this.a + y1 * this.b;
+
+        if (eq(this.a, 0) && eq(this.b, 0)) {
+            throw new Error(
+                'Cannot construct a LineSegment with two equal endpoints.');
+        }
+    };
+
+    geo.LineSegment.prototype.intersect = function(that) {
+        var d = (this.x1 - this.x2) * (that.y1 - that.y2) -
+            (this.y1 - this.y2) * (that.x1 - that.x2);
+
+        if (eq(d, 0)) {
+            // The two lines are parallel or very close.
+            return {
+                x : NaN,
+                y : NaN
+            };
+        }
+
+        var t1  = this.x1 * this.y2 - this.y1 * this.x2,
+            t2  = that.x1 * that.y2 - that.y1 * that.x2,
+            x   = (t1 * (that.x1 - that.x2) - t2 * (this.x1 - this.x2)) / d,
+            y   = (t1 * (that.y1 - that.y2) - t2 * (this.y1 - this.y2)) / d,
+            in1 = (gt(x, Math.min(this.x1, this.x2)) && lt(x, Math.max(this.x1, this.x2)) &&
+                gt(y, Math.min(this.y1, this.y2)) && lt(y, Math.max(this.y1, this.y2))),
+            in2 = (gt(x, Math.min(that.x1, that.x2)) && lt(x, Math.max(that.x1, that.x2)) &&
+                gt(y, Math.min(that.y1, that.y2)) && lt(y, Math.max(that.y1, that.y2)));
+
+        return {
+            x   : x,
+            y   : y,
+            in1 : in1,
+            in2 : in2
+        };
+    };
+
+    geo.LineSegment.prototype.x = function(y) {
+        // x = (C - By) / a;
+        if (this.a) {
+            return (this.c - this.b * y) / this.a;
+        } else {
+            // a == 0 -> horizontal line
+            return NaN;
+        }
+    };
+
+    geo.LineSegment.prototype.y = function(x) {
+        // y = (C - Ax) / b;
+        if (this.b) {
+            return (this.c - this.a * x) / this.b;
+        } else {
+            // b == 0 -> vertical line
+            return NaN;
+        }
+    };
+
+    geo.LineSegment.prototype.length = function() {
+        return Math.sqrt(
+                (this.y2 - this.y1) * (this.y2 - this.y1) +
+                (this.x2 - this.x1) * (this.x2 - this.x1));
+    };
+
+    geo.LineSegment.prototype.offset = function(x, y) {
+        return new geo.LineSegment(
+                this.x1 + x, this.y1 + y,
+                this.x2 + x, this.y2 + y);
+    };
+
+})();
diff --git a/web/gui/src/main/webapp/index2.html b/web/gui/src/main/webapp/index2.html
new file mode 100644
index 0000000..1ddc318
--- /dev/null
+++ b/web/gui/src/main/webapp/index2.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<!--
+  ~ Copyright 2014 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.
+  -->
+
+<!--
+  ONOS UI - single page web app
+  Version 1.1
+
+  @author Simon Hunt
+  -->
+<html>
+<head>
+    <meta charset="utf-8">
+    <title>ONOS GUI (v1.1)</title>
+
+    <link rel="shortcut icon" href="img/onos-logo.png">
+
+    <!-- first script to be run -->
+    <script src="preamble.js"></script>
+
+    <!-- Third party library code included here -->
+    <!--TODO: use the minified version of d3, once debugging is complete -->
+    <script src="libs/d3.js"></script>
+    <script src="libs/jquery-2.1.1.min.js"></script>
+
+    <!-- Base library and framework stylesheets included here -->
+    <link rel="stylesheet" href="base.css">
+    <link rel="stylesheet" href="onos2.css">
+    <link rel="stylesheet" href="mast2.css">
+
+    <!-- This is where contributed stylesheets get INJECTED -->
+    <!-- TODO: replace with template marker and inject refs server-side -->
+    <link rel="stylesheet" href="topo2.css">
+
+
+    <!-- General library modules included here-->
+    <script src="geometry2.js"></script>
+
+    <!-- ONOS UI Framework included here-->
+    <script src="onos2.js"></script>
+
+</head>
+<body>
+    <div id="frame">
+        <div id="mast">
+            <!-- NOTE: masthead injected here by mast.js -->
+        </div>
+        <div id="view">
+            <!-- NOTE: views injected here by onos.js -->
+        </div>
+        <div id="overlays">
+            <!-- NOTE: overlays injected here, as needed -->
+        </div>
+    </div>
+
+    <!-- Initialize the UI...-->
+    <script type="text/javascript">
+        var ONOS = $.onos({
+            comment: "configuration options",
+            startVid: 'topo',
+//            startVid: 'sampleKeys',
+            trace: false
+        });
+    </script>
+
+    <!-- Framework module files included here -->
+    <script src="mast2.js"></script>
+
+    <!-- Sample views; can be dispensed with eventually -->
+    <script src="sample2.js"></script>
+    <script src="sampleAlt2.js"></script>
+    <script src="sampleRadio.js"></script>
+    <script src="sampleKeys.js"></script>
+
+    <!-- Contributed (application) views injected here -->
+    <!-- TODO: replace with template marker and inject refs server-side -->
+    <script src="topo2.js"></script>
+
+    <!-- finally, build the UI-->
+    <script type="text/javascript">
+        $(ONOS.buildUi);
+    </script>
+
+</body>
+</html>
diff --git a/web/gui/src/main/webapp/mast2.css b/web/gui/src/main/webapp/mast2.css
new file mode 100644
index 0000000..7f094b3
--- /dev/null
+++ b/web/gui/src/main/webapp/mast2.css
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ ONOS GUI -- Masthead -- CSS file
+
+ @author Simon Hunt
+ */
+
+#mast {
+    height: 36px;
+    padding: 4px;
+    background-color: #bbb;
+    vertical-align: baseline;
+    box-shadow: 0px 2px 8px #777;
+}
+
+#mast img#logo {
+    height: 38px;
+    padding-left: 8px;
+    padding-right: 8px;
+}
+
+#mast span.title {
+    color: #369;
+    font-size: 14pt;
+    font-style: italic;
+    vertical-align: 12px;
+}
+
+#mast span.right {
+    padding-top: 8px;
+    padding-right: 16px;
+    float: right;
+}
+
+#mast span.radio {
+    font-size: 10pt;
+    margin: 4px 2px;
+    border: 1px dotted #222;
+    padding: 1px 6px;
+    color: #eee;
+    cursor: pointer;
+}
+
+#mast span.radio.active {
+    background-color: #bbb;
+    border: 1px solid #eee;
+    padding: 1px 6px;
+    color: #666;
+    font-weight: bold;
+}
diff --git a/web/gui/src/main/webapp/mast2.js b/web/gui/src/main/webapp/mast2.js
new file mode 100644
index 0000000..169dd35
--- /dev/null
+++ b/web/gui/src/main/webapp/mast2.js
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ ONOS GUI -- Masthead
+
+ Defines the masthead for the UI. Injects logo and title, as well as providing
+ the placeholder for a set of radio buttons.
+
+ @author Simon Hunt
+ */
+
+(function (onos){
+    'use strict';
+
+    // API's
+    var api = onos.api;
+
+    // Config variables
+    var guiTitle = 'Open Networking Operating System';
+
+    // DOM elements and the like
+    var mast = d3.select('#mast');
+
+    mast.append('img')
+        .attr({
+            id: 'logo',
+            src: 'img/onos-logo.png'
+        });
+
+    mast.append('span')
+        .attr({
+            class: 'title'
+        })
+        .text(guiTitle);
+
+    mast.append('span')
+        .attr({
+            id: 'mastRadio',
+            class: 'right'
+        });
+
+}(ONOS));
\ No newline at end of file
diff --git a/web/gui/src/main/webapp/module-div-template.js b/web/gui/src/main/webapp/module-div-template.js
new file mode 100644
index 0000000..09ea569
--- /dev/null
+++ b/web/gui/src/main/webapp/module-div-template.js
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ Module template file for DIV based view.
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    var list,
+        data = [ 'foo', 'bar', 'baz' ];
+
+    // invoked only the first time the view is loaded
+    function preload(view, ctx) {
+        // NOTE: view.$div is a D3 selection of the view's div
+        list = view.$div.append('ul');
+    }
+
+    // invoked just prior to loading the view
+    function reset(view) {
+
+    }
+
+    // invoked when the view is loaded
+    function load(view, ctx) {
+        list.selectAll('li')
+            .data(data)
+            .enter()
+            .append('li')
+            .text(function (d) { return d; })
+    }
+
+    // invoked when the view is resized
+    function resize(view, ctx) {
+
+    }
+
+    // == register the view here, with links to lifecycle callbacks
+
+    onos.ui.addView('myViewId', {
+        preload: preload,
+        reset: reset,
+        load: load,
+        // unload: unload,
+        // error: error
+        resize: resize
+    });
+
+}(ONOS));
diff --git a/web/gui/src/main/webapp/module-svg-template.js b/web/gui/src/main/webapp/module-svg-template.js
new file mode 100644
index 0000000..66da760
--- /dev/null
+++ b/web/gui/src/main/webapp/module-svg-template.js
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ Module template file for SVG based view.
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    var svg;
+
+    // set the size of the SVG layer to match that of the view
+    function sizeSvg(view) {
+        svg.attr({
+            width: view.width(),
+            height: view.height()
+        });
+    }
+
+    // invoked only the first time the view is loaded
+    function preload(view, ctx) {
+        // NOTE: view.$div is a D3 selection of the view's div
+        svg = view.$div.append('svg');
+        sizeSvg(view);
+        // ... further code to initialize the SVG view ...
+
+    }
+
+    // invoked just prior to loading the view
+    function reset(view) {
+
+    }
+
+    // invoked when the view is loaded
+    function load(view, ctx) {
+
+    }
+
+    // invoked when the view is resized
+    function resize(view, ctx) {
+        sizeSvg(view);
+        // ... further code to handle resize of view ...
+
+    }
+
+    // == register the view here, with links to lifecycle callbacks
+
+    onos.ui.addView('myViewId', {
+        preload: preload,
+        reset: reset,
+        load: load,
+        // unload: unload,
+        // error: error
+        resize: resize
+    });
+
+}(ONOS));
diff --git a/web/gui/src/main/webapp/network.js b/web/gui/src/main/webapp/network.js
index b249c09..01e4acd 100644
--- a/web/gui/src/main/webapp/network.js
+++ b/web/gui/src/main/webapp/network.js
@@ -28,7 +28,7 @@
 
     // configuration data
     var config = {
-        useLiveData: true,
+        useLiveData: false,
         debugOn: false,
         debug: {
             showNodeXY: false,
diff --git a/web/gui/src/main/webapp/onos2.css b/web/gui/src/main/webapp/onos2.css
new file mode 100644
index 0000000..f693ecc
--- /dev/null
+++ b/web/gui/src/main/webapp/onos2.css
@@ -0,0 +1,217 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ ONOS GUI -- Base Framework -- CSS file
+
+ @author Simon Hunt
+ */
+
+html, body {
+    height: 100%;
+}
+
+div.onosView {
+    display: none;
+}
+
+div.onosView.currentView {
+    display: block;
+}
+
+/*
+ * ==============================================================
+ * END OF NEW ONOS.JS file
+ * ==============================================================
+ */
+
+/*
+ * === DEBUGGING ======
+ */
+svg {
+    /*border: 1px dashed red;*/
+}
+
+svg #bg {
+    opacity: 0.5;
+}
+
+
+/*
+ * Network Graph elements ======================================
+ */
+
+svg .link {
+    fill: none;
+    stroke: #666;
+    stroke-width: 2.0px;
+    opacity: .7;
+
+    transition: opacity 250ms;
+    -webkit-transition: opacity 250ms;
+    -moz-transition: opacity 250ms;
+}
+
+svg .link.host {
+    stroke: #666;
+    stroke-width: 1px;
+}
+
+svg g.portLayer rect.port {
+    fill: #ccc;
+}
+
+svg g.portLayer text {
+    font: 8pt sans-serif;
+    pointer-events: none;
+}
+
+svg .node.device rect {
+    stroke-width: 1.5px;
+
+    transition: opacity 250ms;
+    -webkit-transition: opacity 250ms;
+    -moz-transition: opacity 250ms;
+}
+
+svg .node.device.fixed rect {
+    stroke-width: 1.5;
+    stroke: #ccc;
+}
+
+svg .node.device.roadm rect {
+    fill: #03c;
+}
+
+svg .node.device.switch rect {
+    fill: #06f;
+}
+
+svg .node.host circle {
+    fill: #c96;
+    stroke: #000;
+}
+
+svg .node text {
+    fill: white;
+    font: 10pt sans-serif;
+    pointer-events: none;
+}
+
+/* for debugging */
+svg .node circle.debug {
+    fill: white;
+    stroke: red;
+}
+svg .node rect.debug {
+    fill: yellow;
+    stroke: red;
+    opacity: 0.35;
+}
+
+
+svg .node.selected rect,
+svg .node.selected circle {
+    filter: url(#blue-glow);
+}
+
+svg .link.inactive,
+svg .port.inactive,
+svg .portText.inactive,
+svg .node.inactive rect,
+svg .node.inactive circle,
+svg .node.inactive text,
+svg .node.inactive image {
+    opacity: .1;
+}
+
+svg .node.inactive.selected rect,
+svg .node.inactive.selected text,
+svg .node.inactive.selected image {
+    opacity: .6;
+}
+
+/*
+ * =============================================================
+ */
+
+/*
+ * Specific structural elements
+ */
+
+/* This is to ensure that the body does not expand to account for the
+   flyout details pane, that is positioned "off screen".
+ */
+body {
+    overflow: hidden;
+}
+
+
+#frame {
+    width: 100%;
+    height: 100%;
+    background-color: #fff;
+}
+
+#flyout {
+    position: absolute;
+    z-index: 100;
+    display: block;
+    top: 10%;
+    width: 280px;
+    right: -300px;
+    opacity: 0;
+    background-color: rgba(255,255,255,0.8);
+
+    padding: 10px;
+    color: black;
+    font-size: 10pt;
+    box-shadow: 2px 2px 16px #777;
+}
+
+#flyout h2 {
+    margin: 8px 4px;
+    color: black;
+    vertical-align: middle;
+}
+
+#flyout h2 img {
+    height: 32px;
+    padding-right: 8px;
+    vertical-align: middle;
+}
+
+#flyout p, table {
+    margin: 4px 4px;
+}
+
+#flyout td.label {
+    font-style: italic;
+    color: #777;
+    padding-right: 12px;
+}
+
+#flyout td.value {
+
+}
+
+#flyout hr {
+    height: 1px;
+    color: #ccc;
+    background-color: #ccc;
+    border: 0;
+}
+
diff --git a/web/gui/src/main/webapp/onos2.js b/web/gui/src/main/webapp/onos2.js
new file mode 100644
index 0000000..f509757
--- /dev/null
+++ b/web/gui/src/main/webapp/onos2.js
@@ -0,0 +1,612 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ ONOS GUI -- Base Framework
+
+ @author Simon Hunt
+ */
+
+(function ($) {
+    'use strict';
+    var tsI = new Date().getTime(),         // initialize time stamp
+        tsB,                                // build time stamp
+        mastHeight = 36,                    // see mast2.css
+        defaultVid = 'sample';
+
+
+    // attach our main function to the jQuery object
+    $.onos = function (options) {
+        var uiApi,
+            viewApi,
+            navApi;
+
+        var defaultOptions = {
+            trace: false,
+            startVid: defaultVid
+        };
+
+        // compute runtime settings
+        var settings = $.extend({}, defaultOptions, options);
+
+        // internal state
+        var views = {},
+            current = {
+                view: null,
+                ctx: ''
+            },
+            built = false,
+            errorCount = 0,
+            keyHandler = {};
+
+        // DOM elements etc.
+        var $view,
+            $mastRadio;
+
+
+        function whatKey(code) {
+            switch (code) {
+                case 13: return 'enter';
+                case 16: return 'shift';
+                case 17: return 'ctrl';
+                case 18: return 'alt';
+                case 27: return 'esc';
+                case 32: return 'space';
+                case 37: return 'leftArrow';
+                case 38: return 'upArrow';
+                case 39: return 'rightArrow';
+                case 40: return 'downArrow';
+                case 91: return 'cmdLeft';
+                case 93: return 'cmdRight';
+                default:
+                    if ((code >= 48 && code <= 57) ||
+                        (code >= 65 && code <= 90)) {
+                        return String.fromCharCode(code);
+                    } else if (code >= 112 && code <= 123) {
+                        return 'F' + (code - 111);
+                    }
+                    return '.';
+            }
+        }
+
+
+        // ..........................................................
+        // Internal functions
+
+        // throw an error
+        function throwError(msg) {
+            // separate function, as we might add tracing here too, later
+            throw new Error(msg);
+        }
+
+        function doError(msg) {
+            errorCount++;
+            console.error(msg);
+        }
+
+        function trace(msg) {
+            if (settings.trace) {
+                console.log(msg);
+            }
+        }
+
+        function traceFn(fn, params) {
+            if (settings.trace) {
+                console.log('*FN* ' + fn + '(...): ' + params);
+            }
+        }
+
+        // hash navigation
+        function hash() {
+            var hash = window.location.hash,
+                redo = false,
+                view,
+                t;
+
+            traceFn('hash', hash);
+
+            if (!hash) {
+                hash = settings.startVid;
+                redo = true;
+            }
+
+            t = parseHash(hash);
+            if (!t || !t.vid) {
+                doError('Unable to parse target hash: ' + hash);
+            }
+
+            view = views[t.vid];
+            if (!view) {
+                doError('No view defined with id: ' + t.vid);
+            }
+
+            if (redo) {
+                window.location.hash = makeHash(t);
+                // the above will result in a hashchange event, invoking
+                // this function again
+            } else {
+                // hash was not modified... navigate to where we need to be
+                navigate(hash, view, t);
+            }
+        }
+
+        function parseHash(s) {
+            // extract navigation coordinates from the supplied string
+            // "vid,ctx" --> { vid:vid, ctx:ctx }
+            traceFn('parseHash', s);
+
+            var m = /^[#]{0,1}(\S+),(\S*)$/.exec(s);
+            if (m) {
+                return { vid: m[1], ctx: m[2] };
+            }
+
+            m = /^[#]{0,1}(\S+)$/.exec(s);
+            return m ? { vid: m[1] } : null;
+        }
+
+        function makeHash(t, ctx) {
+            traceFn('makeHash');
+            // make a hash string from the given navigation coordinates.
+            // if t is not an object, then it is a vid
+            var h = t,
+                c = ctx || '';
+
+            if ($.isPlainObject(t)) {
+                h = t.vid;
+                c = t.ctx || '';
+            }
+
+            if (c) {
+                h += ',' + c;
+            }
+            trace('hash = "' + h + '"');
+            return h;
+        }
+
+        function navigate(hash, view, t) {
+            traceFn('navigate', view.vid);
+            // closePanes()     // flyouts etc.
+            // updateNav()      // accordion / selected nav item etc.
+            createView(view);
+            setView(view, hash, t);
+        }
+
+        function reportBuildErrors() {
+            traceFn('reportBuildErrors');
+            // TODO: validate registered views / nav-item linkage etc.
+            console.log('(no build errors)');
+        }
+
+        // returns the reference if it is a function, null otherwise
+        function isF(f) {
+            return $.isFunction(f) ? f : null;
+        }
+
+        // ..........................................................
+        // View life-cycle functions
+
+        function setViewDimensions(sel) {
+            var w = window.innerWidth,
+                h = window.innerHeight - mastHeight;
+            sel.each(function () {
+                $(this)
+                    .css('width', w + 'px')
+                    .css('height', h + 'px')
+            });
+        }
+
+        function createView(view) {
+            var $d;
+
+            // lazy initialization of the view
+            if (view && !view.$div) {
+                trace('creating view for ' + view.vid);
+                $d = $view.append('div')
+                        .attr({
+                            id: view.vid,
+                            class: 'onosView'
+                         });
+                setViewDimensions($d);
+                view.$div = $d;   // cache a reference to the D3 selection
+            }
+        }
+
+        function setView(view, hash, t) {
+            traceFn('setView', view.vid);
+            // set the specified view as current, while invoking the
+            // appropriate life-cycle callbacks
+
+            // if there is a current view, and it is not the same as
+            // the incoming view, then unload it...
+            if (current.view && (current.view.vid !== view.vid)) {
+                current.view.unload();
+
+                // detach radio buttons, key handlers, etc.
+                $('#mastRadio').children().detach();
+                keyHandler.fn = null;
+                keyHandler.map = {};
+            }
+
+            // cache new view and context
+            current.view = view;
+            current.ctx = t.ctx || '';
+
+            // preload is called only once, after the view is in the DOM
+            if (!view.preloaded) {
+                view.preload(current.ctx);
+                view.preloaded = true;
+            }
+
+            // clear the view of stale data
+            view.reset();
+
+            // load the view
+            view.load(current.ctx);
+        }
+
+        // generate 'unique' id by prefixing view id
+        function uid(view, id) {
+            return view.vid + '-' + id;
+        }
+
+        // restore id by removing view id prefix
+        function unUid(view, uid) {
+            var re = new RegExp('^' + view.vid + '-');
+            return uid.replace(re, '');
+        }
+
+        function setRadioButtons(vid, btnSet, callback) {
+            var view = views[vid],
+                btnG;
+
+            // lazily create the buttons...
+            if (!(btnG = view.radioButtons)) {
+                btnG = d3.select(document.createElement('div'));
+
+                btnSet.forEach(function (btn, i) {
+                    var bid = btn.id || 'b' + i,
+                        txt = btn.text || 'Button #' + i,
+                        b = btnG.append('span')
+                        .attr({
+                            id: uid(view, bid),
+                            class: 'radio'
+                        })
+                        .text(txt);
+                    if (i === 0) {
+                        b.classed('active', true);
+                    }
+                });
+
+                btnG.selectAll('span')
+                    .on('click', function (d) {
+                        var btn = d3.select(this),
+                            bid = btn.attr('id'),
+                            act = btn.classed('active');
+
+                        if (!act) {
+                            $mastRadio.selectAll('span')
+                                .classed('active', false);
+                            btn.classed('active', true);
+
+                            callback(view.token(), unUid(view, bid));
+                        }
+                    });
+
+                view.radioButtons = btnG;
+            }
+
+            // attach the buttons to the masthead
+            $mastRadio.node().appendChild(btnG.node());
+        }
+
+        function setKeyBindings(keyArg) {
+            if ($.isFunction(keyArg)) {
+                // set general key handler callback
+                keyHandler.fn = keyArg;
+            } else {
+                // set specific key filter map
+                keyHandler.map = keyArg;
+            }
+        }
+
+        function keyIn() {
+            var event = d3.event,
+                keyCode = event.keyCode,
+                key = whatKey(keyCode),
+                cb = isF(keyHandler.map[key]) || isF(keyHandler.fn);
+
+            if (cb) {
+                cb(current.view.token(), key, keyCode, event);
+            }
+        }
+
+        function resize(e) {
+            d3.selectAll('.onosView').call(setViewDimensions);
+            // allow current view to react to resize event...
+            if (current.view) {
+                current.view.resize(current.ctx);
+            }
+        }
+
+        // ..........................................................
+        // View class
+        //   Captures state information about a view.
+
+        // Constructor
+        //      vid : view id
+        //      nid : id of associated nav-item (optional)
+        //      cb  : callbacks (preload, reset, load, unload, resize, error)
+        function View(vid) {
+            var av = 'addView(): ',
+                args = Array.prototype.slice.call(arguments),
+                nid,
+                cb;
+
+            args.shift();   // first arg is always vid
+            if (typeof args[0] === 'string') {  // nid specified
+                nid = args.shift();
+            }
+            cb = args.shift();
+
+            this.vid = vid;
+
+            if (validateViewArgs(vid)) {
+                this.nid = nid;     // explicit navitem id (can be null)
+                this.cb = $.isPlainObject(cb) ? cb : {};    // callbacks
+                this.$div = null;               // view not yet added to DOM
+                this.radioButtons = null;       // no radio buttons yet
+                this.ok = true;                 // valid view
+            }
+        }
+
+        function validateViewArgs(vid) {
+            var av = "ui.addView(...): ",
+                ok = false;
+            if (typeof vid !== 'string' || !vid) {
+                doError(av + 'vid required');
+            } else if (views[vid]) {
+                doError(av + 'View ID "' + vid + '" already exists');
+            } else {
+                ok = true;
+            }
+            return ok;
+        }
+
+        var viewInstanceMethods = {
+            token: function () {
+                return {
+                    // attributes
+                    vid: this.vid,
+                    nid: this.nid,
+                    $div: this.$div,
+
+                    // functions
+                    width: this.width,
+                    height: this.height,
+                    uid: this.uid,
+                    setRadio: this.setRadio,
+                    setKeys: this.setKeys
+                }
+            },
+
+            preload: function (ctx) {
+                var c = ctx || '',
+                    fn = isF(this.cb.preload);
+                traceFn('View.preload', this.vid + ', ' + c);
+                if (fn) {
+                    trace('PRELOAD cb for ' + this.vid);
+                    fn(this.token(), c);
+                }
+            },
+
+            reset: function () {
+                var fn = isF(this.cb.reset);
+                traceFn('View.reset', this.vid);
+                if (fn) {
+                    trace('RESET cb for ' + this.vid);
+                    fn(this.token());
+                } else if (this.cb.reset === true) {
+                    // boolean true signifies "clear view"
+                    trace('  [true] cleaing view...');
+                    viewApi.empty();
+                }
+            },
+
+            load: function (ctx) {
+                var c = ctx || '',
+                    fn = isF(this.cb.load);
+                traceFn('View.load', this.vid + ', ' + c);
+                this.$div.classed('currentView', true);
+                // TODO: add radio button set, if needed
+                if (fn) {
+                    trace('LOAD cb for ' + this.vid);
+                    fn(this.token(), c);
+                }
+            },
+
+            unload: function () {
+                var fn = isF(this.cb.unload);
+                traceFn('View.unload', this.vid);
+                this.$div.classed('currentView', false);
+                // TODO: remove radio button set, if needed
+                if (fn) {
+                    trace('UNLOAD cb for ' + this.vid);
+                    fn(this.token());
+                }
+            },
+
+            resize: function (ctx) {
+                var c = ctx || '',
+                    fn = isF(this.cb.resize),
+                    w = this.width(),
+                    h = this.height();
+                traceFn('View.resize', this.vid + '/' + c +
+                        ' [' + w + 'x' + h + ']');
+                if (fn) {
+                    trace('RESIZE cb for ' + this.vid);
+                    fn(this.token(), c);
+                }
+            },
+
+            error: function (ctx) {
+                var c = ctx || '',
+                    fn = isF(this.cb.error);
+                traceFn('View.error', this.vid + ', ' + c);
+                if (fn) {
+                    trace('ERROR cb for ' + this.vid);
+                    fn(this.token(), c);
+                }
+            },
+
+            width: function () {
+                return $(this.$div.node()).width();
+            },
+
+            height: function () {
+                return $(this.$div.node()).height();
+            },
+
+            setRadio: function (btnSet, cb) {
+                setRadioButtons(this.vid, btnSet, cb);
+            },
+
+            setKeys: function (keyArg) {
+                setKeyBindings(keyArg);
+            },
+
+            uid: function (id) {
+                return uid(this, id);
+            }
+
+            // TODO: consider schedule, clearTimer, etc.
+        };
+
+        // attach instance methods to the view prototype
+        $.extend(View.prototype, viewInstanceMethods);
+
+        // ..........................................................
+        // UI API
+
+        uiApi = {
+            /** @api ui addView( vid, nid, cb )
+             * Adds a view to the UI.
+             * <p>
+             * Views are loaded/unloaded into the view content pane at
+             * appropriate times, by the navigation framework. This method
+             * adds a view to the UI and returns a token object representing
+             * the view. A view's token is always passed as the first
+             * argument to each of the view's life-cycle callback functions.
+             * <p>
+             * Note that if the view is directly referenced by a nav-item,
+             * or in a group of views with one of those views referenced by
+             * a nav-item, then the <i>nid</i> argument can be omitted as
+             * the framework can infer it.
+             * <p>
+             * <i>cb</i> is a plain object containing callback functions:
+             * "preload", "reset", "load", "unload", "resize", "error".
+             * <pre>
+             *     function myLoad(view, ctx) { ... }
+             *     ...
+             *     // short form...
+             *     onos.ui.addView('viewId', {
+             *         load: myLoad
+             *     });
+             * </pre>
+             *
+             * @param vid (string) [*] view ID (a unique DOM element id)
+             * @param nid (string) nav-item ID (a unique DOM element id)
+             * @param cb (object) [*] callbacks object
+             * @return the view token
+             */
+            addView: function (vid, nid, cb) {
+                traceFn('addView', vid);
+                var view = new View(vid, nid, cb),
+                    token;
+                if (view.ok) {
+                    views[vid] = view;
+                    token = view.token();
+                } else {
+                    token = { vid: view.vid, bad: true };
+                }
+                return token;
+            }
+        };
+
+        // ..........................................................
+        // View API
+
+        viewApi = {
+            /** @api view empty( )
+             * Empties the current view.
+             * <p>
+             * More specifically, removes all DOM elements from the
+             * current view's display div.
+             */
+            empty: function () {
+                if (!current.view) {
+                    return;
+                }
+                current.view.$div.html('');
+            }
+        };
+
+        // ..........................................................
+        // Nav API
+        navApi = {
+
+        };
+
+        // ..........................................................
+        // Exported API
+
+        // function to be called from index.html to build the ONOS UI
+        function buildOnosUi() {
+            tsB = new Date().getTime();
+            tsI = tsB - tsI; // initialization duration
+
+            console.log('ONOS UI initialized in ' + tsI + 'ms');
+
+            if (built) {
+                throwError("ONOS UI already built!");
+            }
+            built = true;
+
+            $view = d3.select('#view');
+            $mastRadio = d3.select('#mastRadio');
+
+            $(window).on('hashchange', hash);
+            $(window).on('resize', resize);
+
+            d3.select('body').on('keydown', keyIn);
+
+            // Invoke hashchange callback to navigate to content
+            // indicated by the window location hash.
+            hash();
+
+            // If there were any build errors, report them
+            reportBuildErrors();
+        }
+
+        // export the api and build-UI function
+        return {
+            ui: uiApi,
+            view: viewApi,
+            nav: navApi,
+            buildUi: buildOnosUi
+        };
+    };
+
+}(jQuery));
diff --git a/web/gui/src/main/webapp/module-template.js b/web/gui/src/main/webapp/preamble.js
similarity index 65%
copy from web/gui/src/main/webapp/module-template.js
copy to web/gui/src/main/webapp/preamble.js
index 3de7d79..8ee8e45 100644
--- a/web/gui/src/main/webapp/module-template.js
+++ b/web/gui/src/main/webapp/preamble.js
@@ -15,22 +15,18 @@
  */
 
 /*
- Module template file.
+ ONOS GUI -- Preamble -- the first thing we do
 
  @author Simon Hunt
  */
 
-(function (onos) {
-    'use strict';
+(function () {
+    // Check if the URL in the address bar contains a parameter section
+    // (delineated by '?'). If this is the case, rewrite using '#' instead.
 
-    var api = onos.api;
+    var m = /([^?]*)\?(.*)/.exec(window.location.href);
+    if (m) {
+        window.location.href = m[1] + '#' + m[2];
+    }
 
-    // == define your functions here.....
-
-
-    // == register views here, with links to lifecycle callbacks
-
-//    api.addView('view-id', {/* callbacks */});
-
-
-}(ONOS));
+}());
diff --git a/web/gui/src/main/webapp/sample2.js b/web/gui/src/main/webapp/sample2.js
new file mode 100644
index 0000000..6b4329f
--- /dev/null
+++ b/web/gui/src/main/webapp/sample2.js
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ Sample module file to illustrate framework integration.
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    var pi = Math.PI,
+        svg,
+        dotG,
+        nCircles = 12,
+        circleData = [],
+        dotId = 0,
+        angle = 360 / nCircles,
+        baseAngle = -90 - angle,
+        groupRadius = 120,
+        dotRadius = 24,
+        dotMoveMs = 800,
+        dotAppearMs = 300,
+        dotEase = 'elastic',
+        colorScale = d3.scale.linear()
+            .domain([-pi/2, 2*pi/4, 3*pi/2])
+            .range(['green', 'goldenrod', 'blue']);
+
+    // set the size of the SVG layer to match that of the view
+    function sizeSvg(view) {
+        svg.attr({
+            width: view.width(),
+            height: view.height()
+        });
+    }
+
+    // gets invoked only the first time the view is loaded
+    function preload(view, ctx) {
+        // prepare our SVG layer...
+        svg = view.$div.append('svg');
+        sizeSvg(view);
+        dotG = svg.append('g').attr('id', 'dots');
+    }
+
+    // gets invoked just before our view is loaded
+    function reset(view) {
+        // clear dot group and reset circle data
+        dotG.html('');
+        circleData = [];
+        // also clear text, if any
+        svg.selectAll('text').remove();
+    }
+
+    function updateCirclePositions(view, addNew) {
+        var w = view.width(),
+            h = view.height(),
+            ox = w / 2,
+            oy = h / 2;
+
+        // reposition existing dots
+        circleData.forEach(function (c, i) {
+            var inc = addNew ? 1 : 0,
+                theta = ((i + inc) * angle + baseAngle) * pi/180,
+                dx = Math.cos(theta) * groupRadius,
+                dy = Math.sin(theta) * groupRadius,
+                x = ox + dx,
+                y = oy + dy;
+            if (!addNew && i === 0) {
+                x = ox;
+                y = oy;
+            }
+            c.cx = x;
+            c.cy = y;
+            c.rgb = colorScale(theta);
+        });
+
+        if (addNew) {
+            // introduce a new dot
+            circleData.unshift({
+                cx: ox,
+                cy: oy,
+                id: dotId++
+            });
+        }
+
+        // +1 to account for the circle in the center..
+        if (circleData.length > nCircles + 1) {
+            circleData.splice(nCircles + 1, 1);
+        }
+    }
+
+    function doCircles(view) {
+        var ox = view.width() / 2,
+            oy = view.height() / 2,
+            stroke = 'black',
+            fill = 'red',
+            hoverFill = 'magenta';
+
+        // move existing circles, and add a new one
+        updateCirclePositions(view, true);
+
+        var circ = dotG.selectAll('circle')
+            .data(circleData, function (d) { return d.id; });
+
+        // operate on existing elements
+        circ.on('mouseover', null)
+            .on('mouseout', null)
+            .on('click', null)
+            .transition()
+            .duration(dotMoveMs)
+            .ease(dotEase)
+            .attr({
+                cx: function (d) { return d.cx; },
+                cy: function (d) { return d.cy; }
+            })
+            .style({
+                cursor: 'default',
+                fill: function (d) { return d.rgb; }
+            });
+
+        // operate on entering elements
+        circ.enter()
+            .append('circle')
+            .attr({
+                cx: function (d) { return d.cx; },
+                cy: function (d) { return d.cy; },
+                r: 0
+            })
+            .style({
+                fill: fill,
+                stroke: stroke,
+                'stroke-width': 3.5,
+                cursor: 'pointer',
+                opacity: 0
+            })
+            .on('mouseover', function (d) {
+                d3.select(this).style('fill', hoverFill);
+            })
+            .on('mouseout', function (d) {
+                d3.select(this).style('fill', fill);
+            })
+            .on('click', function (d) {
+                setTimeout(function() {
+                    doCircles(view, true);
+                }, 10);
+            })
+            .transition()
+            .delay(dotMoveMs)
+            .duration(dotAppearMs)
+            .attr('r', dotRadius)
+            .style('opacity', 1);
+
+        // operate on exiting elements
+        circ.exit()
+            .transition()
+            .duration(750)
+            .style('opacity', 0)
+            .attr({
+                cx: ox,
+                cy: oy,
+                r: groupRadius - dotRadius
+            })
+            .remove();
+    }
+
+    function load(view, ctx) {
+        var ctxText = ctx ? 'Context is "' + ctx + '"' : '';
+
+        // display our view context
+        if (ctxText) {
+            svg.append('text')
+                .text(ctxText)
+                .attr({
+                    x: 20,
+                    y: '1.5em'
+                })
+                .style({
+                    fill: 'darkgreen',
+                    'font-size': '20pt'
+                });
+        }
+
+        doCircles(view);
+    }
+
+    function resize(view, ctx) {
+        sizeSvg(view);
+        updateCirclePositions(view);
+
+        // move exiting dots into new positions, relative to view size
+        var circ = dotG.selectAll('circle')
+            .data(circleData, function (d) { return d.id; });
+        circ.attr({
+                cx: function (d) { return d.cx; },
+                cy: function (d) { return d.cy; }
+            });
+    }
+
+    // == register our view here, with links to lifecycle callbacks
+
+    onos.ui.addView('sample', {
+        preload: preload,
+        reset: reset,
+        load: load,
+        resize: resize
+    });
+
+}(ONOS));
diff --git a/web/gui/src/main/webapp/sampleAlt2.js b/web/gui/src/main/webapp/sampleAlt2.js
new file mode 100644
index 0000000..60cbe9d
--- /dev/null
+++ b/web/gui/src/main/webapp/sampleAlt2.js
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ Alternate sample module file to illustrate framework integration.
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    var svg;
+
+
+    function sizeSvg(view) {
+        svg.attr({
+            width: view.width(),
+            height: view.height()
+        });
+    }
+
+    // gets invoked only the first time the view is loaded
+    function preload(view, ctx) {
+        svg = view.$div.append('svg');
+        sizeSvg(view);
+    }
+
+    function reset(view) {
+        // clear our svg of all objects
+        svg.html('');
+    }
+
+    function load(view, ctx) {
+        var fill = 'teal',
+            stroke = 'black';
+
+        svg.append('circle')
+            .attr({
+                cx: view.width() / 2,
+                cy: view.height() / 2,
+                r: 30
+            })
+            .style({
+                fill: fill,
+                stroke: stroke,
+                'stroke-width': 1.5,
+                opacity: 0.5
+            });
+    }
+
+    function resize(view, ctx) {
+        sizeSvg(view);
+        svg.selectAll('circle')
+            .attr({
+                cx: view.width() / 2,
+                cy: view.height() / 2
+            });
+    }
+
+    // == register views here, with links to lifecycle callbacks
+
+    onos.ui.addView('sampleAlt', {
+        preload: preload,
+        reset: reset,
+        load: load,
+        resize: resize
+    });
+
+
+}(ONOS));
diff --git a/web/gui/src/main/webapp/sampleKeys.js b/web/gui/src/main/webapp/sampleKeys.js
new file mode 100644
index 0000000..a99d6a7
--- /dev/null
+++ b/web/gui/src/main/webapp/sampleKeys.js
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ Sample view to illustrate key bindings.
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    var keyDispatch =  {
+        Z: keyUndo,
+        X: keyCut,
+        C: keyCopy,
+        V: keyPaste,
+        space: keySpace
+    };
+
+    function keyUndo(view) {
+        note(view, 'Z = UNDO');
+    }
+
+    function keyCut(view) {
+        note(view, 'X = CUT');
+    }
+
+    function keyCopy(view) {
+        note(view, 'C = COPY');
+    }
+
+    function keyPaste(view) {
+        note(view, 'V = PASTE');
+    }
+
+    function keySpace(view) {
+        note(view, 'The SpaceBar');
+    }
+
+    function note(view, msg) {
+        view.$div.append('p')
+            .text(msg)
+            .style({
+                'font-size': '10pt',
+                color: 'darkorange',
+                padding: '0 20px',
+                margin: 0
+            });
+    }
+
+    function keyCallback(view, key, keyCode, event) {
+        note(view, 'Key = ' + key + ' KeyCode = ' + keyCode);
+    }
+
+    function load(view, ctx) {
+        // this maps specific keys to specific functions (1)
+        view.setKeys(keyDispatch);
+        // whereas, this installs a general key handler function (2)
+        view.setKeys(keyCallback);
+
+        // Note that (1) takes precedence over (2)
+
+        view.$div.append('p')
+            .text('Press a key or two (try Z,X,C,V and others) ...')
+            .style('padding', '2px 8px');
+    }
+
+    // == register the view here, with links to lifecycle callbacks
+
+    onos.ui.addView('sampleKeys', {
+        reset: true,    // empty the div on reset
+        load: load
+    });
+
+}(ONOS));
diff --git a/web/gui/src/main/webapp/sampleRadio.js b/web/gui/src/main/webapp/sampleRadio.js
new file mode 100644
index 0000000..4d362a9
--- /dev/null
+++ b/web/gui/src/main/webapp/sampleRadio.js
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ Sample view to illustrate radio buttons.
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    var data = [ 'Yo, radio button set...', 'Time to shine' ],
+        btnSet = [
+            { id: 'b1', text: 'First Button' },
+            { id: 'b2', text: 'Second Button' },
+            { id: 'b3', text: 'Third Button' }
+        ],
+        btnLookup = {};
+
+    btnSet.forEach(function (b) {
+        btnLookup[b.id] = b;
+    });
+
+    // invoked when the view is loaded
+    function load(view, ctx) {
+        view.setRadio(btnSet, doRadio);
+
+        view.$div.selectAll('p')
+            .data(data)
+            .enter()
+            .append('p')
+            .text(function (d) { return d; })
+            .style('padding', '2px 8px');
+    }
+
+    function doRadio(view, id) {
+        view.$div.append('p')
+            .text('You pressed the ' + btnLookup[id].text)
+            .style({
+                'font-size': '10pt',
+                color: 'green',
+                padding: '0 20px',
+                margin: '2px'
+            });
+    }
+
+    // == register the view here, with links to lifecycle callbacks
+
+    onos.ui.addView('sampleRadio', {
+        reset: true,    // empty the div on reset
+        load: load
+    });
+
+}(ONOS));
diff --git a/web/gui/src/main/webapp/topo2-OLD.js b/web/gui/src/main/webapp/topo2-OLD.js
new file mode 100644
index 0000000..04ce7ab
--- /dev/null
+++ b/web/gui/src/main/webapp/topo2-OLD.js
@@ -0,0 +1,1219 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ ONOS network topology viewer - PoC version 1.0
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    // configuration data
+    var config = {
+        useLiveData: true,
+        debugOn: false,
+        debug: {
+            showNodeXY: false,
+            showKeyHandler: true
+        },
+        options: {
+            layering: true,
+            collisionPrevention: true,
+            loadBackground: true
+        },
+        backgroundUrl: 'img/us-map.png',
+        data: {
+            live: {
+                jsonUrl: 'rs/topology/graph',
+                detailPrefix: 'rs/topology/graph/',
+                detailSuffix: ''
+            },
+            fake: {
+                jsonUrl: 'json/network2.json',
+                detailPrefix: 'json/',
+                detailSuffix: '.json'
+            }
+        },
+        iconUrl: {
+            device: 'img/device.png',
+            host: 'img/host.png',
+            pkt: 'img/pkt.png',
+            opt: 'img/opt.png'
+        },
+        mastHeight: 36,
+        force: {
+            note: 'node.class or link.class is used to differentiate',
+            linkDistance: {
+                infra: 200,
+                host: 40
+            },
+            linkStrength: {
+                infra: 1.0,
+                host: 1.0
+            },
+            charge: {
+                device: -800,
+                host: -1000
+            },
+            ticksWithoutCollisions: 50,
+            marginLR: 20,
+            marginTB: 20,
+            translate: function() {
+                return 'translate(' +
+                    config.force.marginLR + ',' +
+                    config.force.marginTB + ')';
+            }
+        },
+        labels: {
+            imgPad: 16,
+            padLR: 8,
+            padTB: 6,
+            marginLR: 3,
+            marginTB: 2,
+            port: {
+                gap: 3,
+                width: 18,
+                height: 14
+            }
+        },
+        icons: {
+            w: 32,
+            h: 32,
+            xoff: -12,
+            yoff: -8
+        },
+        constraints: {
+            ypos: {
+                host: 0.05,
+                switch: 0.3,
+                roadm: 0.7
+            }
+        },
+        hostLinkWidth: 1.0,
+        hostRadius: 7,
+        mouseOutTimerDelayMs: 120
+    };
+
+    // state variables
+    var netView = {},
+        network = {},
+        selected = {},
+        highlighted = null,
+        hovered = null,
+        viewMode = 'showAll',
+        portLabelsOn = false;
+
+
+    function debug(what) {
+        return config.debugOn && config.debug[what];
+    }
+
+    function urlData() {
+        return config.data[config.useLiveData ? 'live' : 'fake'];
+    }
+
+    function networkJsonUrl() {
+        return urlData().jsonUrl;
+    }
+
+    function safeId(id) {
+        return id.replace(/[^a-z0-9]/gi, '_');
+    }
+
+    function detailJsonUrl(id) {
+        var u = urlData(),
+            encId = config.useLiveData ? encodeURIComponent(id) : safeId(id);
+        return u.detailPrefix + encId + u.detailSuffix;
+    }
+
+
+    // load the topology view of the network
+    function loadNetworkView() {
+        // Hey, here I am, calling something on the ONOS api:
+        api.printTime();
+
+        resize();
+
+        // go get our network data from the server...
+        var url = networkJsonUrl();
+        d3.json(url , function (err, data) {
+            if (err) {
+                alert('Oops! Error reading JSON...\n\n' +
+                    'URL: ' + url + '\n\n' +
+                    'Error: ' + err.message);
+                return;
+            }
+//            console.log("here is the JSON data...");
+//            console.log(data);
+
+            network.data = data;
+            drawNetwork();
+        });
+
+        // while we wait for the data, set up the handlers...
+        setUpClickHandler();
+        setUpRadioButtonHandler();
+        setUpKeyHandler();
+        $(window).on('resize', resize);
+    }
+
+    function setUpClickHandler() {
+        // click handler for "selectable" objects
+        $(document).on('click', '.select-object', function () {
+            // when any object of class "select-object" is clicked...
+            var obj = network.lookup[$(this).data('id')];
+            if (obj) {
+                selectObject(obj);
+            }
+            // stop propagation of event (I think) ...
+            return false;
+        });
+    }
+
+    function setUpRadioButtonHandler() {
+        d3.selectAll('#displayModes .radio').on('click', function () {
+            var id = d3.select(this).attr('id');
+            if (id !== viewMode) {
+                radioButton('displayModes', id);
+                viewMode = id;
+                doRadioAction(id);
+            }
+        });
+    }
+
+    function doRadioAction(id) {
+        showAllLayers();
+        if (id === 'showPkt') {
+            showPacketLayer();
+        } else if (id === 'showOpt') {
+            showOpticalLayer();
+        }
+    }
+
+    function showAllLayers() {
+        network.node.classed('inactive', false);
+        network.link.classed('inactive', false);
+        d3.selectAll('svg .port').classed('inactive', false);
+        d3.selectAll('svg .portText').classed('inactive', false);
+    }
+
+    function showPacketLayer() {
+        network.node.each(function(d) {
+            // deactivate nodes that are not hosts or switches
+            if (d.class === 'device' && d.type !== 'switch') {
+                d3.select(this).classed('inactive', true);
+            }
+        });
+
+        network.link.each(function(lnk) {
+            // deactivate infrastructure links that have opt's as endpoints
+            if (lnk.source.type === 'roadm' || lnk.target.type === 'roadm') {
+                d3.select(this).classed('inactive', true);
+            }
+        });
+
+        // deactivate non-packet ports
+        d3.selectAll('svg .optPort').classed('inactive', true)
+    }
+
+    function showOpticalLayer() {
+        network.node.each(function(d) {
+            // deactivate nodes that are not optical devices
+            if (d.type !== 'roadm') {
+                d3.select(this).classed('inactive', true);
+            }
+        });
+
+        network.link.each(function(lnk) {
+            // deactivate infrastructure links that have opt's as endpoints
+            if (lnk.source.type !== 'roadm' || lnk.target.type !== 'roadm') {
+                d3.select(this).classed('inactive', true);
+            }
+        });
+
+        // deactivate non-packet ports
+        d3.selectAll('svg .pktPort').classed('inactive', true)
+    }
+
+    function setUpKeyHandler() {
+        d3.select('body')
+            .on('keydown', function () {
+                processKeyEvent();
+                if (debug('showKeyHandler')) {
+                    network.svg.append('text')
+                        .attr('x', 5)
+                        .attr('y', 15)
+                        .style('font-size', '20pt')
+                        .text('keyCode: ' + d3.event.keyCode +
+                            ' applied to : ' + contextLabel())
+                        .transition().duration(2000)
+                        .style('font-size', '2pt')
+                        .style('fill-opacity', 0.01)
+                        .remove();
+                }
+            });
+    }
+
+    function contextLabel() {
+        return hovered === null ? "(nothing)" : hovered.id;
+    }
+
+    function radioButton(group, id) {
+        d3.selectAll("#" + group + " .radio").classed("active", false);
+        d3.select("#" + group + " #" + id).classed("active", true);
+    }
+
+    function processKeyEvent() {
+        var code = d3.event.keyCode;
+        switch (code) {
+            case 66:    // B
+                toggleBackground();
+                break;
+            case 71:    // G
+                cycleLayout();
+                break;
+            case 76:    // L
+                cycleLabels();
+                break;
+            case 80:    // P
+                togglePorts();
+                break;
+            case 85:    // U
+                unpin();
+                break;
+        }
+
+    }
+
+    function toggleBackground() {
+        var bg = d3.select('#bg'),
+            vis = bg.style('visibility'),
+            newvis = (vis === 'hidden') ? 'visible' : 'hidden';
+        bg.style('visibility', newvis);
+    }
+
+    function cycleLayout() {
+        config.options.layering = !config.options.layering;
+        network.force.resume();
+    }
+
+    function cycleLabels() {
+        console.log('Cycle Labels - context = ' + contextLabel());
+    }
+
+    function togglePorts() {
+        portLabelsOn = !portLabelsOn;
+        var portVis = portLabelsOn ? 'visible' : 'hidden';
+        d3.selectAll('.port').style('visibility', portVis);
+        d3.selectAll('.portText').style('visibility', portVis);
+    }
+
+    function unpin() {
+        if (hovered) {
+            hovered.fixed = false;
+            findNodeFromData(hovered).classed('fixed', false);
+            network.force.resume();
+        }
+        console.log('Unpin - context = ' + contextLabel());
+    }
+
+
+    // ========================================================
+
+    function drawNetwork() {
+        $('#view').empty();
+
+        prepareNodesAndLinks();
+        createLayout();
+        console.log("\n\nHere is the augmented network object...");
+        console.log(network);
+    }
+
+    function prepareNodesAndLinks() {
+        network.lookup = {};
+        network.nodes = [];
+        network.links = [];
+
+        var nw = network.forceWidth,
+            nh = network.forceHeight;
+
+        function yPosConstraintForNode(n) {
+            return config.constraints.ypos[n.type || 'host'];
+        }
+
+        // Note that both 'devices' and 'hosts' get mapped into the nodes array
+
+        // first, the devices...
+        network.data.devices.forEach(function(n) {
+            var ypc = yPosConstraintForNode(n),
+                ix = Math.random() * 0.6 * nw + 0.2 * nw,
+                iy = ypc * nh,
+                node = {
+                    id: n.id,
+                    labels: n.labels,
+                    class: 'device',
+                    icon: 'device',
+                    type: n.type,
+                    x: ix,
+                    y: iy,
+                    constraint: {
+                        weight: 0.7,
+                        y: iy
+                    }
+                };
+            network.lookup[n.id] = node;
+            network.nodes.push(node);
+        });
+
+        // then, the hosts...
+        network.data.hosts.forEach(function(n) {
+            var ypc = yPosConstraintForNode(n),
+                ix = Math.random() * 0.6 * nw + 0.2 * nw,
+                iy = ypc * nh,
+                node = {
+                    id: n.id,
+                    labels: n.labels,
+                    class: 'host',
+                    icon: 'host',
+                    type: n.type,
+                    x: ix,
+                    y: iy,
+                    constraint: {
+                        weight: 0.7,
+                        y: iy
+                    }
+                };
+            network.lookup[n.id] = node;
+            network.nodes.push(node);
+        });
+
+
+        // now, process the explicit links...
+        network.data.links.forEach(function(lnk) {
+            var src = network.lookup[lnk.src],
+                dst = network.lookup[lnk.dst],
+                id = src.id + "-" + dst.id;
+
+            var link = {
+                class: 'infra',
+                id: id,
+                type: lnk.type,
+                width: lnk.linkWidth,
+                source: src,
+                srcPort: lnk.srcPort,
+                target: dst,
+                tgtPort: lnk.dstPort,
+                strength: config.force.linkStrength.infra
+            };
+            network.links.push(link);
+        });
+
+        // finally, infer host links...
+        network.data.hosts.forEach(function(n) {
+            var src = network.lookup[n.id],
+                dst = network.lookup[n.cp.device],
+                id = src.id + "-" + dst.id;
+
+            var link = {
+                class: 'host',
+                id: id,
+                type: 'hostLink',
+                width: config.hostLinkWidth,
+                source: src,
+                target: dst,
+                strength: config.force.linkStrength.host
+            };
+            network.links.push(link);
+        });
+    }
+
+    function createLayout() {
+
+        var cfg = config.force;
+
+        network.force = d3.layout.force()
+            .size([network.forceWidth, network.forceHeight])
+            .nodes(network.nodes)
+            .links(network.links)
+            .linkStrength(function(d) { return cfg.linkStrength[d.class]; })
+            .linkDistance(function(d) { return cfg.linkDistance[d.class]; })
+            .charge(function(d) { return cfg.charge[d.class]; })
+            .on('tick', tick);
+
+        network.svg = d3.select('#view').append('svg')
+            .attr('width', netView.width)
+            .attr('height', netView.height)
+            .append('g')
+            .attr('transform', config.force.translate());
+//            .attr('id', 'zoomable')
+//            .call(d3.behavior.zoom().on("zoom", zoomRedraw));
+
+        network.svg.append('svg:image')
+            .attr({
+                id: 'bg',
+                width: netView.width,
+                height: netView.height,
+                'xlink:href': config.backgroundUrl
+            })
+            .style('visibility',
+                    config.options.loadBackground ? 'visible' : 'hidden');
+
+//        function zoomRedraw() {
+//            d3.select("#zoomable").attr("transform",
+//                    "translate(" + d3.event.translate + ")"
+//                    + " scale(" + d3.event.scale + ")");
+//        }
+
+        // TODO: move glow/blur stuff to util script
+        var glow = network.svg.append('filter')
+            .attr('x', '-50%')
+            .attr('y', '-50%')
+            .attr('width', '200%')
+            .attr('height', '200%')
+            .attr('id', 'blue-glow');
+
+        glow.append('feColorMatrix')
+            .attr('type', 'matrix')
+            .attr('values', '0 0 0 0  0 ' +
+                '0 0 0 0  0 ' +
+                '0 0 0 0  .7 ' +
+                '0 0 0 1  0 ');
+
+        glow.append('feGaussianBlur')
+            .attr('stdDeviation', 3)
+            .attr('result', 'coloredBlur');
+
+        glow.append('feMerge').selectAll('feMergeNode')
+            .data(['coloredBlur', 'SourceGraphic'])
+            .enter().append('feMergeNode')
+            .attr('in', String);
+
+        // TODO: legend (and auto adjust on scroll)
+//        $('#view').on('scroll', function() {
+//
+//        });
+
+
+        // TODO: move drag behavior into separate method.
+        // == define node drag behavior...
+        network.draggedThreshold = d3.scale.linear()
+            .domain([0, 0.1])
+            .range([5, 20])
+            .clamp(true);
+
+        function dragged(d) {
+            var threshold = network.draggedThreshold(network.force.alpha()),
+                dx = d.oldX - d.px,
+                dy = d.oldY - d.py;
+            if (Math.abs(dx) >= threshold || Math.abs(dy) >= threshold) {
+                d.dragged = true;
+            }
+            return d.dragged;
+        }
+
+        network.drag = d3.behavior.drag()
+            .origin(function(d) { return d; })
+            .on('dragstart', function(d) {
+                d.oldX = d.x;
+                d.oldY = d.y;
+                d.dragged = false;
+                d.fixed |= 2;
+            })
+            .on('drag', function(d) {
+                d.px = d3.event.x;
+                d.py = d3.event.y;
+                if (dragged(d)) {
+                    if (!network.force.alpha()) {
+                        network.force.alpha(.025);
+                    }
+                }
+            })
+            .on('dragend', function(d) {
+                if (!dragged(d)) {
+                    selectObject(d, this);
+                }
+                d.fixed &= ~6;
+
+                // once we've finished moving, pin the node in position,
+                // if it is a device (not a host)
+                if (d.class === 'device') {
+                    d.fixed = true;
+                    d3.select(this).classed('fixed', true)
+                }
+            });
+
+        $('#view').on('click', function(e) {
+            if (!$(e.target).closest('.node').length) {
+                deselectObject();
+            }
+        });
+
+        // ...............................................................
+
+        // add links to the display
+        network.link = network.svg.append('g').attr('id', 'links')
+            .selectAll('.link')
+            .data(network.force.links(), function(d) {return d.id})
+            .enter().append('line')
+            .attr('class', function(d) {return 'link ' + d.class});
+
+        network.linkSrcPort = network.svg.append('g')
+            .attr({
+                id: 'srcPorts',
+                class: 'portLayer'
+            });
+        network.linkTgtPort = network.svg.append('g')
+            .attr({
+                id: 'tgtPorts',
+                class: 'portLayer'
+            });
+
+        var portVis = portLabelsOn ? 'visible' : 'hidden',
+            pw = config.labels.port.width,
+            ph = config.labels.port.height;
+
+        network.link.filter('.infra').each(function(d) {
+            var srcType = d.source.type === 'roadm' ? 'optPort' : 'pktPort',
+                tgtType = d.target.type === 'roadm' ? 'optPort' : 'pktPort';
+
+            if (d.source.type)
+
+            network.linkSrcPort.append('rect').attr({
+                id: 'srcPort-' + safeId(d.id),
+                class: 'port ' + srcType,
+                width: pw,
+                height: ph,
+                rx: 4,
+                ry: 4
+            }).style('visibility', portVis);
+
+            network.linkTgtPort.append('rect').attr({
+                id: 'tgtPort-' + safeId(d.id),
+                class: 'port ' + tgtType,
+                width: pw,
+                height: ph,
+                rx: 4,
+                ry: 4
+            }).style('visibility', portVis);
+
+            network.linkSrcPort.append('text').attr({
+                id: 'srcText-' + safeId(d.id),
+                class: 'portText ' + srcType
+            }).text(d.srcPort)
+                .style('visibility', portVis);
+
+            network.linkTgtPort.append('text').attr({
+                id: 'tgtText-' + safeId(d.id),
+                class: 'portText ' + tgtType
+            }).text(d.tgtPort)
+                .style('visibility', portVis);
+        });
+
+        // ...............................................................
+
+        // add nodes to the display
+        network.node = network.svg.selectAll('.node')
+            .data(network.force.nodes(), function(d) {return d.id})
+            .enter().append('g')
+            .attr('class', function(d) {
+                var cls = 'node ' + d.class;
+                if (d.type) {
+                    cls += ' ' + d.type;
+                }
+                return cls;
+            })
+            .attr('transform', function(d) {
+                return translate(d.x, d.y);
+            })
+            .call(network.drag)
+            .on('mouseover', function(d) {
+                // TODO: show tooltip
+                if (network.mouseoutTimeout) {
+                    clearTimeout(network.mouseoutTimeout);
+                    network.mouseoutTimeout = null;
+                }
+                hoverObject(d);
+            })
+            .on('mouseout', function(d) {
+                // TODO: hide tooltip
+                if (network.mouseoutTimeout) {
+                    clearTimeout(network.mouseoutTimeout);
+                    network.mouseoutTimeout = null;
+                }
+                network.mouseoutTimeout = setTimeout(function() {
+                    hoverObject(null);
+                }, config.mouseOutTimerDelayMs);
+            });
+
+
+        // deal with device nodes first
+        network.nodeRect = network.node.filter('.device')
+            .append('rect')
+            .attr({
+                rx: 5,
+                ry: 5,
+                width: 100,
+                height: 12
+            });
+            // note that width/height are adjusted to fit the label text
+            // then padded, and space made for the icon.
+
+        network.node.filter('.device').each(function(d) {
+            var node = d3.select(this),
+                icon = iconUrl(d);
+
+            node.append('text')
+            // TODO: add label cycle behavior
+                .text(d.id)
+                .attr('dy', '1.1em');
+
+            if (icon) {
+                var cfg = config.icons;
+                node.append('svg:image')
+                    .attr({
+                        width: cfg.w,
+                        height: cfg.h,
+                        'xlink:href': icon
+                    });
+                // note, icon relative positioning (x,y) is done after we have
+                // adjusted the bounds of the rectangle...
+            }
+
+            // debug function to show the modelled x,y coordinates of nodes...
+            if (debug('showNodeXY')) {
+                node.select('rect').attr('fill-opacity', 0.5);
+                node.append('circle')
+                    .attr({
+                        class: 'debug',
+                        cx: 0,
+                        cy: 0,
+                        r: '3px'
+                    });
+            }
+        });
+
+        // now process host nodes
+        network.nodeCircle = network.node.filter('.host')
+            .append('circle')
+            .attr({
+                r: config.hostRadius
+            });
+
+        network.node.filter('.host').each(function(d) {
+            var node = d3.select(this),
+                icon = iconUrl(d);
+
+            // debug function to show the modelled x,y coordinates of nodes...
+            if (debug('showNodeXY')) {
+                node.select('circle').attr('fill-opacity', 0.5);
+                node.append('circle')
+                    .attr({
+                        class: 'debug',
+                        cx: 0,
+                        cy: 0,
+                        r: '3px'
+                    });
+            }
+        });
+
+        // this function is scheduled to happen soon after the given thread ends
+        setTimeout(function() {
+            var lab = config.labels,
+                portGap = lab.port.gap,
+                midW = portGap + lab.port.width/ 2,
+                midH = portGap + lab.port.height / 2;
+
+            // post process the device nodes, to pad their size to fit the
+            // label text and attach the icon to the right location.
+            network.node.filter('.device').each(function(d) {
+                // for every node, recompute size, padding, etc. so text fits
+                var node = d3.select(this),
+                    text = node.select('text'),
+                    box = adjustRectToFitText(node);
+
+                // now make the computed adjustment
+                node.select('rect')
+                    .attr(box);
+
+                node.select('image')
+                    .attr('x', box.x + config.icons.xoff)
+                    .attr('y', box.y + config.icons.yoff);
+
+                var bounds = boundsFromBox(box),
+                    portBounds = {
+                        x1: bounds.x1 - midW,
+                        x2: bounds.x2 + midW,
+                        y1: bounds.y1 - midH,
+                        y2: bounds.y2 + midH
+                    };
+
+                // todo: clean up extent and edge work..
+                d.extent = {
+                    left: bounds.x1 - lab.marginLR,
+                    right: bounds.x2 + lab.marginLR,
+                    top: bounds.y1 - lab.marginTB,
+                    bottom: bounds.y2 + lab.marginTB
+                };
+
+                d.edge = {
+                    left   : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x1, bounds.y2),
+                    right  : new geo.LineSegment(bounds.x2, bounds.y1, bounds.x2, bounds.y2),
+                    top    : new geo.LineSegment(bounds.x1, bounds.y1, bounds.x2, bounds.y1),
+                    bottom : new geo.LineSegment(bounds.x1, bounds.y2, bounds.x2, bounds.y2)
+                };
+
+                d.portEdge = {
+                    left   : new geo.LineSegment(
+                        portBounds.x1, portBounds.y1, portBounds.x1, portBounds.y2
+                    ),
+                    right  : new geo.LineSegment(
+                        portBounds.x2, portBounds.y1, portBounds.x2, portBounds.y2
+                    ),
+                    top    : new geo.LineSegment(
+                        portBounds.x1, portBounds.y1, portBounds.x2, portBounds.y1
+                    ),
+                    bottom : new geo.LineSegment(
+                        portBounds.x1, portBounds.y2, portBounds.x2, portBounds.y2
+                    )
+                };
+
+            });
+
+            network.numTicks = 0;
+            network.preventCollisions = false;
+            network.force.start();
+            for (var i = 0; i < config.force.ticksWithoutCollisions; i++) {
+                network.force.tick();
+            }
+            network.preventCollisions = true;
+            $('#view').css('visibility', 'visible');
+        });
+
+
+        // returns the newly computed bounding box of the rectangle
+        function adjustRectToFitText(n) {
+            var text = n.select('text'),
+                box = text.node().getBBox(),
+                lab = config.labels;
+
+            // not sure why n.data() returns an array of 1 element...
+            var data = n.data()[0];
+
+            text.attr('text-anchor', 'middle')
+                .attr('y', '-0.8em')
+                .attr('x', lab.imgPad/2)
+            ;
+
+            // translate the bbox so that it is centered on [x,y]
+            box.x = -box.width / 2;
+            box.y = -box.height / 2;
+
+            // add padding
+            box.x -= (lab.padLR + lab.imgPad/2);
+            box.width += lab.padLR * 2 + lab.imgPad;
+            box.y -= lab.padTB;
+            box.height += lab.padTB * 2;
+
+            return box;
+        }
+
+        function boundsFromBox(box) {
+            return {
+                x1: box.x,
+                y1: box.y,
+                x2: box.x + box.width,
+                y2: box.y + box.height
+            };
+        }
+
+    }
+
+    function iconUrl(d) {
+        return 'img/' + d.type + '.png';
+//        return config.iconUrl[d.icon];
+    }
+
+    function translate(x, y) {
+        return 'translate(' + x + ',' + y + ')';
+    }
+
+    // prevents collisions amongst device nodes
+    function preventCollisions() {
+        var quadtree = d3.geom.quadtree(network.nodes),
+            hrad = config.hostRadius;
+
+        network.nodes.forEach(function(n) {
+            var nx1, nx2, ny1, ny2;
+
+            if (n.class === 'device') {
+                nx1 = n.x + n.extent.left;
+                nx2 = n.x + n.extent.right;
+                ny1 = n.y + n.extent.top;
+                ny2 = n.y + n.extent.bottom;
+
+            } else {
+                nx1 = n.x - hrad;
+                nx2 = n.x + hrad;
+                ny1 = n.y - hrad;
+                ny2 = n.y + hrad;
+            }
+
+            quadtree.visit(function(quad, x1, y1, x2, y2) {
+                if (quad.point && quad.point !== n) {
+                    // check if the rectangles/circles intersect
+                    var p = quad.point,
+                        px1, px2, py1, py2, ix;
+
+                    if (p.class === 'device') {
+                        px1 = p.x + p.extent.left;
+                        px2 = p.x + p.extent.right;
+                        py1 = p.y + p.extent.top;
+                        py2 = p.y + p.extent.bottom;
+
+                    } else {
+                        px1 = p.x - hrad;
+                        px2 = p.x + hrad;
+                        py1 = p.y - hrad;
+                        py2 = p.y + hrad;
+                    }
+
+                    ix = (px1 <= nx2 && nx1 <= px2 && py1 <= ny2 && ny1 <= py2);
+
+                    if (ix) {
+                        var xa1 = nx2 - px1, // shift n left , p right
+                            xa2 = px2 - nx1, // shift n right, p left
+                            ya1 = ny2 - py1, // shift n up   , p down
+                            ya2 = py2 - ny1, // shift n down , p up
+                            adj = Math.min(xa1, xa2, ya1, ya2);
+
+                        if (adj == xa1) {
+                            n.x -= adj / 2;
+                            p.x += adj / 2;
+                        } else if (adj == xa2) {
+                            n.x += adj / 2;
+                            p.x -= adj / 2;
+                        } else if (adj == ya1) {
+                            n.y -= adj / 2;
+                            p.y += adj / 2;
+                        } else if (adj == ya2) {
+                            n.y += adj / 2;
+                            p.y -= adj / 2;
+                        }
+                    }
+                    return ix;
+                }
+            });
+
+        });
+    }
+
+    function tick(e) {
+        network.numTicks++;
+
+        if (config.options.layering) {
+            // adjust the y-coord of each node, based on y-pos constraints
+            network.nodes.forEach(function (n) {
+                var z = e.alpha * n.constraint.weight;
+                if (!isNaN(n.constraint.y)) {
+                    n.y = (n.constraint.y * z + n.y * (1 - z));
+                }
+            });
+        }
+
+        if (config.options.collisionPrevention && network.preventCollisions) {
+            preventCollisions();
+        }
+
+        var portHalfW = config.labels.port.width / 2,
+            portHalfH = config.labels.port.height / 2;
+
+        // clip visualization of links at bounds of nodes...
+        network.link.each(function(d) {
+            var xs = d.source.x,
+                ys = d.source.y,
+                xt = d.target.x,
+                yt = d.target.y,
+                line = new geo.LineSegment(xs, ys, xt, yt),
+                e, ix,
+                exs, eys, ext, eyt,
+                pxs, pys, pxt, pyt;
+
+            if (d.class === 'host') {
+                // no adjustment for source end of link, since hosts are dots
+                exs = xs;
+                eys = ys;
+
+            } else {
+                for (e in d.source.edge) {
+                    ix = line.intersect(d.source.edge[e].offset(xs, ys));
+                    if (ix.in1 && ix.in2) {
+                        exs = ix.x;
+                        eys = ix.y;
+
+                        // also pick off the port label intersection
+                        ix = line.intersect(d.source.portEdge[e].offset(xs, ys));
+                        pxs = ix.x;
+                        pys = ix.y;
+                        break;
+                    }
+                }
+            }
+
+            for (e in d.target.edge) {
+                ix = line.intersect(d.target.edge[e].offset(xt, yt));
+                if (ix.in1 && ix.in2) {
+                    ext = ix.x;
+                    eyt = ix.y;
+
+                    // also pick off the port label intersection
+                    ix = line.intersect(d.target.portEdge[e].offset(xt, yt));
+                    pxt = ix.x;
+                    pyt = ix.y;
+                    break;
+                }
+            }
+
+            // adjust the endpoints of the link's line to match rectangles
+            var sid = safeId(d.id);
+            d3.select(this)
+                .attr('x1', exs)
+                .attr('y1', eys)
+                .attr('x2', ext)
+                .attr('y2', eyt);
+
+            d3.select('#srcPort-' + sid)
+                .attr('x', pxs - portHalfW)
+                .attr('y', pys - portHalfH);
+
+            d3.select('#tgtPort-' + sid)
+                .attr('x', pxt - portHalfW)
+                .attr('y', pyt - portHalfH);
+
+            // TODO: fit label rect to size of port number.
+            d3.select('#srcText-' + sid)
+                .attr('x', pxs - 5)
+                .attr('y', pys + 3);
+
+            d3.select('#tgtText-' + sid)
+                .attr('x', pxt - 5)
+                .attr('y', pyt + 3);
+
+        });
+
+        // position each node by translating the node (group) by x,y
+        network.node
+            .attr('transform', function(d) {
+                return translate(d.x, d.y);
+            });
+
+    }
+
+    //    $('#docs-close').on('click', function() {
+    //        deselectObject();
+    //        return false;
+    //    });
+
+    //    $(document).on('click', '.select-object', function() {
+    //        var obj = graph.data[$(this).data('name')];
+    //        if (obj) {
+    //            selectObject(obj);
+    //        }
+    //        return false;
+    //    });
+
+    function findNodeFromData(d) {
+        var el = null;
+        network.node.filter('.' + d.class).each(function(n) {
+            if (n.id === d.id) {
+                el = d3.select(this);
+            }
+        });
+        return el;
+    }
+
+    function selectObject(obj, el) {
+        var node;
+        if (el) {
+            node = d3.select(el);
+        } else {
+            network.node.each(function(d) {
+                if (d == obj) {
+                    node = d3.select(el = this);
+                }
+            });
+        }
+        if (!node) return;
+
+        if (node.classed('selected')) {
+            deselectObject();
+            flyinPane(null);
+            return;
+        }
+        deselectObject(false);
+
+        selected = {
+            obj : obj,
+            el  : el
+        };
+
+        node.classed('selected', true);
+        flyinPane(obj);
+    }
+
+    function deselectObject(doResize) {
+        // Review: logic of 'resize(...)' function.
+        if (doResize || typeof doResize == 'undefined') {
+            resize(false);
+        }
+
+        // deselect all nodes in the network...
+        network.node.classed('selected', false);
+        selected = {};
+        flyinPane(null);
+    }
+
+    function flyinPane(obj) {
+        var pane = d3.select('#flyout'),
+            url;
+
+        if (obj) {
+            // go get details of the selected object from the server...
+            url = detailJsonUrl(obj.id);
+            d3.json(url, function (err, data) {
+                if (err) {
+                    alert('Oops! Error reading JSON...\n\n' +
+                        'URL: ' + url + '\n\n' +
+                        'Error: ' + err.message);
+                    return;
+                }
+//                console.log("JSON data... " + url);
+//                console.log(data);
+
+                displayDetails(data, pane);
+            });
+
+        } else {
+            // hide pane
+            pane.transition().duration(750)
+                .style('right', '-320px')
+                .style('opacity', 0.0);
+        }
+    }
+
+    function displayDetails(data, pane) {
+        $('#flyout').empty();
+
+        var title = pane.append("h2"),
+            table = pane.append("table"),
+            tbody = table.append("tbody");
+
+        $('<img src="img/' + data.type + '.png">').appendTo(title);
+        $('<span>').attr('class', 'icon').text(data.id).appendTo(title);
+
+
+        // TODO: consider using d3 data bind to TR/TD
+
+        data.propOrder.forEach(function(p) {
+            if (p === '-') {
+                addSep(tbody);
+            } else {
+                addProp(tbody, p, data.props[p]);
+            }
+        });
+
+        function addSep(tbody) {
+            var tr = tbody.append('tr');
+            $('<hr>').appendTo(tr.append('td').attr('colspan', 2));
+        }
+
+        function addProp(tbody, label, value) {
+            var tr = tbody.append('tr');
+
+            tr.append('td')
+                .attr('class', 'label')
+                .text(label + ' :');
+
+            tr.append('td')
+                .attr('class', 'value')
+                .text(value);
+        }
+
+        // show pane
+        pane.transition().duration(750)
+            .style('right', '20px')
+            .style('opacity', 1.0);
+    }
+
+    function highlightObject(obj) {
+        if (obj) {
+            if (obj != highlighted) {
+                // TODO set or clear "inactive" class on nodes, based on criteria
+                network.node.classed('inactive', function(d) {
+                    //                return (obj !== d &&
+                    //                    d.relation(obj.id));
+                    return (obj !== d);
+                });
+                // TODO: same with links
+                network.link.classed('inactive', function(d) {
+                    return (obj !== d.source && obj !== d.target);
+                });
+            }
+            highlighted = obj;
+        } else {
+            if (highlighted) {
+                // clear the inactive flag (no longer suppressed visually)
+                network.node.classed('inactive', false);
+                network.link.classed('inactive', false);
+            }
+            highlighted = null;
+
+        }
+    }
+
+    function hoverObject(obj) {
+        if (obj) {
+            hovered = obj;
+        } else {
+            if (hovered) {
+                hovered = null;
+            }
+        }
+    }
+
+
+    function resize() {
+        netView.height = window.innerHeight - config.mastHeight;
+        netView.width = window.innerWidth;
+        $('#view')
+            .css('height', netView.height + 'px')
+            .css('width', netView.width + 'px');
+
+        network.forceWidth = netView.width - config.force.marginLR;
+        network.forceHeight = netView.height - config.force.marginTB;
+    }
+
+    // ======================================================================
+    // register with the UI framework
+
+    onos.ui.addView('topo', {
+        load: loadNetworkView
+    });
+
+
+}(ONOS));
+
diff --git a/web/gui/src/main/webapp/module-template.js b/web/gui/src/main/webapp/topo2.css
similarity index 71%
rename from web/gui/src/main/webapp/module-template.js
rename to web/gui/src/main/webapp/topo2.css
index 3de7d79..88fcd94 100644
--- a/web/gui/src/main/webapp/module-template.js
+++ b/web/gui/src/main/webapp/topo2.css
@@ -15,22 +15,12 @@
  */
 
 /*
- Module template file.
+ ONOS GUI -- Topology view -- CSS file
 
  @author Simon Hunt
  */
 
-(function (onos) {
-    'use strict';
+svg #topo-bg {
+    opacity: 0.5;
+}
 
-    var api = onos.api;
-
-    // == define your functions here.....
-
-
-    // == register views here, with links to lifecycle callbacks
-
-//    api.addView('view-id', {/* callbacks */});
-
-
-}(ONOS));
diff --git a/web/gui/src/main/webapp/topo2.js b/web/gui/src/main/webapp/topo2.js
new file mode 100644
index 0000000..fcc4bb4
--- /dev/null
+++ b/web/gui/src/main/webapp/topo2.js
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2014 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.
+ */
+
+/*
+ ONOS network topology viewer - version 1.1
+
+ @author Simon Hunt
+ */
+
+(function (onos) {
+    'use strict';
+
+    // configuration data
+    var config = {
+        useLiveData: false,
+        debugOn: false,
+        debug: {
+            showNodeXY: false,
+            showKeyHandler: true
+        },
+        options: {
+            layering: true,
+            collisionPrevention: true,
+            showBackground: true
+        },
+        backgroundUrl: 'img/us-map.png',
+        data: {
+            live: {
+                jsonUrl: 'rs/topology/graph',
+                detailPrefix: 'rs/topology/graph/',
+                detailSuffix: ''
+            },
+            fake: {
+                jsonUrl: 'json/network2.json',
+                detailPrefix: 'json/',
+                detailSuffix: '.json'
+            }
+        },
+        iconUrl: {
+            device: 'img/device.png',
+            host: 'img/host.png',
+            pkt: 'img/pkt.png',
+            opt: 'img/opt.png'
+        },
+        force: {
+            marginLR: 20,
+            marginTB: 20,
+            translate: function() {
+                return 'translate(' +
+                    config.force.marginLR + ',' +
+                    config.force.marginTB + ')';
+            }
+        }
+    };
+
+    // radio buttons
+    var btnSet = [
+            { id: 'showAll', text: 'All Layers' },
+            { id: 'showPkt', text: 'Packet Only' },
+            { id: 'showOpt', text: 'Optical Only' }
+        ];
+
+    // state variables
+    var svg,
+        bgImg,
+        network = {},
+        selected = {},
+        highlighted = null,
+        hovered = null,
+        viewMode = 'showAll',
+        portLabelsOn = false;
+
+
+    // ==============================
+    // Private functions
+
+    // set the size of the SVG layer (or other element) to that of the view
+    function setSize(view, el) {
+        var thing = el || svg;
+        thing.attr({
+            width: view.width(),
+            height: view.height()
+        });
+    }
+
+    function doRadio(view, id) {
+        showAllLayers();
+        if (id === 'showPkt') {
+            showPacketLayer();
+        } else if (id === 'showOpt') {
+            showOpticalLayer();
+        }
+    }
+
+    function showAllLayers() {
+//        network.node.classed('inactive', false);
+//        network.link.classed('inactive', false);
+//        d3.selectAll('svg .port').classed('inactive', false);
+//        d3.selectAll('svg .portText').classed('inactive', false);
+        alert('show all layers');
+    }
+
+    function showPacketLayer() {
+        alert('show packet layer');
+    }
+
+    function showOpticalLayer() {
+        alert('show optical layer');
+    }
+
+    // ==============================
+    // View life-cycle callbacks
+
+    function preload(view, ctx) {
+        var w = view.width(),
+            h = view.height(),
+            idBg = view.uid('bg'),
+            showBg = config.options.showBackground ? 'visible' : 'hidden';
+
+        // NOTE: view.$div is a D3 selection of the view's div
+        svg = view.$div.append('svg');
+        setSize(view);
+        svg.append('g')
+            .attr('transform', config.force.translate());
+
+        // load the background image
+        bgImg = svg.append('svg:image')
+            .attr({
+                id: idBg,
+                width: w,
+                height: h,
+                'xlink:href': config.backgroundUrl
+            })
+            .style({
+                visibility: showBg
+            });
+    }
+
+
+    function load(view, ctx) {
+        view.setRadio(btnSet, doRadio);
+
+    }
+
+    function resize(view, ctx) {
+        setSize(view);
+        setSize(view, bgImg);
+    }
+
+
+    // ==============================
+    // View registration
+
+    onos.ui.addView('topo', {
+        preload: preload,
+        load: load,
+        resize: resize
+    });
+
+}(ONOS));