Merge branch 'optical_path_provisioner'

Conflicts:
	apps/optical/src/main/resources/demo-3-roadm-2-ps.json

Change-Id: I6fb95eb8de5b8331678c8cdac621309ea4d2d2b8
diff --git a/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetrics.java b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetrics.java
index 38366b6..cfc88b8 100644
--- a/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetrics.java
+++ b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetrics.java
@@ -5,8 +5,6 @@
 import java.util.LinkedList;
 import java.util.List;
 
-import com.codahale.metrics.Gauge;
-import com.codahale.metrics.Meter;
 import com.google.common.collect.ImmutableList;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
@@ -14,8 +12,7 @@
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
-import org.onlab.metrics.MetricsComponent;
-import org.onlab.metrics.MetricsFeature;
+import org.onlab.metrics.EventMetric;
 import org.onlab.metrics.MetricsService;
 import org.onlab.onos.net.intent.IntentEvent;
 import org.onlab.onos.net.intent.IntentListener;
@@ -33,56 +30,32 @@
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected IntentService intentService;
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MetricsService metricsService;
+
     private LinkedList<IntentEvent> lastEvents = new LinkedList<>();
     private static final int LAST_EVENTS_MAX_N = 100;
 
     //
     // Metrics
     //
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected MetricsService metricsService;
-    //
     private static final String COMPONENT_NAME = "Intent";
     private static final String FEATURE_SUBMITTED_NAME = "Submitted";
     private static final String FEATURE_INSTALLED_NAME = "Installed";
     private static final String FEATURE_WITHDRAW_REQUESTED_NAME =
         "WithdrawRequested";
     private static final String FEATURE_WITHDRAWN_NAME = "Withdrawn";
-    private static final String GAUGE_TIMESTAMP_NAME = "Timestamp.EpochMs";
-    private static final String METER_RATE_NAME = "Rate";
     //
-    private MetricsComponent metricsComponent;
-    private MetricsFeature metricsFeatureSubmitted;
-    private MetricsFeature metricsFeatureInstalled;
-    private MetricsFeature metricsFeatureWithdrawRequested;
-    private MetricsFeature metricsFeatureWithdrawn;
+    // Event metrics:
+    //  - Intent Submitted API operation
+    //  - Intent Installed operation completion
+    //  - Intent Withdraw Requested API operation
+    //  - Intent Withdrawn operation completion
     //
-    // Timestamps:
-    //  - Intent Submitted API operation (ms from the Epoch)
-    //  - Intent Installed operation completion (ms from the Epoch)
-    //  - Intent Withdraw Requested API operation (ms from the Epoch)
-    //  - Intent Withdrawn operation completion (ms from the Epoch)
-    //
-    private volatile long intentSubmittedTimestampEpochMs = 0;
-    private volatile long intentInstalledTimestampEpochMs = 0;
-    private volatile long intentWithdrawRequestedTimestampEpochMs = 0;
-    private volatile long intentWithdrawnTimestampEpochMs = 0;
-    //
-    private Gauge<Long> intentSubmittedTimestampEpochMsGauge;
-    private Gauge<Long> intentInstalledTimestampEpochMsGauge;
-    private Gauge<Long> intentWithdrawRequestedTimestampEpochMsGauge;
-    private Gauge<Long> intentWithdrawnTimestampEpochMsGauge;
-    //
-    // Rate meters:
-    //  - Rate of the Submitted Intent API operations
-    //  - Rate of the Installed Intent operations
-    //  - Rate of the Withdrawn Requested Intent API operations
-    //  - Rate of the Withdrawn Intent operations
-    //
-    private Meter intentSubmittedRateMeter;
-    private Meter intentInstalledRateMeter;
-    private Meter intentWithdrawRequestedRateMeter;
-    private Meter intentWithdrawnRateMeter;
+    private EventMetric intentSubmittedEventMetric;
+    private EventMetric intentInstalledEventMetric;
+    private EventMetric intentWithdrawRequestedEventMetric;
+    private EventMetric intentWithdrawnEventMetric;
 
     @Activate
     protected void activate() {
@@ -108,43 +81,23 @@
     }
 
     @Override
-    public Gauge<Long> intentSubmittedTimestampEpochMsGauge() {
-        return intentSubmittedTimestampEpochMsGauge;
+    public EventMetric intentSubmittedEventMetric() {
+        return intentSubmittedEventMetric;
     }
 
     @Override
-    public Gauge<Long> intentInstalledTimestampEpochMsGauge() {
-        return intentInstalledTimestampEpochMsGauge;
+    public EventMetric intentInstalledEventMetric() {
+        return intentInstalledEventMetric;
     }
 
     @Override
-    public Gauge<Long> intentWithdrawRequestedTimestampEpochMsGauge() {
-        return intentWithdrawRequestedTimestampEpochMsGauge;
+    public EventMetric intentWithdrawRequestedEventMetric() {
+        return intentWithdrawRequestedEventMetric;
     }
 
     @Override
-    public Gauge<Long> intentWithdrawnTimestampEpochMsGauge() {
-        return intentWithdrawnTimestampEpochMsGauge;
-    }
-
-    @Override
-    public Meter intentSubmittedRateMeter() {
-        return intentSubmittedRateMeter;
-    }
-
-    @Override
-    public Meter intentInstalledRateMeter() {
-        return intentInstalledRateMeter;
-    }
-
-    @Override
-    public Meter intentWithdrawRequestedRateMeter() {
-        return intentWithdrawRequestedRateMeter;
-    }
-
-    @Override
-    public Meter intentWithdrawnRateMeter() {
-        return intentWithdrawnRateMeter;
+    public EventMetric intentWithdrawnEventMetric() {
+        return intentWithdrawnEventMetric;
     }
 
     @Override
@@ -156,26 +109,21 @@
             //
             switch (event.type()) {
             case SUBMITTED:
-                intentSubmittedTimestampEpochMs = System.currentTimeMillis();
-                intentSubmittedRateMeter.mark(1);
+                intentSubmittedEventMetric.eventReceived();
                 break;
             case INSTALLED:
-                intentInstalledTimestampEpochMs = System.currentTimeMillis();
-                intentInstalledRateMeter.mark(1);
+                intentInstalledEventMetric.eventReceived();
                 break;
             case FAILED:
                 // TODO: Just ignore?
                 break;
                 /*
             case WITHDRAW_REQUESTED:
-                intentWithdrawRequestedTimestampEpochMs =
-                    System.currentTimeMillis();
-                    intentWithdrawRequestedRateMeter.mark(1);
+                intentWithdrawRequestedEventMetric.eventReceived();
                 break;
                 */
             case WITHDRAWN:
-                intentWithdrawnTimestampEpochMs = System.currentTimeMillis();
-                intentWithdrawnRateMeter.mark(1);
+                intentWithdrawnEventMetric.eventReceived();
                 break;
             default:
                 break;
@@ -199,10 +147,6 @@
      */
     private void clear() {
         synchronized (lastEvents) {
-            intentSubmittedTimestampEpochMs = 0;
-            intentInstalledTimestampEpochMs = 0;
-            intentWithdrawRequestedTimestampEpochMs = 0;
-            intentWithdrawnTimestampEpochMs = 0;
             lastEvents.clear();
         }
     }
@@ -211,109 +155,32 @@
      * Registers the metrics.
      */
     private void registerMetrics() {
-        metricsComponent = metricsService.registerComponent(COMPONENT_NAME);
-        //
-        metricsFeatureSubmitted =
-            metricsComponent.registerFeature(FEATURE_SUBMITTED_NAME);
-        metricsFeatureInstalled =
-            metricsComponent.registerFeature(FEATURE_INSTALLED_NAME);
-        metricsFeatureWithdrawRequested =
-            metricsComponent.registerFeature(FEATURE_WITHDRAW_REQUESTED_NAME);
-        metricsFeatureWithdrawn =
-            metricsComponent.registerFeature(FEATURE_WITHDRAWN_NAME);
-        //
-        intentSubmittedTimestampEpochMsGauge =
-            metricsService.registerMetric(metricsComponent,
-                                          metricsFeatureSubmitted,
-                                          GAUGE_TIMESTAMP_NAME,
-                                          new Gauge<Long>() {
-                                              @Override
-                                              public Long getValue() {
-                                                  return intentSubmittedTimestampEpochMs;
-                                              }
-                                          });
-        //
-        intentInstalledTimestampEpochMsGauge =
-            metricsService.registerMetric(metricsComponent,
-                                          metricsFeatureInstalled,
-                                          GAUGE_TIMESTAMP_NAME,
-                                          new Gauge<Long>() {
-                                              @Override
-                                              public Long getValue() {
-                                                  return intentInstalledTimestampEpochMs;
-                                              }
-                                          });
-        //
-        intentWithdrawRequestedTimestampEpochMsGauge =
-            metricsService.registerMetric(metricsComponent,
-                                          metricsFeatureWithdrawRequested,
-                                          GAUGE_TIMESTAMP_NAME,
-                                          new Gauge<Long>() {
-                                              @Override
-                                              public Long getValue() {
-                                                  return intentWithdrawRequestedTimestampEpochMs;
-                                              }
-                                          });
-        //
-        intentWithdrawnTimestampEpochMsGauge =
-            metricsService.registerMetric(metricsComponent,
-                                          metricsFeatureWithdrawn,
-                                          GAUGE_TIMESTAMP_NAME,
-                                          new Gauge<Long>() {
-                                              @Override
-                                              public Long getValue() {
-                                                  return intentWithdrawnTimestampEpochMs;
-                                              }
-                                          });
-        //
-        intentSubmittedRateMeter =
-            metricsService.createMeter(metricsComponent,
-                                       metricsFeatureSubmitted,
-                                       METER_RATE_NAME);
-        //
-        intentInstalledRateMeter =
-            metricsService.createMeter(metricsComponent,
-                                       metricsFeatureInstalled,
-                                       METER_RATE_NAME);
-        //
-        intentWithdrawRequestedRateMeter =
-            metricsService.createMeter(metricsComponent,
-                                       metricsFeatureWithdrawRequested,
-                                       METER_RATE_NAME);
-        //
-        intentWithdrawnRateMeter =
-            metricsService.createMeter(metricsComponent,
-                                       metricsFeatureWithdrawn,
-                                       METER_RATE_NAME);
+        intentSubmittedEventMetric =
+            new EventMetric(metricsService, COMPONENT_NAME,
+                            FEATURE_SUBMITTED_NAME);
+        intentInstalledEventMetric =
+            new EventMetric(metricsService, COMPONENT_NAME,
+                            FEATURE_INSTALLED_NAME);
+        intentWithdrawRequestedEventMetric =
+            new EventMetric(metricsService, COMPONENT_NAME,
+                            FEATURE_WITHDRAW_REQUESTED_NAME);
+        intentWithdrawnEventMetric =
+            new EventMetric(metricsService, COMPONENT_NAME,
+                            FEATURE_WITHDRAWN_NAME);
+
+        intentSubmittedEventMetric.registerMetrics();
+        intentInstalledEventMetric.registerMetrics();
+        intentWithdrawRequestedEventMetric.registerMetrics();
+        intentWithdrawnEventMetric.registerMetrics();
     }
 
     /**
      * Removes the metrics.
      */
     private void removeMetrics() {
-        metricsService.removeMetric(metricsComponent,
-                                    metricsFeatureSubmitted,
-                                    GAUGE_TIMESTAMP_NAME);
-        metricsService.removeMetric(metricsComponent,
-                                    metricsFeatureInstalled,
-                                    GAUGE_TIMESTAMP_NAME);
-        metricsService.removeMetric(metricsComponent,
-                                    metricsFeatureWithdrawRequested,
-                                    GAUGE_TIMESTAMP_NAME);
-        metricsService.removeMetric(metricsComponent,
-                                    metricsFeatureWithdrawn,
-                                    GAUGE_TIMESTAMP_NAME);
-        metricsService.removeMetric(metricsComponent,
-                                    metricsFeatureSubmitted,
-                                    METER_RATE_NAME);
-        metricsService.removeMetric(metricsComponent,
-                                    metricsFeatureInstalled,
-                                    METER_RATE_NAME);
-        metricsService.removeMetric(metricsComponent,
-                                    metricsFeatureWithdrawRequested,
-                                    METER_RATE_NAME);
-        metricsService.removeMetric(metricsComponent,
-                                    metricsFeatureWithdrawn,
-                                    METER_RATE_NAME);
+        intentSubmittedEventMetric.removeMetrics();
+        intentInstalledEventMetric.removeMetrics();
+        intentWithdrawRequestedEventMetric.removeMetrics();
+        intentWithdrawnEventMetric.removeMetrics();
     }
 }
diff --git a/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetricsService.java b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetricsService.java
index 4acd00f..73df4bc 100644
--- a/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetricsService.java
+++ b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/IntentMetricsService.java
@@ -1,9 +1,7 @@
 package org.onlab.onos.metrics.intent;
 
 import java.util.List;
-
-import com.codahale.metrics.Gauge;
-import com.codahale.metrics.Meter;
+import org.onlab.metrics.EventMetric;
 import org.onlab.onos.net.intent.IntentEvent;
 
 /**
@@ -18,68 +16,32 @@
     public List<IntentEvent> getEvents();
 
     /**
-     * Gets the Metrics' Gauge for the intent SUBMITTED event timestamp
-     * (ms from the epoch).
+     * Gets the Event Metric for the intent SUBMITTED events.
      *
-     * @return the Metrics' Gauge for the intent SUBMITTED event timestamp
-     * (ms from the epoch)
+     * @return the Event Metric for the intent SUBMITTED events.
      */
-    public Gauge<Long> intentSubmittedTimestampEpochMsGauge();
+    public EventMetric intentSubmittedEventMetric();
 
     /**
-     * Gets the Metrics' Gauge for the intent INSTALLED event timestamp
-     * (ms from the epoch).
+     * Gets the Event Metric for the intent INSTALLED events.
      *
-     * @return the Metrics' Gauge for the intent INSTALLED event timestamp
-     * (ms from the epoch)
+     * @return the Event Metric for the intent INSTALLED events.
      */
-    public Gauge<Long> intentInstalledTimestampEpochMsGauge();
+    public EventMetric intentInstalledEventMetric();
 
     /**
-     * Gets the Metrics' Gauge for the intent WITHDRAW_REQUESTED event
-     * timestamp (ms from the epoch).
+     * Gets the Event Metric for the intent WITHDRAW_REQUESTED events.
      *
      * TODO: This intent event is not implemented yet.
      *
-     * @return the Metrics' Gauge for the intent WITHDRAW_REQUESTED event
-     * timestamp (ms from the epoch)
+     * @return the Event Metric for the intent WITHDRAW_REQUESTED events.
      */
-    public Gauge<Long> intentWithdrawRequestedTimestampEpochMsGauge();
+    public EventMetric intentWithdrawRequestedEventMetric();
 
     /**
-     * Gets the Metrics' Gauge for the intent WITHDRAWN event timestamp
-     * (ms from the epoch).
+     * Gets the Event Metric for the intent WITHDRAWN events.
      *
-     * @return the Metrics' Gauge for the intent WITHDRAWN event timestamp
-     * (ms from the epoch)
+     * @return the Event Metric for the intent WITHDRAWN events.
      */
-    public Gauge<Long> intentWithdrawnTimestampEpochMsGauge();
-
-    /**
-     * Gets the Metrics' Meter for the submitted intents event rate.
-     *
-     * @return the Metrics' Meter for the submitted intents event rate
-     */
-    public Meter intentSubmittedRateMeter();
-
-    /**
-     * Gets the Metrics' Meter for the installed intents event rate.
-     *
-     * @return the Metrics' Meter for the installed intent event rate
-     */
-    public Meter intentInstalledRateMeter();
-
-    /**
-     * Gets the Metrics' Meter for the withdraw requested intents event rate.
-     *
-     * @return the Metrics' Meter for the withdraw requested intents event rate
-     */
-    public Meter intentWithdrawRequestedRateMeter();
-
-    /**
-     * Gets the Metrics' Meter for the withdraw completed intents event rate.
-     *
-     * @return the Metrics' Meter for the withdraw completed intents event rate
-     */
-    public Meter intentWithdrawnRateMeter();
+    public EventMetric intentWithdrawnEventMetric();
 }
diff --git a/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/cli/IntentEventsMetricsCommand.java b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/cli/IntentEventsMetricsCommand.java
index 204cfd6..6f8d013 100644
--- a/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/cli/IntentEventsMetricsCommand.java
+++ b/apps/metrics/intent/src/main/java/org/onlab/onos/metrics/intent/cli/IntentEventsMetricsCommand.java
@@ -11,6 +11,7 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.karaf.shell.commands.Command;
+import org.onlab.metrics.EventMetric;
 import org.onlab.onos.cli.AbstractShellCommand;
 import org.onlab.onos.metrics.intent.IntentMetricsService;
 
@@ -29,8 +30,6 @@
     @Override
     protected void execute() {
         IntentMetricsService service = get(IntentMetricsService.class);
-        Gauge<Long> gauge;
-        Meter meter;
 
         if (outputJson()) {
             ObjectMapper mapper = new ObjectMapper()
@@ -38,49 +37,49 @@
                                                   TimeUnit.MILLISECONDS,
                                                   false));
             ObjectNode result = mapper.createObjectNode();
-            //
-            gauge = service.intentSubmittedTimestampEpochMsGauge();
-            result.put("intentSubmittedTimestamp", json(mapper, gauge));
-            gauge = service.intentInstalledTimestampEpochMsGauge();
-            result.put("intentInstalledTimestamp", json(mapper, gauge));
-            gauge = service.intentWithdrawRequestedTimestampEpochMsGauge();
-            result.put("intentWithdrawRequestedTimestamp",
-                       json(mapper, gauge));
-            gauge = service.intentWithdrawnTimestampEpochMsGauge();
-            result.put("intentWithdrawnTimestamp", json(mapper, gauge));
-            //
-            meter = service.intentSubmittedRateMeter();
-            result.put("intentSubmittedRate", json(mapper, meter));
-            meter = service.intentInstalledRateMeter();
-            result.put("intentInstalledRate", json(mapper, meter));
-            meter = service.intentWithdrawRequestedRateMeter();
-            result.put("intentWithdrawRequestedRate", json(mapper, meter));
-            meter = service.intentWithdrawnRateMeter();
-            result.put("intentWithdrawnRate", json(mapper, meter));
-            //
+            result = json(mapper, result, "intentSubmitted",
+                          service.intentSubmittedEventMetric());
+            result = json(mapper, result, "intentInstalled",
+                          service.intentInstalledEventMetric());
+            result = json(mapper, result, "intentWithdrawRequested",
+                          service.intentWithdrawRequestedEventMetric());
+            result = json(mapper, result, "intentWithdrawn",
+                          service.intentWithdrawnEventMetric());
             print("%s", result);
         } else {
-            gauge = service.intentSubmittedTimestampEpochMsGauge();
-            printGauge("Submitted", gauge);
-            gauge = service.intentInstalledTimestampEpochMsGauge();
-            printGauge("Installed", gauge);
-            gauge = service.intentWithdrawRequestedTimestampEpochMsGauge();
-            printGauge("Withdraw Requested", gauge);
-            gauge = service.intentWithdrawnTimestampEpochMsGauge();
-            printGauge("Withdrawn", gauge);
-            //
-            meter = service.intentSubmittedRateMeter();
-            printMeter("Submitted", meter);
-            meter = service.intentInstalledRateMeter();
-            printMeter("Installed", meter);
-            meter = service.intentWithdrawRequestedRateMeter();
-            printMeter("Withdraw Requested", meter);
-            meter = service.intentWithdrawnRateMeter();
-            printMeter("Withdrawn", meter);
+            printEventMetric("Submitted",
+                             service.intentSubmittedEventMetric());
+            printEventMetric("Installed",
+                             service.intentInstalledEventMetric());
+            printEventMetric("Withdraw Requested",
+                             service.intentWithdrawRequestedEventMetric());
+            printEventMetric("Withdrawn",
+                             service.intentWithdrawnEventMetric());
         }
     }
 
     /**
+     * Produces JSON node for an Event Metric.
+     *
+     * @param mapper the JSON object mapper to use
+     * @param objectNode the JSON object node to use
+     * @param propertyPrefix the property prefix to use
+     * @param eventMetric the Event Metric with the data
+     * @return JSON object node for the Event Metric
+     */
+    private ObjectNode json(ObjectMapper mapper, ObjectNode objectNode,
+                            String propertyPrefix, EventMetric eventMetric) {
+        String gaugeName = propertyPrefix + "Timestamp";
+        String meterName = propertyPrefix + "Rate";
+        Gauge<Long> gauge = eventMetric.lastEventTimestampGauge();
+        Meter meter = eventMetric.eventRateMeter();
+
+        objectNode.put(gaugeName, json(mapper, gauge));
+        objectNode.put(meterName, json(mapper, meter));
+        return objectNode;
+    }
+
+    /**
      * Produces JSON node for an Object.
      *
      * @param mapper the JSON object mapper to use
@@ -94,8 +93,8 @@
         //
         try {
             final String objectJson = mapper.writeValueAsString(object);
-            JsonNode objectNode = mapper.readTree(objectJson);
-            return objectNode;
+            JsonNode jsonNode = mapper.readTree(objectJson);
+            return jsonNode;
         } catch (JsonProcessingException e) {
             log.error("Error writing value as JSON string", e);
         } catch (IOException e) {
@@ -105,28 +104,26 @@
     }
 
     /**
-     * Prints a Gauge.
+     * Prints an Event Metric.
      *
      * @param operationStr the string with the intent operation to print
-     * @param gauge the Gauge to print
+     * @param eventMetric the Event Metric to print
      */
-    private void printGauge(String operationStr, Gauge<Long> gauge) {
-        print(FORMAT_GAUGE, operationStr, gauge.getValue());
-    }
+    private void printEventMetric(String operationStr,
+                                  EventMetric eventMetric) {
+        Gauge<Long> gauge = eventMetric.lastEventTimestampGauge();
+        Meter meter = eventMetric.eventRateMeter();
+        TimeUnit rateUnit = TimeUnit.SECONDS;
+        double rateFactor = rateUnit.toSeconds(1);
 
-    /**
-     * Prints a Meter.
-     *
-     * @param operationStr the string with the intent operation to print
-     * @param meter the Meter to print
-     */
-    private void printMeter(String operationStr, Meter meter) {
-            TimeUnit rateUnit = TimeUnit.SECONDS;
-            double rateFactor = rateUnit.toSeconds(1);
-            print(FORMAT_METER, operationStr, meter.getCount(),
-                  meter.getMeanRate() * rateFactor,
-                  meter.getOneMinuteRate() * rateFactor,
-                  meter.getFiveMinuteRate() * rateFactor,
-                  meter.getFifteenMinuteRate() * rateFactor);
+        // Print the Gauge
+        print(FORMAT_GAUGE, operationStr, gauge.getValue());
+
+        // Print the Meter
+        print(FORMAT_METER, operationStr, meter.getCount(),
+              meter.getMeanRate() * rateFactor,
+              meter.getOneMinuteRate() * rateFactor,
+              meter.getFiveMinuteRate() * rateFactor,
+              meter.getFifteenMinuteRate() * rateFactor);
     }
 }
diff --git a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetrics.java b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetrics.java
index 32cf0cf..814e178 100644
--- a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetrics.java
+++ b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetrics.java
@@ -5,8 +5,6 @@
 import java.util.LinkedList;
 import java.util.List;
 
-import com.codahale.metrics.Gauge;
-import com.codahale.metrics.Meter;
 import com.google.common.collect.ImmutableList;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
@@ -14,8 +12,7 @@
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
-import org.onlab.metrics.MetricsComponent;
-import org.onlab.metrics.MetricsFeature;
+import org.onlab.metrics.EventMetric;
 import org.onlab.metrics.MetricsService;
 import org.onlab.onos.event.Event;
 import org.onlab.onos.net.device.DeviceEvent;
@@ -48,6 +45,8 @@
     protected LinkService linkService;
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected TopologyService topologyService;
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected MetricsService metricsService;
 
     private LinkedList<Event> lastEvents = new LinkedList<>();
     private static final int LAST_EVENTS_MAX_N = 100;
@@ -61,22 +60,22 @@
     //
     // Metrics
     //
-    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected MetricsService metricsService;
-    //
     private static final String COMPONENT_NAME = "Topology";
-    private static final String FEATURE_NAME = "EventNotification";
-    private static final String GAUGE_NAME = "LastEventTimestamp.EpochMs";
-    private static final String METER_NAME = "EventRate";
+    private static final String FEATURE_DEVICE_NAME = "DeviceEvent";
+    private static final String FEATURE_HOST_NAME = "HostEvent";
+    private static final String FEATURE_LINK_NAME = "LinkEvent";
+    private static final String FEATURE_GRAPH_NAME = "GraphEvent";
     //
-    private MetricsComponent metricsComponent;
-    private MetricsFeature metricsFeatureEventNotification;
+    // Event metrics:
+    //  - Device events
+    //  - Host events
+    //  - Link events
+    //  - Topology Graph events
     //
-    // Timestamp of the last Topology event (ms from the Epoch)
-    private volatile long lastEventTimestampEpochMs = 0;
-    private Gauge<Long> lastEventTimestampEpochMsGauge;
-    // Rate of the Topology events published to the Topology listeners
-    private Meter eventRateMeter;
+    private EventMetric topologyDeviceEventMetric;
+    private EventMetric topologyHostEventMetric;
+    private EventMetric topologyLinkEventMetric;
+    private EventMetric topologyGraphEventMetric;
 
     @Activate
     protected void activate() {
@@ -113,27 +112,34 @@
     }
 
     @Override
-    public Gauge<Long> lastEventTimestampEpochMsGauge() {
-        return lastEventTimestampEpochMsGauge;
+    public EventMetric topologyDeviceEventMetric() {
+        return topologyDeviceEventMetric;
     }
 
     @Override
-    public Meter eventRateMeter() {
-        return eventRateMeter;
+    public EventMetric topologyHostEventMetric() {
+        return topologyHostEventMetric;
+    }
+
+    @Override
+    public EventMetric topologyLinkEventMetric() {
+        return topologyLinkEventMetric;
+    }
+
+    @Override
+    public EventMetric topologyGraphEventMetric() {
+        return topologyGraphEventMetric;
     }
 
     /**
      * Records an event.
      *
      * @param event the event to record
-     * @param updateEventRateMeter if true, update the Event Rate Meter
+     * @param eventMetric the Event Metric to use
      */
-    private void recordEvent(Event event, boolean updateEventRateMeter) {
+    private void recordEvent(Event event, EventMetric eventMetric) {
         synchronized (lastEvents) {
-            lastEventTimestampEpochMs = System.currentTimeMillis();
-            if (updateEventRateMeter) {
-                eventRateMeter.mark(1);
-            }
+            eventMetric.eventReceived();
 
             //
             // Keep only the last N events, where N = LAST_EVENTS_MAX_N
@@ -151,7 +157,7 @@
     private class InnerDeviceListener implements DeviceListener {
         @Override
         public void event(DeviceEvent event) {
-            recordEvent(event, true);
+            recordEvent(event, topologyDeviceEventMetric);
             log.debug("Device Event: time = {} type = {} event = {}",
                       event.time(), event.type(), event);
         }
@@ -163,7 +169,7 @@
     private class InnerHostListener implements HostListener {
         @Override
         public void event(HostEvent event) {
-            recordEvent(event, true);
+            recordEvent(event, topologyHostEventMetric);
             log.debug("Host Event: time = {} type = {} event = {}",
                       event.time(), event.type(), event);
         }
@@ -175,7 +181,7 @@
     private class InnerLinkListener implements LinkListener {
         @Override
         public void event(LinkEvent event) {
-            recordEvent(event, true);
+            recordEvent(event, topologyLinkEventMetric);
             log.debug("Link Event: time = {} type = {} event = {}",
                       event.time(), event.type(), event);
         }
@@ -187,11 +193,7 @@
     private class InnerTopologyListener implements TopologyListener {
         @Override
         public void event(TopologyEvent event) {
-            //
-            // NOTE: Don't update the eventRateMeter, because the real
-            // events are already captured/counted.
-            //
-            recordEvent(event, false);
+            recordEvent(event, topologyGraphEventMetric);
             log.debug("Topology Event: time = {} type = {} event = {}",
                       event.time(), event.type(), event);
             for (Event reason : event.reasons()) {
@@ -206,7 +208,6 @@
      */
     private void clear() {
         synchronized (lastEvents) {
-            lastEventTimestampEpochMs = 0;
             lastEvents.clear();
         }
     }
@@ -215,35 +216,32 @@
      * Registers the metrics.
      */
     private void registerMetrics() {
-        metricsComponent = metricsService.registerComponent(COMPONENT_NAME);
-        metricsFeatureEventNotification =
-            metricsComponent.registerFeature(FEATURE_NAME);
-        lastEventTimestampEpochMsGauge =
-            metricsService.registerMetric(metricsComponent,
-                                          metricsFeatureEventNotification,
-                                          GAUGE_NAME,
-                                          new Gauge<Long>() {
-                                              @Override
-                                              public Long getValue() {
-                                                  return lastEventTimestampEpochMs;
-                                              }
-                                          });
-        eventRateMeter =
-            metricsService.createMeter(metricsComponent,
-                                       metricsFeatureEventNotification,
-                                       METER_NAME);
+        topologyDeviceEventMetric =
+            new EventMetric(metricsService, COMPONENT_NAME,
+                            FEATURE_DEVICE_NAME);
+        topologyHostEventMetric =
+            new EventMetric(metricsService, COMPONENT_NAME,
+                            FEATURE_HOST_NAME);
+        topologyLinkEventMetric =
+            new EventMetric(metricsService, COMPONENT_NAME,
+                            FEATURE_LINK_NAME);
+        topologyGraphEventMetric =
+            new EventMetric(metricsService, COMPONENT_NAME,
+                            FEATURE_GRAPH_NAME);
 
+        topologyDeviceEventMetric.registerMetrics();
+        topologyHostEventMetric.registerMetrics();
+        topologyLinkEventMetric.registerMetrics();
+        topologyGraphEventMetric.registerMetrics();
     }
 
     /**
      * Removes the metrics.
      */
     private void removeMetrics() {
-        metricsService.removeMetric(metricsComponent,
-                                    metricsFeatureEventNotification,
-                                    GAUGE_NAME);
-        metricsService.removeMetric(metricsComponent,
-                                    metricsFeatureEventNotification,
-                                    METER_NAME);
+        topologyDeviceEventMetric.removeMetrics();
+        topologyHostEventMetric.removeMetrics();
+        topologyLinkEventMetric.removeMetrics();
+        topologyGraphEventMetric.removeMetrics();
     }
 }
diff --git a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetricsService.java b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetricsService.java
index aeb2e32..9203d9b 100644
--- a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetricsService.java
+++ b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/TopologyMetricsService.java
@@ -1,9 +1,7 @@
 package org.onlab.onos.metrics.topology;
 
 import java.util.List;
-
-import com.codahale.metrics.Gauge;
-import com.codahale.metrics.Meter;
+import org.onlab.metrics.EventMetric;
 import org.onlab.onos.event.Event;
 
 /**
@@ -18,18 +16,30 @@
     public List<Event> getEvents();
 
     /**
-     * Gets the Metrics' Gauge for the last topology event timestamp
-     * (ms from the epoch).
+     * Gets the Event Metric for the Device Events.
      *
-     * @return the Metrics' Gauge for the last topology event timestamp
-     * (ms from the epoch)
+     * @return the Event Metric for the Device Events.
      */
-    public Gauge<Long> lastEventTimestampEpochMsGauge();
+    public EventMetric topologyDeviceEventMetric();
 
     /**
-     * Gets the Metrics' Meter for the topology events rate.
+     * Gets the Event Metric for the Host Events.
      *
-     * @return the Metrics' Meter for the topology events rate
+     * @return the Event Metric for the Host Events.
      */
-    public Meter eventRateMeter();
+    public EventMetric topologyHostEventMetric();
+
+    /**
+     * Gets the Event Metric for the Link Events.
+     *
+     * @return the Event Metric for the Link Events.
+     */
+    public EventMetric topologyLinkEventMetric();
+
+    /**
+     * Gets the Event Metric for the Topology Graph Events.
+     *
+     * @return the Event Metric for the Topology Graph Events.
+     */
+    public EventMetric topologyGraphEventMetric();
 }
diff --git a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsMetricsCommand.java b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsMetricsCommand.java
index 54d3a95..b7e0401 100644
--- a/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsMetricsCommand.java
+++ b/apps/metrics/topology/src/main/java/org/onlab/onos/metrics/topology/cli/TopologyEventsMetricsCommand.java
@@ -11,6 +11,7 @@
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.apache.karaf.shell.commands.Command;
+import org.onlab.metrics.EventMetric;
 import org.onlab.onos.cli.AbstractShellCommand;
 import org.onlab.onos.metrics.topology.TopologyMetricsService;
 
@@ -22,15 +23,13 @@
 public class TopologyEventsMetricsCommand extends AbstractShellCommand {
 
     private static final String FORMAT_GAUGE =
-        "Last Topology Event Timestamp (ms from epoch)=%d";
+        "Topology %s Event Timestamp (ms from epoch)=%d";
     private static final String FORMAT_METER =
-        "Topology Events count=%d rate(events/sec) mean=%f m1=%f m5=%f m15=%f";
+        "Topology %s Events count=%d rate(events/sec) mean=%f m1=%f m5=%f m15=%f";
 
     @Override
     protected void execute() {
         TopologyMetricsService service = get(TopologyMetricsService.class);
-        Gauge<Long> gauge = service.lastEventTimestampEpochMsGauge();
-        Meter meter = service.eventRateMeter();
 
         if (outputJson()) {
             ObjectMapper mapper = new ObjectMapper()
@@ -38,32 +37,89 @@
                                                   TimeUnit.MILLISECONDS,
                                                   false));
             ObjectNode result = mapper.createObjectNode();
-            try {
-                //
-                // NOTE: The API for custom serializers is incomplete,
-                // hence we have to parse the JSON string to create JsonNode.
-                //
-                final String gaugeJson = mapper.writeValueAsString(gauge);
-                final String meterJson = mapper.writeValueAsString(meter);
-                JsonNode gaugeNode = mapper.readTree(gaugeJson);
-                JsonNode meterNode = mapper.readTree(meterJson);
-                result.put("lastTopologyEventTimestamp", gaugeNode);
-                result.put("topologyEventRate", meterNode);
-            } catch (JsonProcessingException e) {
-                log.error("Error writing value as JSON string", e);
-            } catch (IOException e) {
-                log.error("Error writing value as JSON string", e);
-            }
+            result = json(mapper, result, "topologyDeviceEvent",
+                          service.topologyDeviceEventMetric());
+            result = json(mapper, result, "topologyHostEvent",
+                          service.topologyHostEventMetric());
+            result = json(mapper, result, "topologyLinkEvent",
+                          service.topologyLinkEventMetric());
+            result = json(mapper, result, "topologyGraphEvent",
+                          service.topologyGraphEventMetric());
             print("%s", result);
         } else {
-            TimeUnit rateUnit = TimeUnit.SECONDS;
-            double rateFactor = rateUnit.toSeconds(1);
-            print(FORMAT_GAUGE, gauge.getValue());
-            print(FORMAT_METER, meter.getCount(),
-                  meter.getMeanRate() * rateFactor,
-                  meter.getOneMinuteRate() * rateFactor,
-                  meter.getFiveMinuteRate() * rateFactor,
-                  meter.getFifteenMinuteRate() * rateFactor);
+            printEventMetric("Device", service.topologyDeviceEventMetric());
+            printEventMetric("Host", service.topologyHostEventMetric());
+            printEventMetric("Link", service.topologyLinkEventMetric());
+            printEventMetric("Graph", service.topologyGraphEventMetric());
         }
     }
+
+    /**
+     * Produces JSON node for an Event Metric.
+     *
+     * @param mapper the JSON object mapper to use
+     * @param objectNode the JSON object node to use
+     * @param propertyPrefix the property prefix to use
+     * @param eventMetric the Event Metric with the data
+     * @return JSON object node for the Event Metric
+     */
+    private ObjectNode json(ObjectMapper mapper, ObjectNode objectNode,
+                            String propertyPrefix, EventMetric eventMetric) {
+        String gaugeName = propertyPrefix + "Timestamp";
+        String meterName = propertyPrefix + "Rate";
+        Gauge<Long> gauge = eventMetric.lastEventTimestampGauge();
+        Meter meter = eventMetric.eventRateMeter();
+
+        objectNode.put(gaugeName, json(mapper, gauge));
+        objectNode.put(meterName, json(mapper, meter));
+        return objectNode;
+    }
+
+    /**
+     * Produces JSON node for an Object.
+     *
+     * @param mapper the JSON object mapper to use
+     * @param object the Object with the data
+     * @return JSON node for the Object
+     */
+    private JsonNode json(ObjectMapper mapper, Object object) {
+        //
+        // NOTE: The API for custom serializers is incomplete,
+        // hence we have to parse the JSON string to create JsonNode.
+        //
+        try {
+            final String objectJson = mapper.writeValueAsString(object);
+            JsonNode jsonNode = mapper.readTree(objectJson);
+            return jsonNode;
+        } catch (JsonProcessingException e) {
+            log.error("Error writing value as JSON string", e);
+        } catch (IOException e) {
+            log.error("Error writing value as JSON string", e);
+        }
+        return null;
+    }
+
+    /**
+     * Prints an Event Metric.
+     *
+     * @param operationStr the string with the intent operation to print
+     * @param eventMetric the Event Metric to print
+     */
+    private void printEventMetric(String operationStr,
+                                  EventMetric eventMetric) {
+        Gauge<Long> gauge = eventMetric.lastEventTimestampGauge();
+        Meter meter = eventMetric.eventRateMeter();
+        TimeUnit rateUnit = TimeUnit.SECONDS;
+        double rateFactor = rateUnit.toSeconds(1);
+
+        // Print the Gauge
+        print(FORMAT_GAUGE, operationStr, gauge.getValue());
+
+        // Print the Meter
+        print(FORMAT_METER, operationStr, meter.getCount(),
+              meter.getMeanRate() * rateFactor,
+              meter.getOneMinuteRate() * rateFactor,
+              meter.getFiveMinuteRate() * rateFactor,
+              meter.getFifteenMinuteRate() * rateFactor);
+    }
 }
diff --git a/apps/optical/src/main/java/org/onlab/onos/optical/cfg/OpticalConfigProvider.java b/apps/optical/src/main/java/org/onlab/onos/optical/cfg/OpticalConfigProvider.java
index 285e4a5..6d46807 100644
--- a/apps/optical/src/main/java/org/onlab/onos/optical/cfg/OpticalConfigProvider.java
+++ b/apps/optical/src/main/java/org/onlab/onos/optical/cfg/OpticalConfigProvider.java
@@ -58,7 +58,7 @@
 
     // TODO: fix hard coded file path later.
     private static final String DEFAULT_CONFIG_FILE =
-            "/opt/onos/config/demo-3-roadm-2-ps.json";
+            "config/demo-3-roadm-2-ps.json";
     private String configFileName = DEFAULT_CONFIG_FILE;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
@@ -83,7 +83,8 @@
     protected OpticalNetworkConfig opticalNetworkConfig;
 
     public OpticalConfigProvider() {
-        super(new ProviderId("optical", "org.onlab.onos.provider.opticalConfig", true));
+        super(new ProviderId("optical", "org.onlab.onos.provider" +
+                ".opticalConfig"));
     }
 
     @Activate
diff --git a/apps/sdnip/pom.xml b/apps/sdnip/pom.xml
index b1465c1..5eb16f6 100644
--- a/apps/sdnip/pom.xml
+++ b/apps/sdnip/pom.xml
@@ -45,6 +45,12 @@
 
     <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>
diff --git a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivityManager.java b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivityManager.java
index 400dba0..4056cbe 100644
--- a/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivityManager.java
+++ b/apps/sdnip/src/main/java/org/onlab/onos/sdnip/PeerConnectivityManager.java
@@ -1,5 +1,7 @@
 package org.onlab.onos.sdnip;
 
+import java.util.List;
+
 import org.onlab.onos.ApplicationId;
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.flow.DefaultTrafficSelector;
@@ -8,6 +10,7 @@
 import org.onlab.onos.net.flow.TrafficTreatment;
 import org.onlab.onos.net.intent.IntentService;
 import org.onlab.onos.net.intent.PointToPointIntent;
+import org.onlab.onos.sdnip.bgp.BgpConstants;
 import org.onlab.onos.sdnip.config.BgpPeer;
 import org.onlab.onos.sdnip.config.BgpSpeaker;
 import org.onlab.onos.sdnip.config.Interface;
@@ -20,8 +23,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.List;
-
 /**
  * Manages the connectivity requirements between peers.
  */
@@ -30,37 +31,44 @@
     private static final Logger log = LoggerFactory.getLogger(
             PeerConnectivityManager.class);
 
-    // TODO these shouldn't be defined here
-    private static final short BGP_PORT = 179;
-    private static final int IPV4_BIT_LENGTH = 32;
-
-    private final SdnIpConfigService configInfoService;
+    private final SdnIpConfigService configService;
     private final InterfaceService interfaceService;
     private final IntentService intentService;
 
     private final ApplicationId appId;
 
+    /**
+     * Creates a new PeerConnectivityManager.
+     *
+     * @param appId             the application ID
+     * @param configService     the SDN-IP config service
+     * @param interfaceService  the interface service
+     * @param intentService     the intent service
+     */
     public PeerConnectivityManager(ApplicationId appId,
-                                   SdnIpConfigService configInfoService,
+                                   SdnIpConfigService configService,
                                    InterfaceService interfaceService,
                                    IntentService intentService) {
         this.appId = appId;
-        this.configInfoService = configInfoService;
+        this.configService = configService;
         this.interfaceService = interfaceService;
         this.intentService = intentService;
     }
 
+    /**
+     * Starts the peer connectivity manager.
+     */
     public void start() {
         // TODO are any of these errors?
         if (interfaceService.getInterfaces().isEmpty()) {
 
             log.warn("The interface in configuration file is empty. "
                              + "Thus, the SDN-IP application can not be started.");
-        } else if (configInfoService.getBgpPeers().isEmpty()) {
+        } else if (configService.getBgpPeers().isEmpty()) {
 
             log.warn("The BGP peer in configuration file is empty."
                              + "Thus, the SDN-IP application can not be started.");
-        } else if (configInfoService.getBgpSpeakers() == null) {
+        } else if (configService.getBgpSpeakers() == null) {
 
             log.error("The BGP speaker in configuration file is empty. "
                               + "Thus, the SDN-IP application can not be started.");
@@ -79,7 +87,7 @@
      * for paths from all peers to each BGP speaker.
      */
     private void setupBgpPaths() {
-        for (BgpSpeaker bgpSpeaker : configInfoService.getBgpSpeakers()
+        for (BgpSpeaker bgpSpeaker : configService.getBgpSpeakers()
                 .values()) {
             log.debug("Start to set up BGP paths for BGP speaker: {}",
                       bgpSpeaker);
@@ -88,7 +96,7 @@
             List<InterfaceAddress> interfaceAddresses =
                     bgpSpeaker.interfaceAddresses();
 
-            for (BgpPeer bgpPeer : configInfoService.getBgpPeers().values()) {
+            for (BgpPeer bgpPeer : configService.getBgpPeers().values()) {
 
                 log.debug("Start to set up BGP paths between BGP speaker: {} "
                                   + "to BGP peer: {}", bgpSpeaker, bgpPeer);
@@ -121,16 +129,14 @@
 
                 // install intent for BGP path from BGPd to BGP peer matching
                 // destination TCP port 179
-
-                // TODO: The usage of PacketMatchBuilder will be improved, then we
-                // only need to new the PacketMatchBuilder once.
-                // By then, the code here will be improved accordingly.
                 TrafficSelector selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchTcpDst(BGP_PORT)
+                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchTcpDst((short) BgpConstants.BGP_PORT)
                         .build();
 
                 TrafficTreatment treatment = DefaultTrafficTreatment.builder()
@@ -149,9 +155,11 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchTcpSrc(BGP_PORT)
+                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchTcpSrc((short) BgpConstants.BGP_PORT)
                         .build();
 
                 PointToPointIntent intentMatchSrcTcpPort =
@@ -167,9 +175,11 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchTcpDst(BGP_PORT)
+                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchTcpDst((short) BgpConstants.BGP_PORT)
                         .build();
 
                 PointToPointIntent reversedIntentMatchDstTcpPort =
@@ -185,9 +195,11 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_TCP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchTcpSrc(BGP_PORT)
+                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchTcpSrc((short) BgpConstants.BGP_PORT)
                         .build();
 
                 PointToPointIntent reversedIntentMatchSrcTcpPort =
@@ -211,7 +223,7 @@
      * for paths from all peers to each BGP speaker.
      */
     private void setupIcmpPaths() {
-        for (BgpSpeaker bgpSpeaker : configInfoService.getBgpSpeakers()
+        for (BgpSpeaker bgpSpeaker : configService.getBgpSpeakers()
                 .values()) {
             log.debug("Start to set up ICMP paths for BGP speaker: {}",
                       bgpSpeaker);
@@ -219,7 +231,7 @@
             List<InterfaceAddress> interfaceAddresses = bgpSpeaker
                     .interfaceAddresses();
 
-            for (BgpPeer bgpPeer : configInfoService.getBgpPeers().values()) {
+            for (BgpPeer bgpPeer : configService.getBgpPeers().values()) {
 
                 Interface peerInterface = interfaceService.getInterface(
                         bgpPeer.connectPoint());
@@ -253,8 +265,10 @@
                 TrafficSelector selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_ICMP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
+                        .matchIPSrc(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
                         .build();
 
                 TrafficTreatment treatment = DefaultTrafficTreatment.builder()
@@ -271,8 +285,10 @@
                 selector = DefaultTrafficSelector.builder()
                         .matchEthType(Ethernet.TYPE_IPV4)
                         .matchIPProtocol(IPv4.PROTOCOL_ICMP)
-                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(), IPV4_BIT_LENGTH))
-                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(), IPV4_BIT_LENGTH))
+                        .matchIPSrc(IpPrefix.valueOf(bgpdPeerAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
+                        .matchIPDst(IpPrefix.valueOf(bgpdAddress.toInt(),
+                                IpAddress.MAX_INET_MASK))
                         .build();
 
                 PointToPointIntent reversedIntent =
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 ed5b8df..1d8ef16 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
@@ -55,8 +55,6 @@
 /**
  * This class processes BGP route update, translates each update into a intent
  * and submits the intent.
- * <p/>
- * TODO: Make it thread-safe.
  */
 public class Router implements RouteListener {
 
@@ -69,14 +67,13 @@
     // Stores all incoming route updates in a queue.
     private BlockingQueue<RouteUpdate> routeUpdates;
 
-    // The Ip4Address is the next hop address of each route update.
+    // The IpAddress is the next hop address of each route update.
     private SetMultimap<IpAddress, RouteEntry> routesWaitingOnArp;
     private ConcurrentHashMap<IpPrefix, MultiPointToSinglePointIntent> pushedRouteIntents;
 
     private IntentService intentService;
-    //private IProxyArpService proxyArp;
     private HostService hostService;
-    private SdnIpConfigService configInfoService;
+    private SdnIpConfigService configService;
     private InterfaceService interfaceService;
 
     private ExecutorService bgpUpdatesExecutor;
@@ -98,18 +95,19 @@
     /**
      * Class constructor.
      *
+     * @param appId             the application ID
      * @param intentService     the intent service
      * @param hostService       the host service
-     * @param configInfoService the configuration service
+     * @param configService     the configuration service
      * @param interfaceService  the interface service
      */
     public Router(ApplicationId appId, IntentService intentService,
-                  HostService hostService, SdnIpConfigService configInfoService,
+                  HostService hostService, SdnIpConfigService configService,
                   InterfaceService interfaceService) {
         this.appId = appId;
         this.intentService = intentService;
         this.hostService = hostService;
-        this.configInfoService = configInfoService;
+        this.configService = configService;
         this.interfaceService = interfaceService;
 
         bgpRoutes = new ConcurrentInvertedRadixTree<>(
@@ -172,7 +170,7 @@
 
     @Override
     public void update(RouteUpdate routeUpdate) {
-        log.debug("Received new route Update: {}", routeUpdate);
+        log.debug("Received new route update: {}", routeUpdate);
 
         try {
             routeUpdates.put(routeUpdate);
@@ -498,9 +496,11 @@
     private void executeRouteAdd(RouteEntry routeEntry) {
         log.debug("Executing route add: {}", routeEntry);
 
+        // Monitor the IP address so we'll get notified of updates to the MAC
+        // address.
+        hostService.startMonitoringIp(routeEntry.nextHop());
+
         // See if we know the MAC address of the next hop
-        //MacAddress nextHopMacAddress =
-        //proxyArp.getMacAddress(routeEntry.getNextHop());
         MacAddress nextHopMacAddress = null;
         Set<Host> hosts = hostService.getHostsByIp(
                 routeEntry.nextHop().toPrefix());
@@ -511,9 +511,6 @@
 
         if (nextHopMacAddress == null) {
             routesWaitingOnArp.put(routeEntry.nextHop(), routeEntry);
-            //proxyArp.sendArpRequest(routeEntry.getNextHop(), this, true);
-            // TODO maybe just do this for every prefix anyway
-            hostService.startMonitoringIp(routeEntry.nextHop());
             return;
         }
 
@@ -536,11 +533,11 @@
 
         // Find the attachment point (egress interface) of the next hop
         Interface egressInterface;
-        if (configInfoService.getBgpPeers().containsKey(nextHopIpAddress)) {
+        if (configService.getBgpPeers().containsKey(nextHopIpAddress)) {
             // Route to a peer
             log.debug("Route to peer {}", nextHopIpAddress);
             BgpPeer peer =
-                    configInfoService.getBgpPeers().get(nextHopIpAddress);
+                    configService.getBgpPeers().get(nextHopIpAddress);
             egressInterface =
                     interfaceService.getInterface(peer.connectPoint());
         } else {
@@ -593,17 +590,12 @@
         }
 
         // Match the destination IP prefix at the first hop
-        //PacketMatchBuilder builder = new PacketMatchBuilder();
-        //builder.setEtherType(Ethernet.TYPE_IPV4).setDstIpNet(prefix);
-        //PacketMatch packetMatch = builder.build();
         TrafficSelector selector = DefaultTrafficSelector.builder()
                 .matchEthType(Ethernet.TYPE_IPV4)
                 .matchIPDst(prefix)
                 .build();
 
         // Rewrite the destination MAC address
-        //ModifyDstMacAction modifyDstMacAction =
-        //new ModifyDstMacAction(nextHopMacAddress);
         TrafficTreatment treatment = DefaultTrafficTreatment.builder()
                 .setEthDst(nextHopMacAddress)
                 .build();
@@ -635,10 +627,6 @@
             log.debug("Processing route delete: {}", routeEntry);
             IpPrefix prefix = routeEntry.prefix();
 
-            // TODO check the change of logic here - remove doesn't check that
-            // the route entry was what we expected (and we can't do this
-            // concurrently)
-
             if (bgpRoutes.remove(RouteEntry.createBinaryString(prefix))) {
                 //
                 // Only delete flows if an entry was actually removed from the
@@ -680,17 +668,19 @@
     }
 
     /**
-     * This method handles the prefixes which are waiting for ARP replies for
-     * MAC addresses of next hops.
+     * Signals the Router that the MAC to IP mapping has potentially been
+     * updated. This has the effect of updating the MAC address for any
+     * installed prefixes if it has changed, as well as installing any pending
+     * prefixes that were waiting for MAC resolution.
      *
-     * @param ipAddress  next hop router IP address, for which we sent ARP
-     *                   request out
-     * @param macAddress MAC address which is relative to the ipAddress
+     * @param ipAddress the IP address that an event was received for
+     * @param macAddress the most recently known MAC address for the IP address
      */
-    //@Override
-    // TODO change name
-    public void arpResponse(IpAddress ipAddress, MacAddress macAddress) {
-        log.debug("Received ARP response: {} => {}", ipAddress, macAddress);
+    private void updateMac(IpAddress ipAddress, MacAddress macAddress) {
+        log.debug("Received updated MAC info: {} => {}", ipAddress, macAddress);
+
+        // TODO here we should check whether the next hop for any of our
+        // installed prefixes has changed, not just prefixes pending installation.
 
         // We synchronize on this to prevent changes to the radix tree
         // while we're pushing intents. If the tree changes, the
@@ -708,8 +698,6 @@
                         bgpRoutes.getValueForExactKey(binaryString);
                 if (foundRouteEntry != null &&
                         foundRouteEntry.nextHop().equals(routeEntry.nextHop())) {
-                    log.debug("Pushing prefix {} next hop {}",
-                              routeEntry.prefix(), routeEntry.nextHop());
                     // We only push prefix flows if the prefix is still in the
                     // radix tree and the next hop is the same as our
                     // update.
@@ -717,9 +705,8 @@
                     // for the ARP, or the next hop could have changed.
                     addRouteIntentToNextHop(prefix, ipAddress, macAddress);
                 } else {
-                    log.debug("Received ARP response, but {}/{} is no longer in"
-                                      + " the radix tree", routeEntry.prefix(),
-                              routeEntry.nextHop());
+                    log.debug("{} has been revoked before the MAC was resolved",
+                            routeEntry);
                 }
             }
         }
@@ -769,7 +756,7 @@
                     event.type() == HostEvent.Type.HOST_UPDATED) {
                 Host host = event.subject();
                 for (IpPrefix ip : host.ipAddresses()) {
-                    arpResponse(ip.toIpAddress(), host.mac());
+                    updateMac(ip.toIpAddress(), host.mac());
                 }
             }
         }
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 d66ec9c..5ce37c8 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
@@ -26,7 +26,7 @@
 @Service
 public class SdnIp implements SdnIpService {
 
-    private static final String SDN_ID_APP = "org.onlab.onos.sdnip";
+    private static final String SDN_IP_APP = "org.onlab.onos.sdnip";
 
     private final Logger log = getLogger(getClass());
 
@@ -53,8 +53,10 @@
 
         InterfaceService interfaceService = new HostToInterfaceAdaptor(hostService);
 
-        ApplicationId appId = coreService.registerApplication(SDN_ID_APP);
-        peerConnectivity = new PeerConnectivityManager(appId, config, interfaceService, intentService);
+        ApplicationId appId = coreService.registerApplication(SDN_IP_APP);
+
+        peerConnectivity = new PeerConnectivityManager(appId, config,
+                interfaceService, intentService);
         peerConnectivity.start();
 
         router = new Router(appId, intentService, hostService, config, interfaceService);
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouterTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouterTest.java
index 9ab5916..c61e1ec 100644
--- a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouterTest.java
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/RouterTest.java
@@ -1,7 +1,9 @@
 package org.onlab.onos.sdnip;
 
+import static org.easymock.EasyMock.anyObject;
 import static org.easymock.EasyMock.createMock;
 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;
@@ -15,6 +17,8 @@
 
 import org.junit.Before;
 import org.junit.Test;
+import org.onlab.junit.TestUtils;
+import org.onlab.junit.TestUtils.TestUtilsException;
 import org.onlab.onos.ApplicationId;
 import org.onlab.onos.net.ConnectPoint;
 import org.onlab.onos.net.DefaultHost;
@@ -27,6 +31,7 @@
 import org.onlab.onos.net.flow.DefaultTrafficTreatment;
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
+import org.onlab.onos.net.host.HostListener;
 import org.onlab.onos.net.host.HostService;
 import org.onlab.onos.net.intent.IntentService;
 import org.onlab.onos.net.intent.MultiPointToSinglePointIntent;
@@ -39,8 +44,6 @@
 import org.onlab.packet.IpPrefix;
 import org.onlab.packet.MacAddress;
 import org.onlab.packet.VlanId;
-import org.onlab.util.TestUtils;
-import org.onlab.util.TestUtils.TestUtilsException;
 
 import com.google.common.collect.Sets;
 
@@ -55,10 +58,17 @@
     private IntentService intentService;
     private HostService hostService;
 
-    private Map<IpAddress, BgpPeer> bgpPeers;
-    private Map<IpAddress, BgpPeer> configuredPeers;
-    private Set<Interface> interfaces;
-    private Set<Interface> configuredInterfaces;
+    private static final ConnectPoint SW1_ETH1 = new ConnectPoint(
+            DeviceId.deviceId("of:0000000000000001"),
+            PortNumber.portNumber(1));
+
+    private static final ConnectPoint SW2_ETH1 = new ConnectPoint(
+            DeviceId.deviceId("of:0000000000000002"),
+            PortNumber.portNumber(1));
+
+    private static final ConnectPoint SW3_ETH1 = new ConnectPoint(
+            DeviceId.deviceId("of:0000000000000003"),
+            PortNumber.portNumber(1));
 
     private static final ApplicationId APPID = new ApplicationId() {
         @Override
@@ -76,55 +86,12 @@
 
     @Before
     public void setUp() throws Exception {
-        bgpPeers = setUpBgpPeers();
-        interfaces = setUpInterfaces();
-        initRouter();
-    }
+        setUpBgpPeers();
 
-    /**
-     * Initializes Router class.
-     */
-    private void initRouter() {
+        setUpInterfaceService();
+        setUpHostService();
 
         intentService = createMock(IntentService.class);
-        hostService = createMock(HostService.class);
-
-        interfaceService = createMock(InterfaceService.class);
-        expect(interfaceService.getInterfaces()).andReturn(
-                interfaces).anyTimes();
-
-        Set<IpPrefix> ipAddressesOnSw1Eth1 = new HashSet<IpPrefix>();
-        ipAddressesOnSw1Eth1.add(IpPrefix.valueOf("192.168.10.0/24"));
-        Interface expectedInterface =
-                new Interface(new ConnectPoint(
-                        DeviceId.deviceId("of:0000000000000001"),
-                        PortNumber.portNumber("1")),
-                        ipAddressesOnSw1Eth1,
-                        MacAddress.valueOf("00:00:00:00:00:01"));
-        ConnectPoint egressPoint = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000001"),
-                PortNumber.portNumber(1));
-        expect(interfaceService.getInterface(egressPoint)).andReturn(
-                expectedInterface).anyTimes();
-
-        Set<IpPrefix> ipAddressesOnSw2Eth1 = new HashSet<IpPrefix>();
-        ipAddressesOnSw2Eth1.add(IpPrefix.valueOf("192.168.20.0/24"));
-        Interface expectedInterfaceNew =
-                new Interface(new ConnectPoint(
-                        DeviceId.deviceId("of:0000000000000002"),
-                        PortNumber.portNumber("1")),
-                        ipAddressesOnSw2Eth1,
-                        MacAddress.valueOf("00:00:00:00:00:02"));
-        ConnectPoint egressPointNew = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000002"),
-                PortNumber.portNumber(1));
-        expect(interfaceService.getInterface(egressPointNew)).andReturn(
-                expectedInterfaceNew).anyTimes();
-        replay(interfaceService);
-
-        sdnIpConfigService = createMock(SdnIpConfigService.class);
-        expect(sdnIpConfigService.getBgpPeers()).andReturn(bgpPeers).anyTimes();
-        replay(sdnIpConfigService);
 
         router = new Router(APPID, intentService,
                 hostService, sdnIpConfigService, interfaceService);
@@ -132,67 +99,99 @@
 
     /**
      * Sets up BGP peers in external networks.
-     *
-     * @return configured BGP peers as a Map from peer IP address to BgpPeer
      */
-    private Map<IpAddress, BgpPeer> setUpBgpPeers() {
+    private void setUpBgpPeers() {
 
-        configuredPeers = new HashMap<>();
+        Map<IpAddress, BgpPeer> peers = new HashMap<>();
 
         String peerSw1Eth1 = "192.168.10.1";
-        configuredPeers.put(IpAddress.valueOf(peerSw1Eth1),
+        peers.put(IpAddress.valueOf(peerSw1Eth1),
                 new BgpPeer("00:00:00:00:00:00:00:01", 1, peerSw1Eth1));
 
         // Two BGP peers are connected to switch 2 port 1.
         String peer1Sw2Eth1 = "192.168.20.1";
-        configuredPeers.put(IpAddress.valueOf(peer1Sw2Eth1),
+        peers.put(IpAddress.valueOf(peer1Sw2Eth1),
                 new BgpPeer("00:00:00:00:00:00:00:02", 1, peer1Sw2Eth1));
 
         String peer2Sw2Eth1 = "192.168.20.2";
-        configuredPeers.put(IpAddress.valueOf(peer2Sw2Eth1),
+        peers.put(IpAddress.valueOf(peer2Sw2Eth1),
                 new BgpPeer("00:00:00:00:00:00:00:02", 1, peer2Sw2Eth1));
 
-        return configuredPeers;
+        sdnIpConfigService = createMock(SdnIpConfigService.class);
+        expect(sdnIpConfigService.getBgpPeers()).andReturn(peers).anyTimes();
+        replay(sdnIpConfigService);
+
     }
 
     /**
      * Sets up logical interfaces, which emulate the configured interfaces
      * in SDN-IP application.
-     *
-     * @return configured interfaces as a Set
      */
-    private Set<Interface> setUpInterfaces() {
+    private void setUpInterfaceService() {
+        interfaceService = createMock(InterfaceService.class);
 
-        configuredInterfaces = Sets.newHashSet();
+        Set<Interface> interfaces = Sets.newHashSet();
 
-        Set<IpPrefix> ipAddressesOnSw1Eth1 = new HashSet<IpPrefix>();
-        ipAddressesOnSw1Eth1.add(IpPrefix.valueOf("192.168.10.0/24"));
-        configuredInterfaces.add(
-                new Interface(new ConnectPoint(
-                        DeviceId.deviceId("of:0000000000000001"),
-                        PortNumber.portNumber(1)),
-                        ipAddressesOnSw1Eth1,
-                        MacAddress.valueOf("00:00:00:00:00:01")));
+        Interface sw1Eth1 = new Interface(SW1_ETH1,
+                Sets.newHashSet(IpPrefix.valueOf("192.168.10.101/24")),
+                MacAddress.valueOf("00:00:00:00:00:01"));
 
-        Set<IpPrefix> ipAddressesOnSw2Eth1 = new HashSet<IpPrefix>();
-        ipAddressesOnSw2Eth1.add(IpPrefix.valueOf("192.168.20.0/24"));
-        configuredInterfaces.add(
-                new Interface(new ConnectPoint(
-                        DeviceId.deviceId("of:0000000000000002"),
-                        PortNumber.portNumber(1)),
-                        ipAddressesOnSw2Eth1,
-                        MacAddress.valueOf("00:00:00:00:00:02")));
+        expect(interfaceService.getInterface(SW1_ETH1)).andReturn(sw1Eth1).anyTimes();
+        interfaces.add(sw1Eth1);
 
-        Set<IpPrefix> ipAddressesOnSw3Eth1 = new HashSet<IpPrefix>();
-        ipAddressesOnSw3Eth1.add(IpPrefix.valueOf("192.168.30.0/24"));
-        configuredInterfaces.add(
-                new Interface(new ConnectPoint(
-                        DeviceId.deviceId("of:0000000000000003"),
-                        PortNumber.portNumber(1)),
-                        ipAddressesOnSw3Eth1,
-                        MacAddress.valueOf("00:00:00:00:00:03")));
+        Interface sw2Eth1 = new Interface(SW2_ETH1,
+                Sets.newHashSet(IpPrefix.valueOf("192.168.20.101/24")),
+                MacAddress.valueOf("00:00:00:00:00:02"));
 
-        return configuredInterfaces;
+        expect(interfaceService.getInterface(SW2_ETH1)).andReturn(sw2Eth1).anyTimes();
+        interfaces.add(sw2Eth1);
+
+        Interface sw3Eth1 = new Interface(SW3_ETH1,
+                Sets.newHashSet(IpPrefix.valueOf("192.168.30.101/24")),
+                MacAddress.valueOf("00:00:00:00:00:03"));
+
+        expect(interfaceService.getInterface(SW3_ETH1)).andReturn(sw3Eth1).anyTimes();
+        interfaces.add(sw3Eth1);
+
+        expect(interfaceService.getInterfaces()).andReturn(interfaces).anyTimes();
+
+        replay(interfaceService);
+    }
+
+    /**
+     * Sets up the host service with details of some hosts.
+     */
+    private void setUpHostService() {
+        hostService = createMock(HostService.class);
+
+        hostService.addListener(anyObject(HostListener.class));
+        expectLastCall().anyTimes();
+
+        IpPrefix host1Address = IpPrefix.valueOf("192.168.10.1/32");
+        Host host1 = new DefaultHost(ProviderId.NONE, HostId.NONE,
+                MacAddress.valueOf("00:00:00:00:00:01"), VlanId.NONE,
+                new HostLocation(SW1_ETH1, 1),
+                        Sets.newHashSet(host1Address));
+
+        expect(hostService.getHostsByIp(host1Address))
+                .andReturn(Sets.newHashSet(host1)).anyTimes();
+        hostService.startMonitoringIp(host1Address.toIpAddress());
+        expectLastCall().anyTimes();
+
+
+        IpPrefix host2Address = IpPrefix.valueOf("192.168.20.1/32");
+        Host host2 = new DefaultHost(ProviderId.NONE, HostId.NONE,
+                MacAddress.valueOf("00:00:00:00:00:02"), VlanId.NONE,
+                new HostLocation(SW2_ETH1, 1),
+                        Sets.newHashSet(host2Address));
+
+        expect(hostService.getHostsByIp(host2Address))
+                .andReturn(Sets.newHashSet(host2)).anyTimes();
+        hostService.startMonitoringIp(host2Address.toIpAddress());
+        expectLastCall().anyTimes();
+
+
+        replay(hostService);
     }
 
     /**
@@ -200,7 +199,6 @@
      */
     @Test
     public void testProcessRouteAdd() throws TestUtilsException {
-
         // Construct a route entry
         RouteEntry routeEntry = new RouteEntry(
                 IpPrefix.valueOf("1.1.1.0/24"),
@@ -217,36 +215,13 @@
         treatmentBuilder.setEthDst(MacAddress.valueOf("00:00:00:00:00:01"));
 
         Set<ConnectPoint> ingressPoints = new HashSet<ConnectPoint>();
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000002"),
-                PortNumber.portNumber("1")));
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000003"),
-                PortNumber.portNumber("1")));
-
-        ConnectPoint egressPoint = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000001"),
-                PortNumber.portNumber("1"));
+        ingressPoints.add(SW2_ETH1);
+        ingressPoints.add(SW3_ETH1);
 
         MultiPointToSinglePointIntent intent =
                 new MultiPointToSinglePointIntent(APPID,
                         selectorBuilder.build(), treatmentBuilder.build(),
-                        ingressPoints, egressPoint);
-
-        // Reset host service
-        reset(hostService);
-        Set<Host> hosts = new HashSet<Host>(1);
-        Set<IpPrefix> ipPrefixes = new HashSet<IpPrefix>();
-        ipPrefixes.add(IpPrefix.valueOf("192.168.10.1/32"));
-        hosts.add(new DefaultHost(ProviderId.NONE, HostId.NONE,
-                MacAddress.valueOf("00:00:00:00:00:01"), VlanId.NONE,
-                new HostLocation(
-                        DeviceId.deviceId("of:0000000000000001"),
-                        PortNumber.portNumber(1), 1),
-                        ipPrefixes));
-        expect(hostService.getHostsByIp(
-                IpPrefix.valueOf("192.168.10.1/32"))).andReturn(hosts);
-        replay(hostService);
+                        ingressPoints, SW1_ETH1);
 
         // Set up test expectation
         reset(intentService);
@@ -274,7 +249,6 @@
      */
     @Test
     public void testRouteUpdate() throws TestUtilsException {
-
         // Firstly add a route
         testProcessRouteAdd();
 
@@ -293,22 +267,14 @@
                 DefaultTrafficTreatment.builder();
         treatmentBuilder.setEthDst(MacAddress.valueOf("00:00:00:00:00:01"));
 
-        ConnectPoint egressPoint = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000001"),
-                PortNumber.portNumber("1"));
-
         Set<ConnectPoint> ingressPoints = new HashSet<ConnectPoint>();
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000002"),
-                PortNumber.portNumber("1")));
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000003"),
-                PortNumber.portNumber("1")));
+        ingressPoints.add(SW2_ETH1);
+        ingressPoints.add(SW3_ETH1);
 
         MultiPointToSinglePointIntent intent =
                 new MultiPointToSinglePointIntent(APPID,
                         selectorBuilder.build(), treatmentBuilder.build(),
-                        ingressPoints, egressPoint);
+                        ingressPoints, SW1_ETH1);
 
         // Start to construct a new route entry and new intent
         RouteEntry routeEntryUpdate = new RouteEntry(
@@ -325,38 +291,16 @@
                 DefaultTrafficTreatment.builder();
         treatmentBuilderNew.setEthDst(MacAddress.valueOf("00:00:00:00:00:02"));
 
-        ConnectPoint egressPointNew = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000002"),
-                PortNumber.portNumber("1"));
 
         Set<ConnectPoint> ingressPointsNew = new HashSet<ConnectPoint>();
-        ingressPointsNew.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000001"),
-                PortNumber.portNumber("1")));
-        ingressPointsNew.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000003"),
-                PortNumber.portNumber("1")));
+        ingressPointsNew.add(SW1_ETH1);
+        ingressPointsNew.add(SW3_ETH1);
 
         MultiPointToSinglePointIntent intentNew =
                 new MultiPointToSinglePointIntent(APPID,
                         selectorBuilderNew.build(),
                         treatmentBuilderNew.build(),
-                        ingressPointsNew, egressPointNew);
-
-        // Reset host service
-        reset(hostService);
-        Set<Host> hosts = new HashSet<Host>(1);
-        Set<IpPrefix> ipPrefixes = new HashSet<IpPrefix>();
-        ipPrefixes.add(IpPrefix.valueOf("192.168.20.1/32"));
-        hosts.add(new DefaultHost(ProviderId.NONE, HostId.NONE,
-                MacAddress.valueOf("00:00:00:00:00:02"), VlanId.NONE,
-                new HostLocation(
-                        DeviceId.deviceId("of:0000000000000002"),
-                        PortNumber.portNumber(1), 1),
-                        ipPrefixes));
-        expect(hostService.getHostsByIp(
-                IpPrefix.valueOf("192.168.20.1/32"))).andReturn(hosts);
-        replay(hostService);
+                        ingressPointsNew, SW2_ETH1);
 
         // Set up test expectation
         reset(intentService);
@@ -383,7 +327,6 @@
      */
     @Test
     public void testProcessRouteDelete() throws TestUtilsException {
-
         // Firstly add a route
         testProcessRouteAdd();
 
@@ -402,22 +345,14 @@
                 DefaultTrafficTreatment.builder();
         treatmentBuilder.setEthDst(MacAddress.valueOf("00:00:00:00:00:01"));
 
-        ConnectPoint egressPoint = new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000001"),
-                PortNumber.portNumber("1"));
-
         Set<ConnectPoint> ingressPoints = new HashSet<ConnectPoint>();
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000002"),
-                PortNumber.portNumber("1")));
-        ingressPoints.add(new ConnectPoint(
-                DeviceId.deviceId("of:0000000000000003"),
-                PortNumber.portNumber("1")));
+        ingressPoints.add(SW2_ETH1);
+        ingressPoints.add(SW3_ETH1);
 
         MultiPointToSinglePointIntent intent =
                 new MultiPointToSinglePointIntent(APPID,
                         selectorBuilder.build(), treatmentBuilder.build(),
-                        ingressPoints, egressPoint);
+                        ingressPoints, SW1_ETH1);
 
         // Set up expectation
         reset(intentService);
@@ -442,7 +377,6 @@
      */
     @Test
     public void testLocalRouteAdd() throws TestUtilsException {
-
         // Construct a route entry, the next hop is the local BGP speaker
         RouteEntry routeEntry = new RouteEntry(
                 IpPrefix.valueOf("1.1.1.0/24"), IpAddress.valueOf("0.0.0.0"));
diff --git a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpSessionManagerTest.java b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpSessionManagerTest.java
index 99e3be1..f9dbb7b 100644
--- a/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpSessionManagerTest.java
+++ b/apps/sdnip/src/test/java/org/onlab/onos/sdnip/bgp/BgpSessionManagerTest.java
@@ -26,12 +26,12 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.onlab.junit.TestUtils;
+import org.onlab.junit.TestUtils.TestUtilsException;
 import org.onlab.onos.sdnip.RouteListener;
 import org.onlab.onos.sdnip.RouteUpdate;
 import org.onlab.packet.IpAddress;
 import org.onlab.packet.IpPrefix;
-import org.onlab.util.TestUtils;
-import org.onlab.util.TestUtils.TestUtilsException;
 
 import com.google.common.net.InetAddresses;
 
diff --git a/core/api/src/main/java/org/onlab/onos/codec/CodecService.java b/core/api/src/main/java/org/onlab/onos/codec/CodecService.java
new file mode 100644
index 0000000..c6dbda2
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/codec/CodecService.java
@@ -0,0 +1,58 @@
+/*
+ * 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.codec;
+
+import java.util.Set;
+
+/**
+ * Service for registering and retrieving JSON codecs for various entities.
+ */
+public interface CodecService {
+
+    /**
+     * Returns the set of classes with currently registered codecs.
+     *
+     * @return set of entity classes
+     */
+    Set<Class<?>> getCodecs();
+
+    /**
+     * Returns the JSON codec for the specified entity class.
+     *
+     * @param entityClass entity class
+     * @return JSON codec; null if no codec available for the class
+     */
+    JsonCodec getCodec(Class<?> entityClass);
+
+    /**
+     * Registers the specified JSON codec for the given entity class.
+     *
+     * @param entityClass entity class
+     * @param codec       JSON codec
+     */
+    void registerCodec(Class<?> entityClass, JsonCodec codec);
+
+    /**
+     * Unregisters the JSON codec for the specified entity class.
+     *
+     * @param entityClass entity class
+     */
+    void unregisterCodec(Class<?> entityClass);
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/codec/JsonCodec.java b/core/api/src/main/java/org/onlab/onos/codec/JsonCodec.java
new file mode 100644
index 0000000..4338cde
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/codec/JsonCodec.java
@@ -0,0 +1,88 @@
+/*
+ * 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.codec;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Abstraction of a codec capable for encoding/decoding arbitrary objects to/from JSON.
+ */
+public abstract class JsonCodec<T> {
+
+    /**
+     * Encodes the specified entity into JSON.
+     *
+     * @param entity entity to encode
+     * @param mapper object mapper
+     * @return JSON node
+     * @throws java.lang.UnsupportedOperationException if the codec does not
+     *                                                 support encode operations
+     */
+    public abstract ObjectNode encode(T entity, ObjectMapper mapper);
+
+    /**
+     * Decodes the specified entity from JSON.
+     *
+     * @param json JSON to decode
+     * @return decoded entity
+     * @throws java.lang.UnsupportedOperationException if the codec does not
+     *                                                 support decode operations
+     */
+    public abstract T decode(ObjectNode json);
+
+    /**
+     * Encodes the collection of the specified entities.
+     *
+     * @param entities collection of entities to encode
+     * @param mapper   object mapper
+     * @return JSON array
+     * @throws java.lang.UnsupportedOperationException if the codec does not
+     *                                                 support encode operations
+     */
+    public ArrayNode encode(Iterable<T> entities, ObjectMapper mapper) {
+        ArrayNode result = mapper.createArrayNode();
+        for (T entity : entities) {
+            result.add(encode(entity, mapper));
+        }
+        return result;
+    }
+
+    /**
+     * Decodes the specified JSON array into a collection of entities.
+     *
+     * @param json JSON array to decode
+     * @return collection of decoded entities
+     * @throws java.lang.UnsupportedOperationException if the codec does not
+     *                                                 support decode operations
+     */
+    public List<T> decode(ArrayNode json) {
+        List<T> result = new ArrayList<>();
+        for (JsonNode node : json) {
+            result.add(decode((ObjectNode) node));
+        }
+        return result;
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/codec/package-info.java b/core/api/src/main/java/org/onlab/onos/codec/package-info.java
new file mode 100644
index 0000000..33d6d8c
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/codec/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * Base JSON codec abstraction and a service for tracking various JSON codecs.
+ */
+package org.onlab.onos.codec;
\ No newline at end of file
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 b840318..fa8a64c 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
@@ -55,6 +55,16 @@
     }
 
     @Override
+    public Criterion getCriterion(Criterion.Type type) {
+        for (Criterion c : criteria) {
+            if (c.type() == type) {
+                return c;
+            }
+        }
+        return null;
+    }
+
+    @Override
     public int hashCode() {
         return Objects.hash(criteria);
     }
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 4ba3366..d0d1820 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
@@ -30,7 +30,7 @@
      * @param request batch operation request.
      * @return event.
      */
-    public static FlowRuleBatchEvent create(FlowRuleBatchRequest request) {
+    public static FlowRuleBatchEvent requested(FlowRuleBatchRequest request) {
         FlowRuleBatchEvent event = new FlowRuleBatchEvent(Type.BATCH_OPERATION_REQUESTED, request, null);
         return event;
     }
@@ -41,7 +41,7 @@
      * @param result completed batch operation result.
      * @return event.
      */
-    public static FlowRuleBatchEvent create(FlowRuleBatchRequest request, CompletedBatchOperation result) {
+    public static FlowRuleBatchEvent completed(FlowRuleBatchRequest request, CompletedBatchOperation result) {
         FlowRuleBatchEvent event = new FlowRuleBatchEvent(Type.BATCH_OPERATION_COMPLETED, request, result);
         return event;
     }
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 0414fcb..34e3d31 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
@@ -9,10 +9,12 @@
 
 public class FlowRuleBatchRequest {
 
+    private final int batchId;
     private final List<FlowEntry> toAdd;
     private final List<FlowEntry> toRemove;
 
-    public FlowRuleBatchRequest(List<FlowEntry> toAdd, List<FlowEntry> toRemove) {
+    public FlowRuleBatchRequest(int batchId, List<FlowEntry> toAdd, List<FlowEntry> toRemove) {
+        this.batchId = batchId;
         this.toAdd = Collections.unmodifiableList(toAdd);
         this.toRemove = Collections.unmodifiableList(toRemove);
     }
@@ -35,4 +37,8 @@
         }
         return new FlowRuleBatchOperation(entries);
     }
+
+    public int batchId() {
+        return batchId;
+    }
 }
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 ab9b4a1..bcaf70c 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
@@ -39,6 +39,15 @@
     Set<Criterion> criteria();
 
     /**
+     * Returns the selection criterion for a particular type, if it exists in
+     * this traffic selector.
+     *
+     * @param type criterion type to look up
+     * @return the criterion of the specified type if one exists, otherwise null
+     */
+    Criterion getCriterion(Criterion.Type type);
+
+    /**
      * Builder of traffic selector entities.
      */
     public interface Builder {
diff --git a/core/api/src/test/java/org/onlab/onos/codec/JsonCodecTest.java b/core/api/src/test/java/org/onlab/onos/codec/JsonCodecTest.java
new file mode 100644
index 0000000..f3a10e3
--- /dev/null
+++ b/core/api/src/test/java/org/onlab/onos/codec/JsonCodecTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.codec;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Objects;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test of the base JSON codec abstraction.
+ */
+public class JsonCodecTest {
+
+    private static class Foo {
+        final String name;
+
+        Foo(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(name);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null || getClass() != obj.getClass()) {
+                return false;
+            }
+            final Foo other = (Foo) obj;
+            return Objects.equals(this.name, other.name);
+        }
+    }
+
+    private static class FooCodec extends JsonCodec<Foo> {
+        @Override
+        public ObjectNode encode(Foo entity, ObjectMapper mapper) {
+            return mapper.createObjectNode().put("name", entity.name);
+        }
+
+        @Override
+        public Foo decode(ObjectNode json) {
+            return new Foo(json.get("name").asText());
+        }
+    }
+
+    @Test
+    public void encode() {
+        Foo f1 = new Foo("foo");
+        Foo f2 = new Foo("bar");
+        FooCodec codec = new FooCodec();
+        ImmutableList<Foo> entities = ImmutableList.of(f1, f2);
+        ArrayNode json = codec.encode(entities, new ObjectMapper());
+        List<Foo> foos = codec.decode(json);
+        assertEquals("incorrect encode/decode", entities, foos);
+    }
+
+}
\ No newline at end of file
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java
index 471e397..3e21187 100644
--- a/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java
+++ b/core/api/src/test/java/org/onlab/onos/net/intent/IntentIdTest.java
@@ -6,6 +6,7 @@
 import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 
 /**
  * This class tests the immutability, equality, and non-equality of
@@ -17,7 +18,7 @@
      */
     @Test
     public void intentIdFollowsGuidelineForImmutableObject() {
-        ImmutableClassChecker.assertThatClassIsImmutable(IntentId.class);
+        assertThatClassIsImmutable(IntentId.class);
     }
 
     /**
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 3ef9fc8..60ab307 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
@@ -2,12 +2,14 @@
 
 import static com.google.common.base.Preconditions.checkNotNull;
 import static org.slf4j.LoggerFactory.getLogger;
+import static org.onlab.util.Tools.namedThreads;
 
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CancellationException;
 import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
@@ -74,6 +76,9 @@
 
     private final FlowRuleStoreDelegate delegate = new InternalStoreDelegate();
 
+    private final ExecutorService futureListeners =
+            Executors.newCachedThreadPool(namedThreads("provider-future-listeners"));
+
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected FlowRuleStore store;
 
@@ -92,6 +97,8 @@
 
     @Deactivate
     public void deactivate() {
+        futureListeners.shutdownNow();
+
         store.unsetDelegate(delegate);
         eventDispatcher.removeSink(FlowRuleEvent.class);
         log.info("Stopped");
@@ -398,9 +405,10 @@
                 result.addListener(new Runnable() {
                     @Override
                     public void run() {
-                        store.batchOperationComplete(FlowRuleBatchEvent.create(request, Futures.getUnchecked(result)));
+                        store.batchOperationComplete(FlowRuleBatchEvent.completed(request,
+                                                                                  Futures.getUnchecked(result)));
                     }
-                }, Executors.newCachedThreadPool());
+                }, futureListeners);
 
                 break;
             case BATCH_OPERATION_COMPLETED:
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 1677af6..659a2c4 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
@@ -6,12 +6,11 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_ADDED;
+import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_ADD_REQUESTED;
 import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_REMOVED;
+import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_REMOVE_REQUESTED;
 import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_UPDATED;
 
-
-import static org.onlab.onos.net.flow.FlowRuleEvent.Type.*;
-
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -39,6 +38,7 @@
 import org.onlab.onos.net.PortNumber;
 import org.onlab.onos.net.device.DeviceListener;
 import org.onlab.onos.net.device.DeviceService;
+import org.onlab.onos.net.flow.BatchOperation;
 import org.onlab.onos.net.flow.CompletedBatchOperation;
 import org.onlab.onos.net.flow.DefaultFlowEntry;
 import org.onlab.onos.net.flow.DefaultFlowRule;
@@ -58,7 +58,6 @@
 import org.onlab.onos.net.flow.TrafficTreatment;
 import org.onlab.onos.net.flow.criteria.Criterion;
 import org.onlab.onos.net.flow.instructions.Instruction;
-import org.onlab.onos.net.flow.BatchOperation;
 import org.onlab.onos.net.provider.AbstractProvider;
 import org.onlab.onos.net.provider.ProviderId;
 import org.onlab.onos.store.trivial.impl.SimpleFlowRuleStore;
@@ -583,6 +582,12 @@
         }
 
         @Override
+        public Criterion getCriterion(
+                org.onlab.onos.net.flow.criteria.Criterion.Type type) {
+            return null;
+        }
+
+        @Override
         public int hashCode() {
             return testval;
         }
@@ -594,6 +599,7 @@
             }
             return false;
         }
+
     }
 
     private class TestTreatment implements TrafficTreatment {
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java b/core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java
index 0f020a4..e0e49ef 100644
--- a/core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/IntentTestsMocks.java
@@ -1,5 +1,7 @@
 package org.onlab.onos.net.intent;
 
+import static org.onlab.onos.net.NetTestTools.createPath;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -12,12 +14,11 @@
 import org.onlab.onos.net.flow.TrafficSelector;
 import org.onlab.onos.net.flow.TrafficTreatment;
 import org.onlab.onos.net.flow.criteria.Criterion;
+import org.onlab.onos.net.flow.criteria.Criterion.Type;
 import org.onlab.onos.net.flow.instructions.Instruction;
 import org.onlab.onos.net.topology.LinkWeight;
 import org.onlab.onos.net.topology.PathService;
 
-import static org.onlab.onos.net.NetTestTools.createPath;
-
 /**
  * Common mocks used by the intent framework tests.
  */
@@ -30,6 +31,11 @@
         public Set<Criterion> criteria() {
             return new HashSet<>();
         }
+
+        @Override
+        public Criterion getCriterion(Type type) {
+            return null;
+        }
     }
 
     /**
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/TestHostToHostIntent.java b/core/net/src/test/java/org/onlab/onos/net/intent/TestHostToHostIntent.java
index 4ebee73..2b56e88 100644
--- a/core/net/src/test/java/org/onlab/onos/net/intent/TestHostToHostIntent.java
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/TestHostToHostIntent.java
@@ -11,6 +11,7 @@
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 import static org.onlab.onos.net.NetTestTools.hid;
 
 /**
@@ -104,6 +105,6 @@
      */
     @Test
     public void checkImmutability() {
-        ImmutableClassChecker.assertThatClassIsImmutable(HostToHostIntent.class);
+        assertThatClassIsImmutable(HostToHostIntent.class);
     }
 }
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java b/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java
index 65aa71f..42b5b28 100644
--- a/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/TestLinkCollectionIntent.java
@@ -4,6 +4,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 import static org.onlab.onos.net.NetTestTools.link;
 
 import java.util.HashSet;
@@ -154,6 +155,6 @@
      */
     @Test
     public void checkImmutability() {
-        ImmutableClassChecker.assertThatClassIsImmutable(LinkCollectionIntent.class);
+        assertThatClassIsImmutable(LinkCollectionIntent.class);
     }
 }
diff --git a/core/net/src/test/java/org/onlab/onos/net/intent/TestMultiPointToSinglePointIntent.java b/core/net/src/test/java/org/onlab/onos/net/intent/TestMultiPointToSinglePointIntent.java
index 09dfcb2..0e4f706 100644
--- a/core/net/src/test/java/org/onlab/onos/net/intent/TestMultiPointToSinglePointIntent.java
+++ b/core/net/src/test/java/org/onlab/onos/net/intent/TestMultiPointToSinglePointIntent.java
@@ -15,6 +15,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
 import static org.onlab.onos.net.NetTestTools.connectPoint;
 
 /**
@@ -135,7 +136,6 @@
      */
     @Test
     public void checkImmutability() {
-        ImmutableClassChecker.
-                assertThatClassIsImmutable(MultiPointToSinglePointIntent.class);
+        assertThatClassIsImmutable(MultiPointToSinglePointIntent.class);
     }
 }
diff --git a/core/pom.xml b/core/pom.xml
index c0f74cf..fc8216f 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -35,6 +35,7 @@
         <dependency>
             <groupId>org.onlab.onos</groupId>
             <artifactId>onlab-junit</artifactId>
+            <scope>test</scope>
         </dependency>
     </dependencies>
 
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 4db23ce..b2f679c 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
@@ -103,7 +103,7 @@
         final ControllerNode localNode = clusterService.getLocalNode();
         for (NodeId nodeId : nodes) {
             if (!nodeId.equals(localNode.id())) {
-                ok = unicast(message, nodeId) && ok;
+                ok = unicastUnchecked(message, nodeId) && ok;
             }
         }
         return ok;
@@ -124,6 +124,14 @@
         }
     }
 
+    private boolean unicastUnchecked(ClusterMessage message, NodeId toNodeId) throws IOException {
+        try {
+            return unicast(message, toNodeId);
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
     @Override
     public ClusterMessageResponse sendAndReceive(ClusterMessage message, NodeId toNodeId) throws IOException {
         ControllerNode node = clusterService.getNode(toNodeId);
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 85f928a..cac31c2 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
@@ -3,15 +3,21 @@
 import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_REMOVED;
 import static org.slf4j.LoggerFactory.getLogger;
 import static org.onlab.onos.store.flow.impl.FlowStoreMessageSubjects.*;
+import static org.onlab.util.Tools.namedThreads;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.List;
 
 import org.apache.felix.scr.annotations.Activate;
@@ -22,7 +28,9 @@
 import org.apache.felix.scr.annotations.Service;
 import org.onlab.onos.ApplicationId;
 import org.onlab.onos.cluster.ClusterService;
+import org.onlab.onos.net.Device;
 import org.onlab.onos.net.DeviceId;
+import org.onlab.onos.net.device.DeviceService;
 import org.onlab.onos.net.flow.CompletedBatchOperation;
 import org.onlab.onos.net.flow.DefaultFlowEntry;
 import org.onlab.onos.net.flow.FlowEntry;
@@ -52,8 +60,12 @@
 
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Multimap;
 import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
 
 /**
  * Manages inventory of flow rules using a distributed state management protocol.
@@ -74,13 +86,26 @@
             ArrayListMultimap.<Short, FlowRule>create();
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    private ReplicaInfoService replicaInfoManager;
+    protected ReplicaInfoService replicaInfoManager;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    private ClusterCommunicationService clusterCommunicator;
+    protected ClusterCommunicationService clusterCommunicator;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    private ClusterService clusterService;
+    protected ClusterService clusterService;
+
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+    protected DeviceService deviceService;
+
+    private final AtomicInteger localBatchIdGen = new AtomicInteger();
+
+
+    // FIXME switch to expiraing map/Cache?
+    private Map<Integer, SettableFuture<CompletedBatchOperation>> pendingFutures = Maps.newConcurrentMap();
+
+    private final ExecutorService futureListeners =
+            Executors.newCachedThreadPool(namedThreads("flowstore-peer-responders"));
+
 
     protected static final KryoSerializer SERIALIZER = new KryoSerializer() {
         @Override
@@ -97,36 +122,26 @@
 
     @Activate
     public void activate() {
-        clusterCommunicator.addSubscriber(STORE_FLOW_RULE, new ClusterMessageHandler() {
+        clusterCommunicator.addSubscriber(APPLY_BATCH_FLOWS, new ClusterMessageHandler() {
 
             @Override
-            public void handle(ClusterMessage message) {
-                FlowRule rule = SERIALIZER.decode(message.payload());
-                log.info("received add request for {}", rule);
-                storeFlowRule(rule);
-                // FIXME what to respond.
-                try {
-                    message.respond(SERIALIZER.encode("ACK"));
-                } catch (IOException e) {
-                    log.error("Failed to respond back", e);
-                }
-            }
-        });
+            public void handle(final ClusterMessage message) {
+                FlowRuleBatchOperation operation = SERIALIZER.decode(message.payload());
+                log.info("received batch request {}", operation);
+                final ListenableFuture<CompletedBatchOperation> f = storeBatchInternal(operation);
 
-        clusterCommunicator.addSubscriber(DELETE_FLOW_RULE, new ClusterMessageHandler() {
+                f.addListener(new Runnable() {
 
-            @Override
-            public void handle(ClusterMessage message) {
-                FlowRule rule = SERIALIZER.decode(message.payload());
-                log.info("received delete request for {}", rule);
-                deleteFlowRule(rule);
-                // FIXME what to respond.
-                try {
-                    message.respond(SERIALIZER.encode("ACK"));
-                } catch (IOException e) {
-                    log.error("Failed to respond back", e);
-                }
-
+                    @Override
+                    public void run() {
+                         CompletedBatchOperation result = Futures.getUnchecked(f);
+                        try {
+                            message.respond(SERIALIZER.encode(result));
+                        } catch (IOException e) {
+                            log.error("Failed to respond back", e);
+                        }
+                    }
+                }, futureListeners);
             }
         });
 
@@ -145,6 +160,21 @@
             }
         });
 
+        clusterCommunicator.addSubscriber(GET_DEVICE_FLOW_ENTRIES, new ClusterMessageHandler() {
+
+            @Override
+            public void handle(ClusterMessage message) {
+                DeviceId deviceId = SERIALIZER.decode(message.payload());
+                log.info("Received get flow entries request for {} from {}", deviceId, message.sender());
+                Set<FlowEntry> flowEntries = getFlowEntriesInternal(deviceId);
+                try {
+                    message.respond(SERIALIZER.encode(flowEntries));
+                } catch (IOException e) {
+                    log.error("Failed to respond to peer's getFlowEntries request", e);
+                }
+            }
+        });
+
         log.info("Started");
     }
 
@@ -159,7 +189,13 @@
     // make it device specific.
     @Override
     public int getFlowRuleCount() {
-        return flowEntries.size();
+        // implementing in-efficient operation for debugging purpose.
+        int sum = 0;
+        for (Device device : deviceService.getDevices()) {
+            final DeviceId did = device.id();
+            sum += Iterables.size(getFlowEntries(did));
+        }
+        return sum;
     }
 
     @Override
@@ -197,9 +233,33 @@
 
     @Override
     public synchronized Iterable<FlowEntry> getFlowEntries(DeviceId deviceId) {
+
+        ReplicaInfo replicaInfo = replicaInfoManager.getReplicaInfoFor(deviceId);
+        if (replicaInfo.master().get().equals(clusterService.getLocalNode().id())) {
+            return getFlowEntriesInternal(deviceId);
+        }
+
+        log.info("Forwarding getFlowEntries to {}, which is the primary (master) for device {}",
+                replicaInfo.master().orNull(), deviceId);
+
+        ClusterMessage message = new ClusterMessage(
+                clusterService.getLocalNode().id(),
+                GET_DEVICE_FLOW_ENTRIES,
+                SERIALIZER.encode(deviceId));
+
+        try {
+            ClusterMessageResponse response = clusterCommunicator.sendAndReceive(message, replicaInfo.master().get());
+            return SERIALIZER.decode(response.get(FLOW_RULE_STORE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
+        } catch (IOException | TimeoutException e) {
+            // FIXME: throw a FlowStoreException
+            throw new RuntimeException(e);
+        }
+    }
+
+    private Set<FlowEntry> getFlowEntriesInternal(DeviceId deviceId) {
         Collection<? extends FlowEntry> rules = flowEntries.get(deviceId);
         if (rules == null) {
-            return Collections.emptyList();
+            return Collections.emptySet();
         }
         return ImmutableSet.copyOf(rules);
     }
@@ -218,6 +278,7 @@
         storeBatch(new FlowRuleBatchOperation(Arrays.asList(new FlowRuleBatchEntry(FlowRuleOperation.ADD, rule))));
     }
 
+    @Override
     public Future<CompletedBatchOperation> storeBatch(FlowRuleBatchOperation operation) {
         if (operation.getOperations().isEmpty()) {
             return Futures.immediateFuture(new CompletedBatchOperation(true, Collections.<FlowEntry>emptySet()));
@@ -236,7 +297,7 @@
 
         ClusterMessage message = new ClusterMessage(
                 clusterService.getLocalNode().id(),
-                FlowStoreMessageSubjects.STORE_FLOW_RULE,
+                APPLY_BATCH_FLOWS,
                 SERIALIZER.encode(operation));
 
         try {
@@ -250,7 +311,7 @@
         return null;
     }
 
-    private Future<CompletedBatchOperation> storeBatchInternal(FlowRuleBatchOperation operation) {
+    private ListenableFuture<CompletedBatchOperation> storeBatchInternal(FlowRuleBatchOperation operation) {
         List<FlowEntry> toRemove = new ArrayList<>();
         List<FlowEntry> toAdd = new ArrayList<>();
         // TODO: backup changes to hazelcast map
@@ -261,8 +322,8 @@
                 StoredFlowEntry entry = getFlowEntryInternal(flowRule);
                 if (entry != null) {
                     entry.setState(FlowEntryState.PENDING_REMOVE);
+                    toRemove.add(entry);
                 }
-                toRemove.add(entry);
             } else if (op.equals(FlowRuleOperation.ADD)) {
                 StoredFlowEntry flowEntry = new DefaultFlowEntry(flowRule);
                 DeviceId deviceId = flowRule.deviceId();
@@ -276,9 +337,13 @@
         if (toAdd.isEmpty() && toRemove.isEmpty()) {
             return Futures.immediateFuture(new CompletedBatchOperation(true, Collections.<FlowEntry>emptySet()));
         }
-        notifyDelegate(FlowRuleBatchEvent.create(new FlowRuleBatchRequest(toAdd, toRemove)));
-        // TODO: imlpement this.
-        return Futures.immediateFailedFuture(new RuntimeException("Implement this."));
+
+        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
@@ -293,18 +358,9 @@
             return addOrUpdateFlowRuleInternal(rule);
         }
 
-        ClusterMessage message = new ClusterMessage(
-                clusterService.getLocalNode().id(),
-                FlowStoreMessageSubjects.ADD_OR_UPDATE_FLOW_RULE,
-                SERIALIZER.encode(rule));
-
-        try {
-            ClusterMessageResponse response = clusterCommunicator.sendAndReceive(message, replicaInfo.master().get());
-            return SERIALIZER.decode(response.get(FLOW_RULE_STORE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
-        } catch (IOException | TimeoutException e) {
-            // FIXME: throw a FlowStoreException
-            throw new RuntimeException(e);
-        }
+        log.error("Tried to update FlowRule {} state,"
+                + " while the Node was not the master.", rule);
+        return null;
     }
 
     private synchronized FlowRuleEvent addOrUpdateFlowRuleInternal(FlowEntry rule) {
@@ -338,18 +394,9 @@
             return removeFlowRuleInternal(rule);
         }
 
-        ClusterMessage message = new ClusterMessage(
-                clusterService.getLocalNode().id(),
-                FlowStoreMessageSubjects.REMOVE_FLOW_RULE,
-                SERIALIZER.encode(rule));
-
-        try {
-            ClusterMessageResponse response = clusterCommunicator.sendAndReceive(message, replicaInfo.master().get());
-            return SERIALIZER.decode(response.get(FLOW_RULE_STORE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS));
-        } catch (IOException | TimeoutException e) {
-            // FIXME: throw a FlowStoreException
-            throw new RuntimeException(e);
-        }
+        log.error("Tried to remove FlowRule {},"
+                + " while the Node was not the master.", rule);
+        return null;
     }
 
     private synchronized FlowRuleEvent removeFlowRuleInternal(FlowEntry rule) {
@@ -364,6 +411,11 @@
 
     @Override
     public void batchOperationComplete(FlowRuleBatchEvent event) {
+        SettableFuture<CompletedBatchOperation> future
+            = pendingFutures.get(event.subject().batchId());
+        if (future != null) {
+            future.set(event.result());
+        }
         notifyDelegate(event);
     }
 }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/FlowStoreMessageSubjects.java b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/FlowStoreMessageSubjects.java
index ca833b8..8f4a050 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/FlowStoreMessageSubjects.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/flow/impl/FlowStoreMessageSubjects.java
@@ -7,10 +7,13 @@
  */
 public final class FlowStoreMessageSubjects {
     private FlowStoreMessageSubjects() {}
-    public static final  MessageSubject STORE_FLOW_RULE = new MessageSubject("peer-forward-store-flow-rule");
-    public static final MessageSubject DELETE_FLOW_RULE = new MessageSubject("peer-forward-delete-flow-rule");
-    public static final MessageSubject ADD_OR_UPDATE_FLOW_RULE =
-        new MessageSubject("peer-forward-add-or-update-flow-rule");
-    public static final MessageSubject REMOVE_FLOW_RULE = new MessageSubject("peer-forward-remove-flow-rule");
-    public static final MessageSubject GET_FLOW_ENTRY = new MessageSubject("peer-forward-get-flow-entry");
+
+    public static final  MessageSubject APPLY_BATCH_FLOWS
+        = new MessageSubject("peer-forward-apply-batch");
+
+    public static final MessageSubject GET_FLOW_ENTRY
+        = new MessageSubject("peer-forward-get-flow-entry");
+
+    public static final MessageSubject GET_DEVICE_FLOW_ENTRIES
+        = new MessageSubject("peer-forward-get-device-flow-entries");
 }
diff --git a/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java b/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
index 316a3b4..f9352fe 100644
--- a/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
+++ b/core/store/hz/cluster/src/main/java/org/onlab/onos/store/mastership/impl/DistributedMastershipStore.java
@@ -58,7 +58,6 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected ClusterService clusterService;
 
-    @SuppressWarnings({ "unchecked", "rawtypes" })
     @Override
     @Activate
     public void activate() {
@@ -76,9 +75,9 @@
             }
         };
 
-        roleMap = new SMap(theInstance.getMap("nodeRoles"), this.serializer);
+        roleMap = new SMap<>(theInstance.<byte[], byte[]>getMap("nodeRoles"), this.serializer);
         roleMap.addEntryListener((new RemoteMasterShipEventHandler()), true);
-        terms = new SMap(theInstance.getMap("terms"), this.serializer);
+        terms = new SMap<>(theInstance.<byte[], byte[]>getMap("terms"), this.serializer);
         clusterSize = theInstance.getAtomicLong("clustersize");
 
         log.info("Started");
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 7fddb01..0e9e19c 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
@@ -5,6 +5,7 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedList;
 
 import org.onlab.onos.cluster.ControllerNode;
 import org.onlab.onos.cluster.DefaultControllerNode;
@@ -27,12 +28,15 @@
 import org.onlab.onos.net.PortNumber;
 import org.onlab.onos.net.device.DefaultDeviceDescription;
 import org.onlab.onos.net.device.DefaultPortDescription;
+import org.onlab.onos.net.flow.CompletedBatchOperation;
 import org.onlab.onos.net.flow.DefaultFlowEntry;
 import org.onlab.onos.net.flow.DefaultFlowRule;
 import org.onlab.onos.net.flow.DefaultTrafficSelector;
 import org.onlab.onos.net.flow.DefaultTrafficTreatment;
 import org.onlab.onos.net.flow.FlowEntry;
 import org.onlab.onos.net.flow.FlowId;
+import org.onlab.onos.net.flow.FlowRuleBatchEntry;
+import org.onlab.onos.net.flow.FlowRuleBatchOperation;
 import org.onlab.onos.net.flow.StoredFlowEntry;
 import org.onlab.onos.net.flow.criteria.Criteria;
 import org.onlab.onos.net.flow.criteria.Criterion;
@@ -80,6 +84,7 @@
                     Arrays.asList().getClass(),
                     HashMap.class,
                     HashSet.class,
+                    LinkedList.class,
                     //
                     //
                     ControllerNode.State.class,
@@ -118,7 +123,11 @@
                     DefaultTrafficTreatment.class,
                     Instructions.DropInstruction.class,
                     Instructions.OutputInstruction.class,
-                    RoleInfo.class
+                    RoleInfo.class,
+                    FlowRuleBatchOperation.class,
+                    CompletedBatchOperation.class,
+                    FlowRuleBatchEntry.class,
+                    FlowRuleBatchEntry.FlowRuleOperation.class
                     )
             .register(URI.class, new URISerializer())
             .register(NodeId.class, new NodeIdSerializer())
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 bbfc263..82ea4e2 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
@@ -1,19 +1,8 @@
 package org.onlab.onos.store.trivial.impl;
 
-import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_REMOVED;
-import static org.slf4j.LoggerFactory.getLogger;
-import static org.apache.commons.lang3.concurrent.ConcurrentUtils.createIfAbsentUnchecked;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.Future;
-
+import com.google.common.base.Function;
+import com.google.common.collect.FluentIterable;
+import com.google.common.util.concurrent.Futures;
 import org.apache.felix.scr.annotations.Activate;
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
@@ -40,9 +29,19 @@
 import org.onlab.util.NewConcurrentHashMap;
 import org.slf4j.Logger;
 
-import com.google.common.base.Function;
-import com.google.common.collect.FluentIterable;
-import com.google.common.util.concurrent.Futures;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.Future;
+
+import static org.apache.commons.lang3.concurrent.ConcurrentUtils.createIfAbsentUnchecked;
+import static org.onlab.onos.net.flow.FlowRuleEvent.Type.RULE_REMOVED;
+import static org.slf4j.LoggerFactory.getLogger;
 
 /**
  * Manages inventory of flow rules using trivial in-memory implementation.
@@ -132,15 +131,15 @@
     public Iterable<FlowEntry> getFlowEntries(DeviceId deviceId) {
         // flatten and make iterator unmodifiable
         return FluentIterable.from(getFlowTable(deviceId).values())
-            .transformAndConcat(
-                    new Function<List<StoredFlowEntry>, Iterable<? extends FlowEntry>>() {
+                .transformAndConcat(
+                        new Function<List<StoredFlowEntry>, Iterable<? extends FlowEntry>>() {
 
-                @Override
-                public Iterable<? extends FlowEntry> apply(
-                        List<StoredFlowEntry> input) {
-                    return Collections.unmodifiableList(input);
-                }
-            });
+                            @Override
+                            public Iterable<? extends FlowEntry> apply(
+                                    List<StoredFlowEntry> input) {
+                                return Collections.unmodifiableList(input);
+                            }
+                        });
     }
 
     @Override
@@ -176,10 +175,10 @@
             }
             // new flow rule added
             existing.add(f);
-            notifyDelegate(FlowRuleBatchEvent.create(
-                    new FlowRuleBatchRequest(
-                            Arrays.<FlowEntry>asList(f),
-                            Collections.<FlowEntry>emptyList())));
+            notifyDelegate(FlowRuleBatchEvent.requested(
+                    new FlowRuleBatchRequest(1, /* FIXME generate something */
+                                             Arrays.<FlowEntry>asList(f),
+                                             Collections.<FlowEntry>emptyList())));
         }
     }
 
@@ -194,10 +193,10 @@
                     synchronized (entry) {
                         entry.setState(FlowEntryState.PENDING_REMOVE);
                         // TODO: Should we notify only if it's "remote" event?
-                        notifyDelegate(FlowRuleBatchEvent.create(
-                                new FlowRuleBatchRequest(
-                                        Collections.<FlowEntry>emptyList(),
-                                        Arrays.<FlowEntry>asList(entry))));
+                        notifyDelegate(FlowRuleBatchEvent.requested(
+                                new FlowRuleBatchRequest(1, /* FIXME generate something */
+                                                         Collections.<FlowEntry>emptyList(),
+                                                         Arrays.<FlowEntry>asList(entry))));
                     }
                 }
             }
diff --git a/docs/external.xml b/docs/external.xml
new file mode 100644
index 0000000..45caf24
--- /dev/null
+++ b/docs/external.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <prerequisites>
+        <maven>3.0.0</maven>
+    </prerequisites>
+
+    <parent>
+        <groupId>org.onlab.onos</groupId>
+        <artifactId>onos</artifactId>
+        <version>1.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>onos-docs-external</artifactId>
+    <packaging>pom</packaging>
+
+    <description>ONOS Java API documentation</description>
+
+    <modules>
+        <module>..</module>
+    </modules>
+
+    <url>http://onlab.us/</url>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <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>
+                    <docfilessubdirs>true</docfilessubdirs>
+                    <doctitle>ONOS Java API</doctitle>
+                    <groups>
+                        <group>
+                            <title>Network Model &amp; Services</title>
+                            <packages>
+                                org.onlab.onos:org.onlab.onos.*
+                            </packages>
+                        </group>
+                        <group>
+                            <title>Utilities</title>
+                            <packages>
+                                org.onlab.*
+                            </packages>
+                        </group>
+                    </groups>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/docs/pom.xml b/docs/pom.xml
new file mode 100644
index 0000000..3a38f13
--- /dev/null
+++ b/docs/pom.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <prerequisites>
+        <maven>3.0.0</maven>
+    </prerequisites>
+
+    <parent>
+        <groupId>org.onlab.onos</groupId>
+        <artifactId>onos</artifactId>
+        <version>1.0.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
+    <artifactId>onos-docs</artifactId>
+    <packaging>pom</packaging>
+
+    <description>ONOS Java API documentation</description>
+
+    <modules>
+        <module>..</module>
+    </modules>
+
+    <url>http://onlab.us/</url>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>2.10.1</version>
+                <configuration>
+                    <show>package</show>
+                    <docfilessubdirs>true</docfilessubdirs>
+                    <doctitle>ONOS Java API</doctitle>
+                    <groups>
+                        <group>
+                            <title>Network Model &amp; Services</title>
+                            <packages>
+                                org.onlab.onos:org.onlab.onos.*
+                            </packages>
+                        </group>
+                        <group>
+                            <title>Core Subsystems</title>
+                            <packages>
+                                org.onlab.onos.impl:org.onlab.onos.cluster.impl:org.onlab.onos.net.device.impl:org.onlab.onos.net.link.impl:org.onlab.onos.net.host.impl:org.onlab.onos.net.topology.impl:org.onlab.onos.net.packet.impl:org.onlab.onos.net.flow.impl:org.onlab.onos.store.trivial.*:org.onlab.onos.net.*.impl:org.onlab.onos.event.impl:org.onlab.onos.store.*:org.onlab.onos.net.intent.impl:org.onlab.onos.net.proxyarp.impl:org.onlab.onos.mastership.impl:org.onlab.onos.json:org.onlab.onos.json.*:org.onlab.onos.provider.host.impl:org.onlab.onos.provider.lldp.impl:org.onlab.onos.net.statistic.impl
+                            </packages>
+                        </group>
+                        <group>
+                            <title>OpenFlow Providers &amp; Controller
+                            </title>
+                            <packages>
+                                org.onlab.onos.provider.of.*:org.onlab.onos.openflow.*
+                            </packages>
+                        </group>
+                        <group>
+                            <title>Utilities</title>
+                            <packages>
+                                org.onlab.*
+                            </packages>
+                        </group>
+                        <group>
+                            <title>GUI, REST &amp; Command-Line</title>
+                            <packages>
+                                org.onlab.onos.gui:org.onlab.onos.rest:org.onlab.onos.cli:org.onlab.onos.gui.*:org.onlab.onos.rest.*:org.onlab.onos.cli.*
+                            </packages>
+                        </group>
+                        <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
+                            </packages>
+                        </group>
+                    </groups>
+                    <excludePackageNames>org.onlab.thirdparty
+                    </excludePackageNames>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/src/main/javadoc/doc-files/onos-subsystem.png b/docs/src/main/javadoc/doc-files/onos-subsystem.png
similarity index 100%
rename from src/main/javadoc/doc-files/onos-subsystem.png
rename to docs/src/main/javadoc/doc-files/onos-subsystem.png
Binary files differ
diff --git a/src/main/javadoc/doc-files/onos-tiers.png b/docs/src/main/javadoc/doc-files/onos-tiers.png
similarity index 100%
rename from src/main/javadoc/doc-files/onos-tiers.png
rename to docs/src/main/javadoc/doc-files/onos-tiers.png
Binary files differ
diff --git a/src/main/javadoc/overview.html b/docs/src/main/javadoc/overview.html
similarity index 100%
rename from src/main/javadoc/overview.html
rename to docs/src/main/javadoc/overview.html
diff --git a/features/features.xml b/features/features.xml
index e363094..be9110b 100644
--- a/features/features.xml
+++ b/features/features.xml
@@ -35,7 +35,7 @@
         <bundle>mvn:io.netty/netty-transport-native-epoll/4.0.23.Final</bundle>
         <bundle>mvn:commons-pool/commons-pool/1.6</bundle>
 
-        <bundle>mvn:com.hazelcast/hazelcast/3.3</bundle>
+        <bundle>mvn:com.hazelcast/hazelcast/3.3.2</bundle>
         <bundle>mvn:io.dropwizard.metrics/metrics-core/3.1.0</bundle>
         <bundle>mvn:io.dropwizard.metrics/metrics-json/3.1.0</bundle>
         <bundle>mvn:com.eclipsesource.minimal-json/minimal-json/0.9.1</bundle>
diff --git a/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFOpticalSwitchImplLINC13.java b/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFOpticalSwitchImplLINC13.java
index d65a15f..3d8d93f 100644
--- a/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFOpticalSwitchImplLINC13.java
+++ b/openflow/ctl/src/main/java/org/onlab/onos/openflow/drivers/impl/OFOpticalSwitchImplLINC13.java
@@ -5,30 +5,17 @@
 import org.onlab.onos.openflow.controller.driver.SwitchDriverSubHandshakeNotStarted;
 import org.onlab.onos.openflow.controller.Dpid;
 import org.onlab.onos.openflow.controller.driver.AbstractOpenFlowSwitch;
-import org.projectfloodlight.openflow.protocol.OFBarrierRequest;
 import org.projectfloodlight.openflow.protocol.OFCircuitPortStatus;
 import org.projectfloodlight.openflow.protocol.OFCircuitPortsReply;
 import org.projectfloodlight.openflow.protocol.OFCircuitPortsRequest;
 import org.projectfloodlight.openflow.protocol.OFDescStatsReply;
 import org.projectfloodlight.openflow.protocol.OFErrorMsg;
-import org.projectfloodlight.openflow.protocol.OFMatchV3;
 import org.projectfloodlight.openflow.protocol.OFMessage;
-import org.projectfloodlight.openflow.protocol.OFOxmList;
 import org.projectfloodlight.openflow.protocol.OFPortDesc;
 import org.projectfloodlight.openflow.protocol.OFPortDescStatsReply;
 import org.projectfloodlight.openflow.protocol.OFPortOptical;
 import org.projectfloodlight.openflow.protocol.OFStatsReply;
 import org.projectfloodlight.openflow.protocol.OFStatsType;
-import org.projectfloodlight.openflow.protocol.action.OFAction;
-import org.projectfloodlight.openflow.protocol.action.OFActionCircuit;
-import org.projectfloodlight.openflow.protocol.instruction.OFInstruction;
-import org.projectfloodlight.openflow.protocol.oxm.OFOxmInPort;
-import org.projectfloodlight.openflow.protocol.oxm.OFOxmOchSigid;
-import org.projectfloodlight.openflow.protocol.oxm.OFOxmOchSigidBasic;
-import org.projectfloodlight.openflow.protocol.oxm.OFOxmOchSigtype;
-import org.projectfloodlight.openflow.types.CircuitSignalID;
-import org.projectfloodlight.openflow.types.OFPort;
-import org.projectfloodlight.openflow.types.U8;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -131,12 +118,6 @@
                             (OFCircuitPortsReply) m);
                     driverHandshakeComplete.set(true);
                 }
-                /*try {
-                    testMA();
-                    testReverseMA();
-                } catch (IOException e) {
-                    e.printStackTrace();
-                }*/
                 break;
             default:
                 log.warn("Received message {} during switch-driver " +
@@ -190,7 +171,6 @@
         this.write(Collections.<OFMessage>singletonList(circuitPortsRequest));
     }
 
-
     @Override
     public List<OFPortDesc> getPorts() {
         List<OFPortDesc> portEntries = new ArrayList<>();
@@ -201,370 +181,6 @@
         return Collections.unmodifiableList(portEntries);
     }
 
-
-    public static final U8 SIGNAL_TYPE = U8.of((short) 10);
-    private void testMA() throws IOException {
-        log.debug("LINC OE *** Testing MA ");
-        short lambda = 1;
-        if (getId() == 0x0000ffffffffff01L) {
-            final int inport = 10;
-            final int outport = 20;
-            //Circuit signal id
-            CircuitSignalID sigID = getSignalID(lambda);
-
-            OFOxmOchSigidBasic ofOxmOchSigidBasic =
-                    factory().oxms().ochSigidBasic(sigID);
-
-
-            //Match Port
-            OFOxmInPort fieldPort = factory().oxms()
-                                                .inPort(OFPort.of(inport));
-            OFMatchV3 matchPort =
-                    factory()
-                            .buildMatchV3().
-                            setOxmList(OFOxmList.of(fieldPort)).build();
-
-
-            // Set Action outport ,sigType and sigID
-            List<OFAction> actionList = new ArrayList<>();
-            OFAction actionOutPort =
-                    factory().actions().output(OFPort.of(outport),
-                                                  0xffff);
-
-            OFActionCircuit actionCircuit = factory()
-                    .actions()
-                    .circuit(ofOxmOchSigidBasic);
-
-            actionList.add(actionCircuit);
-            actionList.add(actionOutPort);
-
-            OFInstruction instructionAction =
-                    factory().instructions().buildApplyActions()
-                                .setActions(actionList)
-                                .build();
-            List<OFInstruction> instructions =
-                    Collections.singletonList(instructionAction);
-
-            OFMessage opticalFlowEntry =
-                    factory().buildFlowAdd()
-                                .setMatch(matchPort)
-                                .setPriority(100)
-                                .setInstructions(instructions)
-                                .setXid(getNextTransactionId())
-                                .build();
-            log.debug("Adding optical flow in sw {}", getStringId());
-            List<OFMessage> msglist = new ArrayList<>(1);
-            msglist.add(opticalFlowEntry);
-            write(msglist);
-            sendBarrier(true);
-        } else if (getId() == 0x0000ffffffffff03L) {
-            final int inport = 30;
-            final int outport = 31;
-            //Circuit signal id
-            CircuitSignalID sigID = getSignalID(lambda);
-
-            OFOxmOchSigid fieldSigIDMatch = factory().oxms().ochSigid(sigID);
-            OFOxmOchSigtype fieldSigType = factory()
-                    .oxms()
-                    .ochSigtype(SIGNAL_TYPE);
-
-            OFOxmOchSigidBasic ofOxmOchSigidBasic =
-                    factory().oxms().ochSigidBasic(sigID);
-
-            //Match Port,SigType,SigID
-            OFOxmInPort fieldPort = factory()
-                    .oxms()
-                    .inPort(OFPort.of(inport));
-            OFMatchV3 matchPort = factory()
-                    .buildMatchV3()
-                    .setOxmList(OFOxmList.of(fieldPort,
-                                             fieldSigIDMatch,
-                                             fieldSigType
-                    ))
-                    .build();
-
-            // Set Action outport ,SigType, sigID
-            List<OFAction> actionList = new ArrayList<>();
-            OFAction actionOutPort =
-                    factory().actions().output(OFPort.of(outport),
-                                                  0xffff);
-
-            OFActionCircuit actionCircuit = factory()
-                    .actions()
-                    .circuit(ofOxmOchSigidBasic);
-
-
-
-            //actionList.add(setActionSigType);
-            actionList.add(actionCircuit);
-            actionList.add(actionOutPort);
-
-            OFInstruction instructionAction =
-                    factory().instructions().buildApplyActions()
-                                .setActions(actionList)
-                                .build();
-            List<OFInstruction> instructions =
-                    Collections.singletonList(instructionAction);
-
-            OFMessage opticalFlowEntry =
-                    factory().buildFlowAdd()
-                                .setMatch(matchPort)
-                                .setPriority(100)
-                                .setInstructions(instructions)
-                                .setXid(getNextTransactionId())
-                                .build();
-            log.debug("Adding optical flow in sw {}", getStringId());
-            List<OFMessage> msglist = new ArrayList<>(1);
-            msglist.add(opticalFlowEntry);
-            write(msglist);
-            sendBarrier(true);
-
-        } else if (getId() == 0x0000ffffffffff02L) {
-            final int inport = 21;
-            final int outport = 11;
-            //Circuit signal id
-            CircuitSignalID sigID = getSignalID(lambda);
-
-            OFOxmOchSigid fieldSigIDMatch = factory().oxms().ochSigid(sigID);
-            OFOxmOchSigtype fieldSigType = factory()
-                    .oxms()
-                    .ochSigtype(SIGNAL_TYPE);
-
-
-            //Match Port, sig type and sig id
-            OFOxmInPort fieldPort = factory()
-                    .oxms()
-                    .inPort(OFPort.of(inport));
-            OFMatchV3 matchPort =
-                    factory().buildMatchV3()
-                                .setOxmList(OFOxmList.of(fieldPort,
-                                                         fieldSigIDMatch,
-                                                         fieldSigType
-                                ))
-                                .build();
-
-            // Set Action outport
-            List<OFAction> actionList = new ArrayList<>();
-            OFAction actionOutPort =
-                    factory().actions().output(OFPort.of(outport),
-                                                  0xffff);
-
-            actionList.add(actionOutPort);
-
-            OFInstruction instructionAction =
-                    factory().instructions().buildApplyActions()
-                                .setActions(actionList)
-                                .build();
-            List<OFInstruction> instructions =
-                    Collections.singletonList(instructionAction);
-
-            OFMessage opticalFlowEntry =
-                    factory().buildFlowAdd()
-                                .setMatch(matchPort)
-                                .setPriority(100)
-                                .setInstructions(instructions)
-                                .setXid(getNextTransactionId())
-                                .build();
-            log.debug("Adding optical flow in sw {}", getStringId());
-            List<OFMessage> msglist = new ArrayList<>(1);
-            msglist.add(opticalFlowEntry);
-            write(msglist);
-            sendBarrier(true);
-        }
-
-    }
-    private void testReverseMA() throws IOException {
-        log.debug("LINC OE *** Testing MA ");
-        short lambda = 1;
-        if (getId() == 0x0000ffffffffff02L) {
-            final int inport = 11;
-            final int outport = 21;
-            //Circuit signal id
-            CircuitSignalID sigID = getSignalID(lambda);
-
-            OFOxmOchSigidBasic ofOxmOchSigidBasic =
-                    factory().oxms().ochSigidBasic(sigID);
-
-            //Match Port
-            OFOxmInPort fieldPort = factory().oxms()
-                                                .inPort(OFPort.of(inport));
-            OFMatchV3 matchPort =
-                    factory()
-                            .buildMatchV3().
-                            setOxmList(OFOxmList.of(fieldPort)).build();
-
-
-            // Set Action outport ,sigType and sigID
-            List<OFAction> actionList = new ArrayList<>();
-            OFAction actionOutPort =
-                    factory().actions().output(OFPort.of(outport),
-                                                  0xffff);
-
-            OFActionCircuit actionCircuit = factory()
-                    .actions()
-                    .circuit(ofOxmOchSigidBasic);
-            actionList.add(actionCircuit);
-            actionList.add(actionOutPort);
-
-            OFInstruction instructionAction =
-                    factory().instructions().buildApplyActions()
-                                .setActions(actionList)
-                                .build();
-            List<OFInstruction> instructions =
-                    Collections.singletonList(instructionAction);
-
-            OFMessage opticalFlowEntry =
-                    factory().buildFlowAdd()
-                                .setMatch(matchPort)
-                                .setPriority(100)
-                                .setInstructions(instructions)
-                                .setXid(getNextTransactionId())
-                                .build();
-            log.debug("Adding optical flow in sw {}", getStringId());
-            List<OFMessage> msglist = new ArrayList<>(1);
-            msglist.add(opticalFlowEntry);
-            write(msglist);
-            sendBarrier(true);
-        } else if (getId() == 0x0000ffffffffff03L) {
-            final int inport = 31;
-            final int outport = 30;
-            //Circuit signal id
-            CircuitSignalID sigID = getSignalID(lambda);
-
-            OFOxmOchSigid fieldSigIDMatch = factory().oxms().ochSigid(sigID);
-            OFOxmOchSigtype fieldSigType = factory()
-                    .oxms()
-                    .ochSigtype(SIGNAL_TYPE);
-
-            OFOxmOchSigidBasic ofOxmOchSigidBasic =
-                    factory().oxms().ochSigidBasic(sigID);
-
-            //Match Port,SigType,SigID
-            OFOxmInPort fieldPort = factory()
-                    .oxms()
-                    .inPort(OFPort.of(inport));
-            OFMatchV3 matchPort = factory()
-                    .buildMatchV3()
-                    .setOxmList(OFOxmList.of(fieldPort,
-                                             fieldSigIDMatch,
-                                             fieldSigType
-                    ))
-                    .build();
-
-            // Set Action outport ,SigType, sigID
-            List<OFAction> actionList = new ArrayList<>();
-            OFAction actionOutPort =
-                    factory().actions().output(OFPort.of(outport),
-                                                  0xffff);
-            OFActionCircuit actionCircuit = factory()
-                    .actions()
-                    .circuit(ofOxmOchSigidBasic);
-
-            actionList.add(actionCircuit);
-            actionList.add(actionOutPort);
-
-            OFInstruction instructionAction =
-                    factory().instructions().buildApplyActions()
-                                .setActions(actionList)
-                                .build();
-            List<OFInstruction> instructions =
-                    Collections.singletonList(instructionAction);
-
-            OFMessage opticalFlowEntry =
-                    factory().buildFlowAdd()
-                                .setMatch(matchPort)
-                                .setPriority(100)
-                                .setInstructions(instructions)
-                                .setXid(getNextTransactionId())
-                                .build();
-            log.debug("Adding optical flow in sw {}", getStringId());
-            List<OFMessage> msglist = new ArrayList<>(1);
-            msglist.add(opticalFlowEntry);
-            write(msglist);
-            sendBarrier(true);
-
-        } else if (getId() == 0x0000ffffffffff01L) {
-            final int inport = 20;
-            final int outport = 10;
-            //Circuit signal id
-            CircuitSignalID sigID = getSignalID(lambda);
-
-            OFOxmOchSigid fieldSigIDMatch = factory().oxms().ochSigid(sigID);
-            OFOxmOchSigtype fieldSigType = factory()
-                    .oxms()
-                    .ochSigtype(SIGNAL_TYPE);
-
-
-            //Match Port, sig type and sig id
-            OFOxmInPort fieldPort = factory()
-                    .oxms()
-                    .inPort(OFPort.of(inport));
-            OFMatchV3 matchPort =
-                    factory().buildMatchV3()
-                                .setOxmList(OFOxmList.of(fieldPort,
-                                                         fieldSigIDMatch,
-                                                         fieldSigType
-                                ))
-                                .build();
-
-            // Set Action outport
-            List<OFAction> actionList = new ArrayList<>();
-            OFAction actionOutPort =
-                    factory().actions().output(OFPort.of(outport),
-                                                  0xffff);
-
-            actionList.add(actionOutPort);
-
-            OFInstruction instructionAction =
-                    factory().instructions().buildApplyActions()
-                                .setActions(actionList)
-                                .build();
-            List<OFInstruction> instructions =
-                    Collections.singletonList(instructionAction);
-
-            OFMessage opticalFlowEntry =
-                    factory().buildFlowAdd()
-                                .setMatch(matchPort)
-                                .setPriority(100)
-                                .setInstructions(instructions)
-                                .setXid(getNextTransactionId())
-                                .build();
-            log.debug("Adding optical flow in sw {}", getStringId());
-            List<OFMessage> msglist = new ArrayList<>(1);
-            msglist.add(opticalFlowEntry);
-            write(msglist);
-            sendBarrier(true);
-        }
-
-    }
-
-
-    // Todo remove - for testing purpose only
-    private static CircuitSignalID getSignalID(short lambda) {
-        byte myGrid = 1;
-        byte myCs = 2;
-        short myCn = lambda;
-        short mySw = 1;
-
-        CircuitSignalID signalID = new CircuitSignalID(myGrid,
-                                                       myCs,
-                                                       myCn,
-                                                       mySw);
-        return signalID;
-    }
-
-    private void sendBarrier(boolean finalBarrier) throws IOException {
-        int xid = getNextTransactionId();
-        if (finalBarrier) {
-            barrierXidToWaitFor = xid;
-        }
-        OFBarrierRequest br = factory()
-                .buildBarrierRequest()
-                .setXid(xid)
-                .build();
-        sendMsg(br);
-    }
-
     @Override
     public void write(OFMessage msg) {
         this.channel.write(Collections.singletonList(msg));
diff --git a/pom.xml b/pom.xml
index 8aa5ce4..095e40e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,6 +4,10 @@
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
+    <prerequisites>
+        <maven>3.0.0</maven>
+    </prerequisites>
+
     <groupId>org.onlab.onos</groupId>
     <artifactId>onos</artifactId>
     <packaging>pom</packaging>
@@ -28,7 +32,6 @@
 
     <licenses>
         <license>
-            <!-- TODO: Is this really our license scheme? -->
             <name>Apache License, Version 2.0</name>
             <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
         </license>
@@ -192,7 +195,7 @@
             <dependency>
                 <groupId>com.hazelcast</groupId>
                 <artifactId>hazelcast</artifactId>
-                <version>3.3</version>
+                <version>3.3.2</version>
             </dependency>
             <dependency>
                 <groupId>com.eclipsesource.minimal-json</groupId>
@@ -245,7 +248,7 @@
             <dependency>
                 <groupId>org.onlab.onos</groupId>
                 <artifactId>onlab-junit</artifactId>
-                <version>1.0.0-SNAPSHOT</version>
+                <version>${project.version}</version>
                 <scope>test</scope>
             </dependency>
 
@@ -528,59 +531,6 @@
                     </execution>
                 </executions>
             </plugin>
-
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-javadoc-plugin</artifactId>
-                <version>2.10.1</version>
-                <configuration>
-                    <show>package</show>
-                    <docfilessubdirs>true</docfilessubdirs>
-                    <doctitle>ONOS Java API</doctitle>
-                    <groups>
-                        <group>
-                            <title>Network Model &amp; Services</title>
-                            <packages>
-                                org.onlab.onos:org.onlab.onos.*
-                            </packages>
-                        </group>
-                        <group>
-                            <title>Core Subsystems</title>
-                            <packages>
-                                org.onlab.onos.impl:org.onlab.onos.cluster.impl:org.onlab.onos.net.device.impl:org.onlab.onos.net.link.impl:org.onlab.onos.net.host.impl:org.onlab.onos.net.topology.impl:org.onlab.onos.net.packet.impl:org.onlab.onos.net.flow.impl:org.onlab.onos.store.trivial.*:org.onlab.onos.net.*.impl:org.onlab.onos.event.impl:org.onlab.onos.store.*:org.onlab.onos.net.intent.impl:org.onlab.onos.net.proxyarp.impl:org.onlab.onos.mastership.impl:org.onlab.onos.json:org.onlab.onos.json.*:org.onlab.onos.provider.host.impl:org.onlab.onos.provider.lldp.impl:org.onlab.onos.net.statistic.impl
-                            </packages>
-                        </group>
-                        <group>
-                            <title>OpenFlow Providers &amp; Controller
-                            </title>
-                            <packages>
-                                org.onlab.onos.provider.of.*:org.onlab.onos.openflow.*
-                            </packages>
-                        </group>
-                        <group>
-                            <title>Utilities</title>
-                            <packages>
-                                org.onlab.*
-                            </packages>
-                        </group>
-                        <group>
-                            <title>GUI, REST &amp; Command-Line</title>
-                            <packages>
-                                org.onlab.onos.gui:org.onlab.onos.rest:org.onlab.onos.cli:org.onlab.onos.gui.*:org.onlab.onos.rest.*:org.onlab.onos.cli.*
-                            </packages>
-                        </group>
-                        <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.*
-                            </packages>
-                        </group>
-                    </groups>
-                    <excludePackageNames>org.onlab.thirdparty
-                    </excludePackageNames>
-                </configuration>
-            </plugin>
-
         </plugins>
     </build>
 
@@ -607,10 +557,6 @@
                     </rulesets>
                 </configuration>
             </plugin>
-
         </plugins>
     </reporting>
-    <prerequisites>
-        <maven>3.0.0</maven>
-    </prerequisites>
 </project>
diff --git a/tools/build/onos-build b/tools/build/onos-build
index cf2ebe4..2f31eeb 100755
--- a/tools/build/onos-build
+++ b/tools/build/onos-build
@@ -6,5 +6,4 @@
 [ ! -d "$ONOS_ROOT" ] && echo "ONOS_ROOT is not defined" >&2 && exit 1
 . $ONOS_ROOT/tools/build/envDefaults
 
-cd $ONOS_ROOT
-mvn clean install && mvn javadoc:aggregate
+cd $ONOS_ROOT && mvn clean install && cd docs && mvn javadoc:aggregate
diff --git a/utils/junit/pom.xml b/utils/junit/pom.xml
index d994a07..6ddadac 100644
--- a/utils/junit/pom.xml
+++ b/utils/junit/pom.xml
@@ -27,6 +27,16 @@
             <artifactId>guava-testlib</artifactId>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest-core</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.hamcrest</groupId>
+            <artifactId>hamcrest-library</artifactId>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 
 </project>
diff --git a/core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java b/utils/junit/src/main/java/org/onlab/junit/ImmutableClassChecker.java
similarity index 97%
rename from core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java
rename to utils/junit/src/main/java/org/onlab/junit/ImmutableClassChecker.java
index 0e63af9..30d895e 100644
--- a/core/api/src/test/java/org/onlab/onos/net/intent/ImmutableClassChecker.java
+++ b/utils/junit/src/main/java/org/onlab/junit/ImmutableClassChecker.java
@@ -1,5 +1,4 @@
-package org.onlab.onos.net.intent;
-//TODO is this the right package?
+package org.onlab.junit;
 
 import org.hamcrest.Description;
 import org.hamcrest.StringDescription;
diff --git a/utils/misc/src/main/java/org/onlab/util/TestUtils.java b/utils/junit/src/main/java/org/onlab/junit/TestUtils.java
similarity index 99%
rename from utils/misc/src/main/java/org/onlab/util/TestUtils.java
rename to utils/junit/src/main/java/org/onlab/junit/TestUtils.java
index 7e59564..64b7ae300 100644
--- a/utils/misc/src/main/java/org/onlab/util/TestUtils.java
+++ b/utils/junit/src/main/java/org/onlab/junit/TestUtils.java
@@ -1,4 +1,4 @@
-package org.onlab.util;
+package org.onlab.junit;
 
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
diff --git a/utils/junit/src/main/java/org/onlab/junit/UtilityClassChecker.java b/utils/junit/src/main/java/org/onlab/junit/UtilityClassChecker.java
new file mode 100644
index 0000000..bd272d1
--- /dev/null
+++ b/utils/junit/src/main/java/org/onlab/junit/UtilityClassChecker.java
@@ -0,0 +1,134 @@
+package org.onlab.junit;
+
+import org.hamcrest.Description;
+import org.hamcrest.StringDescription;
+import org.onlab.junit.TestUtils.TestUtilsException;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+
+/**
+ * Hamcrest style class for verifying that a class follows the
+ * accepted rules for utility classes.
+ *
+ * The rules that are enforced for utility classes:
+ *    - the class must be declared final
+ *    - the class must have only one constructor
+ *    - the constructor must be private and inaccessible to callers
+ *    - the class must have only static methods
+ */
+
+public class UtilityClassChecker {
+
+    private String failureReason = "";
+
+    /**
+     * Method to determine if a given class is a properly specified
+     * utility class.  In addition to checking that the class meets the criteria
+     * for utility classes, an object of the class type is allocated to force
+     * test code coverage onto the class constructor.
+     *
+     * @param clazz the class to check
+     * @return true if the given class is a properly specified utility class.
+     */
+    private boolean isProperlyDefinedUtilityClass(Class<?> clazz) {
+        // class must be declared final
+        if (!Modifier.isFinal(clazz.getModifiers())) {
+            failureReason = "a class that is not final";
+            return false;
+        }
+
+        // class must have only one constructor
+        final Constructor<?>[] constructors = clazz.getDeclaredConstructors();
+        if (constructors.length != 1) {
+            failureReason = "a class with more than one constructor";
+            return false;
+        }
+
+        //  constructor must not be accessible outside of the class
+        final Constructor<?> constructor = constructors[0];
+        if (constructor.isAccessible()) {
+            failureReason = "a class with an accessible default constructor";
+            return false;
+        }
+
+        // constructor must be private
+        if (!Modifier.isPrivate(constructor.getModifiers())) {
+            failureReason = "a class with a default constructor that is not private";
+            return false;
+        }
+
+        // class must have only static methods
+        for (final Method method : clazz.getMethods()) {
+            if (method.getDeclaringClass().equals(clazz)) {
+                if (!Modifier.isStatic(method.getModifiers())) {
+                    failureReason = "a class with one or more non-static methods";
+                    return false;
+                }
+            }
+
+        }
+
+        try {
+            final Object newObject = TestUtils.callConstructor(constructor);
+            if (newObject == null) {
+                failureReason = "could not instantiate a new object";
+                return false;
+            }
+        } catch (TestUtilsException e) {
+            failureReason = "could not instantiate a new object";
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Describe why an error was reported.  Uses Hamcrest style Description
+     * interfaces.
+     *
+     * @param description the Description object to use for reporting the
+     *                    mismatch
+     */
+    public void describeMismatch(Description description) {
+        description.appendText(failureReason);
+    }
+
+    /**
+     * Describe the source object that caused an error, using a Hamcrest
+     * Matcher style interface.  In this case, it always returns
+     * that we are looking for a properly defined utility class.
+     *
+     * @param description the Description object to use to report the "to"
+     *                    object
+     */
+    public void describeTo(Description description) {
+        description.appendText("a properly defined utility class");
+    }
+
+    /**
+     * Assert that the given class adheres to the utility class rules.
+     *
+     * @param clazz the class to check
+     *
+     * @throws java.lang.AssertionError if the class is not a valid
+     *         utility class
+     */
+    public static void assertThatClassIsUtility(Class<?> clazz) {
+        final UtilityClassChecker checker = new UtilityClassChecker();
+        if (!checker.isProperlyDefinedUtilityClass(clazz)) {
+            final Description toDescription = new StringDescription();
+            final Description mismatchDescription = new StringDescription();
+
+            checker.describeTo(toDescription);
+            checker.describeMismatch(mismatchDescription);
+            final String reason =
+                "\n" +
+                "Expected: is \"" + toDescription.toString() + "\"\n" +
+                "    but : was \"" + mismatchDescription.toString() + "\"";
+
+            throw new AssertionError(reason);
+        }
+    }
+}
diff --git a/utils/junit/src/test/java/org/onlab/junit/ImmutableClassCheckerTest.java b/utils/junit/src/test/java/org/onlab/junit/ImmutableClassCheckerTest.java
new file mode 100644
index 0000000..b4a6ff5
--- /dev/null
+++ b/utils/junit/src/test/java/org/onlab/junit/ImmutableClassCheckerTest.java
@@ -0,0 +1,120 @@
+package org.onlab.junit;
+
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+
+/**
+ * Set of unit tests to check the implementation of the immutable class
+ * checker.
+ */
+public class ImmutableClassCheckerTest {
+    /**
+     * Test class for non final class check.
+     */
+    // CHECKSTYLE IGNORE FinalClass FOR NEXT 1 LINES
+    static class NonFinal {
+        private NonFinal() { }
+    }
+
+    /**
+     * Check that a non final class correctly produces an error.
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testNonFinalClass() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsImmutable(NonFinal.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                    containsString("is not final"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class for non private member class check.
+     */
+    static final class FinalProtectedMember {
+        protected final int x = 0;
+    }
+
+    /**
+     * Check that a final class with a non-private member is properly detected.
+     *
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testFinalProtectedMember() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsImmutable(FinalProtectedMember.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                       containsString("a field named 'x' that is not private"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class for non private member class check.
+     */
+    static final class NotFinalPrivateMember {
+        private int x = 0;
+    }
+
+    /**
+     * Check that a final class with a non-final private
+     * member is properly detected.
+     *
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testNotFinalPrivateMember() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsImmutable(NotFinalPrivateMember.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                    containsString("a field named 'x' that is not final"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class for non private member class check.
+     */
+    static final class ClassWithSetter {
+        private final int x = 0;
+        public void setX(int newX) {
+        }
+    }
+
+    /**
+     * Check that a final class with a final private
+     * member that is modifyable by a setter is properly detected.
+     *
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testClassWithSetter() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsImmutable(ClassWithSetter.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                    containsString("a class with a setter named 'setX'"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+}
+
diff --git a/utils/misc/src/test/java/org/onlab/util/TestUtilsTest.java b/utils/junit/src/test/java/org/onlab/junit/TestUtilsTest.java
similarity index 97%
rename from utils/misc/src/test/java/org/onlab/util/TestUtilsTest.java
rename to utils/junit/src/test/java/org/onlab/junit/TestUtilsTest.java
index 58e60c1..c57b351 100644
--- a/utils/misc/src/test/java/org/onlab/util/TestUtilsTest.java
+++ b/utils/junit/src/test/java/org/onlab/junit/TestUtilsTest.java
@@ -1,4 +1,4 @@
-package org.onlab.util;
+package org.onlab.junit;
 
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
@@ -6,7 +6,7 @@
 
 import org.junit.Before;
 import org.junit.Test;
-import org.onlab.util.TestUtils.TestUtilsException;
+import org.onlab.junit.TestUtils.TestUtilsException;
 
 /**
  * Test and usage examples for TestUtils.
diff --git a/utils/junit/src/test/java/org/onlab/junit/UtilityClassCheckerTest.java b/utils/junit/src/test/java/org/onlab/junit/UtilityClassCheckerTest.java
new file mode 100644
index 0000000..7f56d81
--- /dev/null
+++ b/utils/junit/src/test/java/org/onlab/junit/UtilityClassCheckerTest.java
@@ -0,0 +1,145 @@
+package org.onlab.junit;
+
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.onlab.junit.UtilityClassChecker.assertThatClassIsUtility;
+
+/**
+ * Set of unit tests to check the implementation of the utility class
+ * checker.
+ */
+public class UtilityClassCheckerTest {
+
+    // CHECKSTYLE:OFF test data intentionally not final
+    /**
+     * Test class for non final class check.
+     */
+    static class NonFinal {
+        private NonFinal() { }
+    }
+    // CHECKSTYLE:ON
+
+    /**
+     * Check that a non final class correctly produces an error.
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testNonFinalClass() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsUtility(NonFinal.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                       containsString("is not final"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class for final no constructor class check.
+     */
+    static final class FinalNoConstructor {
+    }
+
+    /**
+     * Check that a final class with no declared constructor correctly produces
+     * an error.  In this case, the compiler generates a default constructor
+     * for you, but the constructor is 'protected' and will fail the check.
+     *
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testFinalNoConstructorClass() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsUtility(FinalNoConstructor.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                    containsString("class with a default constructor that " +
+                                   "is not private"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class for class with more than one constructor check.
+     */
+    static final class TwoConstructors {
+        private TwoConstructors() { }
+        private TwoConstructors(int x) { }
+    }
+
+    /**
+     * Check that a non static class correctly produces an error.
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testOnlyOneConstructor() throws Exception {
+        boolean gotException = false;
+        try {
+            assertThatClassIsUtility(TwoConstructors.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                       containsString("more than one constructor"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class with a non private constructor.
+     */
+    static final class NonPrivateConstructor {
+        protected NonPrivateConstructor() { }
+    }
+
+    /**
+     * Check that a class with a non private constructor correctly
+     * produces an error.
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testNonPrivateConstructor() throws Exception {
+
+        boolean gotException = false;
+        try {
+            assertThatClassIsUtility(NonPrivateConstructor.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                       containsString("constructor that is not private"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+
+    /**
+     * Test class with a non static method.
+     */
+    static final class NonStaticMethod {
+        private NonStaticMethod() { }
+        public void aPublicMethod() { }
+    }
+
+    /**
+     * Check that a class with a non static method correctly produces an error.
+     * @throws Exception if any of the reflection lookups fail.
+     */
+    @Test
+    public void testNonStaticMethod() throws Exception {
+
+        boolean gotException = false;
+        try {
+            assertThatClassIsUtility(NonStaticMethod.class);
+        } catch (AssertionError assertion) {
+            assertThat(assertion.getMessage(),
+                       containsString("one or more non-static methods"));
+            gotException = true;
+        }
+        assertThat(gotException, is(true));
+    }
+}
diff --git a/utils/misc/pom.xml b/utils/misc/pom.xml
index d1d3a50..fcb0eb8 100644
--- a/utils/misc/pom.xml
+++ b/utils/misc/pom.xml
@@ -24,6 +24,7 @@
         <dependency>
             <groupId>org.onlab.onos</groupId>
             <artifactId>onlab-junit</artifactId>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>io.netty</groupId>
diff --git a/utils/misc/src/main/java/org/onlab/metrics/EventMetric.java b/utils/misc/src/main/java/org/onlab/metrics/EventMetric.java
new file mode 100644
index 0000000..464a123
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/metrics/EventMetric.java
@@ -0,0 +1,103 @@
+package org.onlab.metrics;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.Meter;
+
+/**
+ * Metric measurements for events.
+ */
+public class EventMetric {
+    private static final String GAUGE_TIMESTAMP_NAME = "Timestamp.EpochMs";
+    private static final String METER_RATE_NAME = "Rate";
+
+    private final MetricsService metricsService;
+    private final String componentName;
+    private final String featureName;
+
+    private MetricsComponent metricsComponent;
+    private MetricsFeature metricsFeature;
+
+    private volatile long lastEventTimestampEpochMs = 0;
+    private Gauge<Long> lastEventTimestampGauge;
+    private Meter eventRateMeter;
+
+    /**
+     * Constructor.
+     *
+     * @param metricsService the Metrics Service to use for Metrics
+     * registration and deregistration
+     * @param componentName the Metrics Component Name to use for Metrics
+     * registration and deregistration
+     * @param featureName the Metrics Feature Name to use for Metrics
+     * registration and deregistration
+     */
+    public EventMetric(MetricsService metricsService, String componentName,
+                       String featureName) {
+        this.metricsService = metricsService;
+        this.componentName = componentName;
+        this.featureName = featureName;
+    }
+
+    /**
+     * Registers the metrics.
+     */
+    public void registerMetrics() {
+        metricsComponent = metricsService.registerComponent(componentName);
+        metricsFeature = metricsComponent.registerFeature(featureName);
+
+        lastEventTimestampEpochMs = 0;
+        lastEventTimestampGauge =
+            metricsService.registerMetric(metricsComponent,
+                                          metricsFeature,
+                                          GAUGE_TIMESTAMP_NAME,
+                                          new Gauge<Long>() {
+                                              @Override
+                                              public Long getValue() {
+                                                  return lastEventTimestampEpochMs;
+                                              }
+                                          });
+
+        eventRateMeter = metricsService.createMeter(metricsComponent,
+                                                    metricsFeature,
+                                                    METER_RATE_NAME);
+    }
+
+    /**
+     * Removes the metrics.
+     */
+    public void removeMetrics() {
+        lastEventTimestampEpochMs = 0;
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeature,
+                                    GAUGE_TIMESTAMP_NAME);
+        metricsService.removeMetric(metricsComponent,
+                                    metricsFeature,
+                                    METER_RATE_NAME);
+    }
+
+    /**
+     * Updates the metric measurements for a single event.
+     */
+    public void eventReceived() {
+        lastEventTimestampEpochMs = System.currentTimeMillis();
+        eventRateMeter.mark(1);
+    }
+
+    /**
+     * Gets the last event timestamp Gauge (ms from the Epoch).
+     *
+     * @return the last event timestamp Gauge (ms from the Epoch)
+     */
+    public Gauge<Long> lastEventTimestampGauge() {
+        return lastEventTimestampGauge;
+    }
+
+    /**
+     * Gets the event rate meter.
+     *
+     * @return the event rate meter
+     */
+    public Meter eventRateMeter() {
+        return eventRateMeter;
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip4Address.java b/utils/misc/src/main/java/org/onlab/packet/Ip4Address.java
new file mode 100644
index 0000000..1571cf6
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip4Address.java
@@ -0,0 +1,202 @@
+package org.onlab.packet;
+
+import java.nio.ByteBuffer;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * The 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;
+
+    /**
+     * Default constructor.
+     */
+    public Ip4Address() {
+        this.value = 0;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param other the object to copy from
+     */
+    public Ip4Address(Ip4Address other) {
+        this.value = other.value;
+    }
+
+    /**
+     * Constructor from an integer value.
+     *
+     * @param value the value to use
+     */
+    public Ip4Address(int value) {
+        this.value = value;
+    }
+
+    /**
+     * Constructor from a byte array with the IPv4 address stored in network
+     * byte order (i.e., the most significant byte first).
+     *
+     * @param value the value to use
+     */
+    public Ip4Address(byte[] value) {
+        this(value, 0);
+    }
+
+    /**
+     * 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.
+     *
+     * @param value the value to use
+     * @param offset the offset in bytes from the beginning of the byte array
+     */
+    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);
+    }
+
+    /**
+     * Constructs an IPv4 address from a string representation of the address.
+     *<p>
+     * Example: "1.2.3.4"
+     *
+     * @param value the value to use
+     */
+    public Ip4Address(String value) {
+        checkNotNull(value);
+
+        String[] splits = value.split("\\.");
+        if (splits.length != 4) {
+            final String msg = "Invalid IPv4 address string: " + value;
+            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);
+        }
+        this.value = result;
+    }
+
+    /**
+     * Gets the IPv4 address as a byte array.
+     *
+     * @return a byte array with the IPv4 address stored in network byte order
+     * (i.e., the most significant byte first).
+     */
+    public byte[] toOctets() {
+        return ByteBuffer.allocate(BYTE_LENGTH).putInt(value).array();
+    }
+
+    /**
+     * Creates an IPv4 network mask prefix.
+     *
+     * @param prefixLen 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
+     */
+    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);
+    }
+
+    /**
+     * 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].
+     * @return a new IPv4 address that is masked with a mask prefix of the
+     * specified length
+     */
+    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);
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java b/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java
new file mode 100644
index 0000000..4d0c755
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip4Prefix.java
@@ -0,0 +1,123 @@
+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
+
+    /**
+     * Default constructor.
+     */
+    public Ip4Prefix() {
+        this.address = new Ip4Address();
+        this.prefixLen = 0;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param other the object to copy from
+     */
+    public Ip4Prefix(Ip4Prefix other) {
+        this.address = new Ip4Address(other.address);
+        this.prefixLen = other.prefixLen;
+    }
+
+    /**
+     * Constructor for a given address and prefix length.
+     *
+     * @param address   the address to use
+     * @param prefixLen the prefix length to use
+     */
+    public Ip4Prefix(Ip4Address address, short prefixLen) {
+        this.address = Ip4Address.makeMaskedAddress(address, prefixLen);
+        this.prefixLen = prefixLen;
+    }
+
+    /**
+     * Constructs an IPv4 prefix from a string representation of the
+     * prefix.
+     *<p>
+     * Example: "1.2.0.0/16"
+     *
+     * @param value the value to use
+     */
+    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 '/'");
+        }
+        this.prefixLen = Short.decode(splits[1]);
+        this.address = Ip4Address.makeMaskedAddress(new Ip4Address(splits[0]),
+                this.prefixLen);
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Compares the value of two Ip4Prefix objects.
+     * <p/>
+     * Note the value of the IPv4 address is compared directly between the
+     * objects, and must match exactly for the objects to be considered equal.
+     * This may result in objects which represent the same IP prefix being
+     * classified as unequal, because the unsignificant bits of the address
+     * field don't match (the bits to the right of the prefix length).
+     * <p/>
+     * TODO Change this behavior so that objects that represent the same prefix
+     * are classified as equal according to this equals method.
+     *
+     * @see Object#equals(Object)
+     */
+    @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);
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip6Address.java b/utils/misc/src/main/java/org/onlab/packet/Ip6Address.java
new file mode 100644
index 0000000..57e893f
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip6Address.java
@@ -0,0 +1,260 @@
+package org.onlab.packet;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+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.
+ * 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;
+
+    /**
+     * Default constructor.
+     */
+    public Ip6Address() {
+        this.valueHigh = 0;
+        this.valueLow = 0;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param other the object to copy from
+     */
+    public Ip6Address(Ip6Address other) {
+        this.valueHigh = other.valueHigh;
+        this.valueLow = other.valueLow;
+    }
+
+    /**
+     * 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.
+     *
+     * @param value the value to use
+     * @param offset the offset in bytes from the beginning of the byte array
+     */
+    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();
+    }
+
+    /**
+     * Constructs an IPv6 address from a string representation of the address.
+     *<p>
+     * Example: "1111:2222::8888"
+     *
+     * @param value the value to use
+     */
+    public Ip6Address(String value) {
+        checkNotNull(value);
+
+        if (value.isEmpty()) {
+            final String msg = "Specified IPv6 cannot be an empty string";
+            throw new IllegalArgumentException(msg);
+        }
+        InetAddress addr = null;
+        try {
+            addr = InetAddresses.forString(value);
+        } catch (IllegalArgumentException e) {
+            final String msg = "Invalid IPv6 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();
+    }
+
+    /**
+     * Creates an IPv6 network mask prefix.
+     *
+     * @param prefixLen 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
+     */
+    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);
+    }
+
+    /**
+     * 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].
+     * @return a new IPv6 address that is masked with a mask prefix of the
+     * specified length
+     */
+    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;
+    }
+}
diff --git a/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java b/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java
new file mode 100644
index 0000000..d38f505
--- /dev/null
+++ b/utils/misc/src/main/java/org/onlab/packet/Ip6Prefix.java
@@ -0,0 +1,123 @@
+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
+
+    /**
+     * Default constructor.
+     */
+    public Ip6Prefix() {
+        this.address = new Ip6Address();
+        this.prefixLen = 0;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param other the object to copy from
+     */
+    public Ip6Prefix(Ip6Prefix other) {
+        this.address = new Ip6Address(other.address);
+        this.prefixLen = other.prefixLen;
+    }
+
+    /**
+     * Constructor for a given address and prefix length.
+     *
+     * @param address   the address to use
+     * @param prefixLen the prefix length to use
+     */
+    public Ip6Prefix(Ip6Address address, short prefixLen) {
+        this.address = Ip6Address.makeMaskedAddress(address, prefixLen);
+        this.prefixLen = prefixLen;
+    }
+
+    /**
+     * Constructs an IPv6 prefix from a string representation of the
+     * prefix.
+     *<p>
+     * Example: "1111:2222::/32"
+     *
+     * @param value the value to use
+     */
+    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 '/'");
+        }
+        this.prefixLen = Short.decode(splits[1]);
+        this.address = Ip6Address.makeMaskedAddress(new Ip6Address(splits[0]),
+                this.prefixLen);
+    }
+
+    /**
+     * 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;
+    }
+
+    /**
+     * Compares the value of two Ip6Prefix objects.
+     * <p/>
+     * Note the value of the IPv6 address is compared directly between the
+     * objects, and must match exactly for the objects to be considered equal.
+     * This may result in objects which represent the same IP prefix being
+     * classified as unequal, because the unsignificant bits of the address
+     * field don't match (the bits to the right of the prefix length).
+     * <p/>
+     * TODO Change this behavior so that objects that represent the same prefix
+     * are classified as equal according to this equals method.
+     *
+     * @see Object#equals(Object)
+     */
+    @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);
+    }
+}
diff --git a/utils/misc/src/test/java/org/onlab/packet/Ip4AddressTest.java b/utils/misc/src/test/java/org/onlab/packet/Ip4AddressTest.java
new file mode 100644
index 0000000..6680af4
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip4AddressTest.java
@@ -0,0 +1,330 @@
+package org.onlab.packet;
+
+import org.junit.Test;
+
+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;
+
+/**
+ * Tests for class {@link Ip4Address}.
+ */
+public class Ip4AddressTest {
+    /**
+     * Tests the immutability of {@link Ip4Address}.
+     */
+    @Test
+    public void testImmutable() {
+        assertThatClassIsImmutable(Ip4Address.class);
+    }
+
+    /**
+     * Tests the length of the address in bytes (octets).
+     */
+    @Test
+    public void testAddrBytelen() {
+        assertThat(Ip4Address.BYTE_LENGTH, is(4));
+    }
+
+    /**
+     * Tests the length of the address in bits.
+     */
+    @Test
+    public void testAddrBitlen() {
+        assertThat(Ip4Address.BIT_LENGTH, is(32));
+    }
+
+    /**
+     * Tests default class constructor.
+     */
+    @Test
+    public void testDefaultConstructor() {
+        Ip4Address ip4Address = new Ip4Address();
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+    }
+
+    /**
+     * Tests valid class copy constructor.
+     */
+    @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"));
+
+        fromAddr = new Ip4Address("0.0.0.0");
+        ip4Address = new Ip4Address(fromAddr);
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+
+        fromAddr = new Ip4Address("255.255.255.255");
+        ip4Address = new Ip4Address(fromAddr);
+        assertThat(ip4Address.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests invalid class copy constructor for a null object to copy from.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullObject() {
+        Ip4Address fromAddr = null;
+        Ip4Address ip4Address = new Ip4Address(fromAddr);
+    }
+
+    /**
+     * 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.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructorShortArray() {
+        final byte[] fromArray = new byte[] {1, 2, 3};
+        Ip4Address ip4Address = new Ip4Address(fromArray);
+    }
+
+    /**
+     * 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.
+     */
+    @Test
+    public void testConstructorForString() {
+        Ip4Address ip4Address = new Ip4Address("1.2.3.4");
+        assertThat(ip4Address.toString(), is("1.2.3.4"));
+
+        ip4Address = new Ip4Address("0.0.0.0");
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+
+        ip4Address = new Ip4Address("255.255.255.255");
+        assertThat(ip4Address.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests invalid class constructor for a null string.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullString() {
+        String fromString = null;
+        Ip4Address ip4Address = new Ip4Address(fromString);
+    }
+
+    /**
+     * Tests invalid class constructor for an empty string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructors() {
+        // Check constructor for invalid ID: empty string
+        Ip4Address ip4Address = new Ip4Address("");
+    }
+
+    /**
+     * Tests returning the address as a byte array.
+     */
+    @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));
+
+        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));
+    }
+
+    /**
+     * Tests making a mask prefix for a given prefix length.
+     */
+    @Test
+    public void testMakeMaskPrefix() {
+        Ip4Address ip4Address = Ip4Address.makeMaskPrefix(25);
+        assertThat(ip4Address.toString(), is("255.255.255.128"));
+
+        ip4Address = Ip4Address.makeMaskPrefix(0);
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+
+        ip4Address = Ip4Address.makeMaskPrefix(32);
+        assertThat(ip4Address.toString(), is("255.255.255.255"));
+    }
+
+    /**
+     * Tests making of a masked address.
+     */
+    @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"));
+
+        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"));
+    }
+
+    /**
+     * Tests getting the value of an address.
+     */
+    @Test
+    public void testGetValue() {
+        Ip4Address ip4Address = new Ip4Address("1.2.3.4");
+        assertThat(ip4Address.getValue(), is(0x01020304));
+
+        ip4Address = new Ip4Address("0.0.0.0");
+        assertThat(ip4Address.getValue(), is(0));
+
+        ip4Address = new Ip4Address("255.255.255.255");
+        assertThat(ip4Address.getValue(), is(-1));
+    }
+
+    /**
+     * Tests equality of {@link Ip4Address}.
+     */
+    @Test
+    public void testEquality() {
+        Ip4Address addr1 = new Ip4Address("1.2.3.4");
+        Ip4Address addr2 = new Ip4Address("1.2.3.4");
+        assertThat(addr1, is(addr2));
+
+        addr1 = new Ip4Address("0.0.0.0");
+        addr2 = new Ip4Address("0.0.0.0");
+        assertThat(addr1, is(addr2));
+
+        addr1 = new Ip4Address("255.255.255.255");
+        addr2 = new Ip4Address("255.255.255.255");
+        assertThat(addr1, is(addr2));
+    }
+
+    /**
+     * Tests non-equality of {@link Ip4Address}.
+     */
+    @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)));
+    }
+
+    /**
+     * Tests comparison of {@link Ip4Address}.
+     */
+    @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");
+        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");
+        assertTrue(addr1.compareTo(addr2) == 0);
+        assertTrue(addr1.compareTo(addr3) > 0);
+        assertTrue(addr1.compareTo(addr4) < 0);
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        Ip4Address ip4Address = new Ip4Address("1.2.3.4");
+        assertThat(ip4Address.toString(), is("1.2.3.4"));
+
+        ip4Address = new Ip4Address("0.0.0.0");
+        assertThat(ip4Address.toString(), is("0.0.0.0"));
+
+        ip4Address = new Ip4Address("255.255.255.255");
+        assertThat(ip4Address.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
new file mode 100644
index 0000000..3e502d3
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip4PrefixTest.java
@@ -0,0 +1,195 @@
+package org.onlab.packet;
+
+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.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+
+/**
+ * Tests for class {@link Ip4Prefix}.
+ */
+public class Ip4PrefixTest {
+    /**
+     * Tests the immutability of {@link Ip4Prefix}.
+     */
+    @Test
+    public void testImmutable() {
+        assertThatClassIsImmutable(Ip4Prefix.class);
+    }
+
+    /**
+     * Tests default class constructor.
+     */
+    @Test
+    public void testDefaultConstructor() {
+        Ip4Prefix ip4prefix = new Ip4Prefix();
+        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
+    }
+
+    /**
+     * Tests valid class copy constructor.
+     */
+    @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"));
+    }
+
+    /**
+     * 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.
+     */
+    @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"));
+
+        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"));
+    }
+
+    /**
+     * Tests valid class constructor for a string.
+     */
+    @Test
+    public void testConstructorForString() {
+        Ip4Prefix ip4prefix = new Ip4Prefix("1.2.3.0/24");
+        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+
+        ip4prefix = new Ip4Prefix("1.2.3.4/24");
+        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+
+        ip4prefix = new Ip4Prefix("1.2.3.5/32");
+        assertThat(ip4prefix.toString(), is("1.2.3.5/32"));
+
+        ip4prefix = new Ip4Prefix("0.0.0.0/0");
+        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
+
+        ip4prefix = new Ip4Prefix("255.255.255.255/32");
+        assertThat(ip4prefix.toString(), is("255.255.255.255/32"));
+    }
+
+    /**
+     * Tests invalid class constructor for a null string.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullString() {
+        String fromString = null;
+        Ip4Prefix ip4prefix = new Ip4Prefix(fromString);
+    }
+
+    /**
+     * Tests invalid class constructor for an empty string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructors() {
+        // Check constructor for invalid ID: empty string
+        Ip4Prefix ip4prefix = new Ip4Prefix("");
+    }
+
+    /**
+     * Tests getting the value of an address.
+     */
+    @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));
+
+        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));
+    }
+
+    /**
+     * Tests equality of {@link Ip4Address}.
+     */
+    @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));
+
+        addr1net = new Ip4Prefix("1.2.3.0/24");
+        addr2net = new Ip4Prefix("1.2.3.4/24");
+        assertThat(addr1net, is(addr2net));
+
+        addr1net = new Ip4Prefix("0.0.0.0/0");
+        addr2net = new Ip4Prefix("0.0.0.0/0");
+        assertThat(addr1net, is(addr2net));
+
+        addr1net = new Ip4Prefix("255.255.255.255/32");
+        addr2net = new Ip4Prefix("255.255.255.255/32");
+        assertThat(addr1net, is(addr2net));
+    }
+
+    /**
+     * Tests non-equality of {@link Ip4Address}.
+     */
+    @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)));
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        Ip4Prefix ip4prefix = new Ip4Prefix("1.2.3.0/24");
+        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+
+        ip4prefix = new Ip4Prefix("1.2.3.4/24");
+        assertThat(ip4prefix.toString(), is("1.2.3.0/24"));
+
+        ip4prefix = new Ip4Prefix("0.0.0.0/0");
+        assertThat(ip4prefix.toString(), is("0.0.0.0/0"));
+
+        ip4prefix = new Ip4Prefix("255.255.255.255/32");
+        assertThat(ip4prefix.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
new file mode 100644
index 0000000..a053301
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip6AddressTest.java
@@ -0,0 +1,437 @@
+package org.onlab.packet;
+
+import org.junit.Test;
+
+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;
+
+/**
+ * Tests for class {@link Ip6Address}.
+ */
+public class Ip6AddressTest {
+    /**
+     * Tests the immutability of {@link Ip6Address}.
+     */
+    @Test
+    public void testImmutable() {
+        assertThatClassIsImmutable(Ip6Address.class);
+    }
+
+    /**
+     * Tests the length of the address in bytes (octets).
+     */
+    @Test
+    public void testAddrBytelen() {
+        assertThat(Ip6Address.BYTE_LENGTH, is(16));
+    }
+
+    /**
+     * Tests the length of the address in bits.
+     */
+    @Test
+    public void testAddrBitlen() {
+        assertThat(Ip6Address.BIT_LENGTH, is(128));
+    }
+
+    /**
+     * Tests default class constructor.
+     */
+    @Test
+    public void testDefaultConstructor() {
+        Ip6Address ip6Address = new Ip6Address();
+        assertThat(ip6Address.toString(), is("::"));
+    }
+
+    /**
+     * Tests valid class copy constructor.
+     */
+    @Test
+    public void testCopyConstructor() {
+        Ip6Address fromAddr =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        Ip6Address ip6Address = new Ip6Address(fromAddr);
+        assertThat(ip6Address.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
+
+        fromAddr = new Ip6Address("::");
+        ip6Address = new Ip6Address(fromAddr);
+        assertThat(ip6Address.toString(), is("::"));
+
+        fromAddr = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        ip6Address = new Ip6Address(fromAddr);
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+    }
+
+    /**
+     * Tests invalid class copy constructor for a null object to copy from.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullObject() {
+        Ip6Address fromAddr = null;
+        Ip6Address ip6Address = new Ip6Address(fromAddr);
+    }
+
+    /**
+     * 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.
+     */
+    @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);
+    }
+
+    /**
+     * 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.
+     */
+    @Test
+    public void testConstructorForString() {
+        Ip6Address ip6Address =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        assertThat(ip6Address.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
+
+        ip6Address = new Ip6Address("::");
+        assertThat(ip6Address.toString(), is("::"));
+
+        ip6Address = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+    }
+
+    /**
+     * Tests invalid class constructor for a null string.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullString() {
+        String fromString = null;
+        Ip6Address ip6Address = new Ip6Address(fromString);
+    }
+
+    /**
+     * Tests invalid class constructor for an empty string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructors() {
+        // Check constructor for invalid ID: empty string
+        Ip6Address ip6Address = new Ip6Address("");
+    }
+
+    /**
+     * Tests returning the address as a byte array.
+     */
+    @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));
+
+        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));
+    }
+
+    /**
+     * Tests making a mask prefix for a given prefix length.
+     */
+    @Test
+    public void testMakeMaskPrefix() {
+        Ip6Address ip6Address = Ip6Address.makeMaskPrefix(8);
+        assertThat(ip6Address.toString(), is("ff00::"));
+
+        ip6Address = Ip6Address.makeMaskPrefix(120);
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00"));
+
+        ip6Address = Ip6Address.makeMaskPrefix(0);
+        assertThat(ip6Address.toString(), is("::"));
+
+        ip6Address = Ip6Address.makeMaskPrefix(128);
+        assertThat(ip6Address.toString(),
+                   is("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"));
+
+        ip6Address = Ip6Address.makeMaskPrefix(64);
+        assertThat(ip6Address.toString(), is("ffff:ffff:ffff:ffff::"));
+    }
+
+    /**
+     * Tests making of a masked address.
+     */
+    @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::"));
+
+        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 120);
+        assertThat(ip6AddressMasked.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8800"));
+
+        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 0);
+        assertThat(ip6AddressMasked.toString(), is("::"));
+
+        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 128);
+        assertThat(ip6AddressMasked.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8885"));
+
+        ip6AddressMasked = Ip6Address.makeMaskedAddress(ip6Address, 64);
+        assertThat(ip6AddressMasked.toString(), is("1111:2222:3333:4444::"));
+    }
+
+    /**
+     * Tests getting the value of an address.
+     */
+    @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));
+
+        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));
+    }
+
+    /**
+     * Tests equality of {@link Ip6Address}.
+     */
+    @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));
+
+        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));
+    }
+
+    /**
+     * Tests non-equality of {@link Ip6Address}.
+     */
+    @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)));
+    }
+
+    /**
+     * 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");
+        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");
+        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");
+        assertTrue(addr1.compareTo(addr2) == 0);
+        assertTrue(addr1.compareTo(addr3) > 0);
+        assertTrue(addr1.compareTo(addr4) < 0);
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        Ip6Address ip6Address =
+            new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8888");
+        assertThat(ip6Address.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8888"));
+
+        ip6Address = new Ip6Address("1111::8888");
+        assertThat(ip6Address.toString(), is("1111::8888"));
+
+        ip6Address = new Ip6Address("1111::");
+        assertThat(ip6Address.toString(), is("1111::"));
+
+        ip6Address = new Ip6Address("::8888");
+        assertThat(ip6Address.toString(), is("::8888"));
+
+        ip6Address = new Ip6Address("::");
+        assertThat(ip6Address.toString(), is("::"));
+
+        ip6Address = new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff");
+        assertThat(ip6Address.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
new file mode 100644
index 0000000..dcacdc7
--- /dev/null
+++ b/utils/misc/src/test/java/org/onlab/packet/Ip6PrefixTest.java
@@ -0,0 +1,255 @@
+package org.onlab.packet;
+
+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.onlab.junit.ImmutableClassChecker.assertThatClassIsImmutable;
+
+/**
+ * Tests for class {@link Ip6Prefix}.
+ */
+public class Ip6PrefixTest {
+    /**
+     * Tests the immutability of {@link Ip6Prefix}.
+     */
+    @Test
+    public void testImmutable() {
+        assertThatClassIsImmutable(Ip6Prefix.class);
+    }
+
+    /**
+     * Tests default class constructor.
+     */
+    @Test
+    public void testDefaultConstructor() {
+        Ip6Prefix ip6prefix = new Ip6Prefix();
+        assertThat(ip6prefix.toString(), is("::/0"));
+    }
+
+    /**
+     * Tests valid class copy constructor.
+     */
+    @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"));
+    }
+
+    /**
+     * Tests invalid class copy constructor for a null object to copy from.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullObject() {
+        Ip6Prefix fromAddr = null;
+        Ip6Prefix ip6prefix = new Ip6Prefix(fromAddr);
+    }
+
+    /**
+     * Tests valid class constructor for an address and prefix length.
+     */
+    @Test
+    public void testConstructorForAddressAndPrefixLength() {
+        Ip6Prefix ip6prefix =
+            new Ip6Prefix(new Ip6Address("1100::"), (short) 8);
+        assertThat(ip6prefix.toString(), is("1100::/8"));
+
+        ip6prefix =
+            new Ip6Prefix(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8885"),
+                        (short) 8);
+        assertThat(ip6prefix.toString(), is("1100::/8"));
+
+        ip6prefix =
+            new Ip6Prefix(new Ip6Address("1111:2222:3333:4444:5555:6666:7777:8800"),
+                        (short) 120);
+        assertThat(ip6prefix.toString(),
+                   is("1111:2222:3333:4444:5555:6666:7777:8800/120"));
+
+        ip6prefix = new Ip6Prefix(new Ip6Address("::"), (short) 0);
+        assertThat(ip6prefix.toString(), is("::/0"));
+
+        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"));
+
+        ip6prefix =
+            new Ip6Prefix(new Ip6Address("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"),
+                        (short) 128);
+        assertThat(ip6prefix.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.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testInvalidConstructorNullString() {
+        String fromString = null;
+        Ip6Prefix ip6prefix = new Ip6Prefix(fromString);
+    }
+
+    /**
+     * Tests invalid class constructor for an empty string.
+     */
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidConstructors() {
+        // Check constructor for invalid ID: empty string
+        Ip6Prefix ip6prefix = new Ip6Prefix("");
+    }
+
+    /**
+     * Tests getting the value of an address.
+     */
+    @Test
+    public void testGetValue() {
+        Ip6Prefix ip6prefix = new Ip6Prefix("1100::/8");
+        assertThat(ip6prefix.getAddress(), equalTo(new Ip6Address("1100::")));
+        assertThat(ip6prefix.getPrefixLen(), is((short) 8));
+
+        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));
+    }
+
+    /**
+     * Tests equality of {@link Ip6Address}.
+     */
+    @Test
+    public void testEquality() {
+        Ip6Prefix addr1net = new Ip6Prefix("1100::/8");
+        Ip6Prefix addr2net = new Ip6Prefix("1100::/8");
+        assertThat(addr1net, is(addr2net));
+
+        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));
+    }
+
+    /**
+     * Tests non-equality of {@link Ip6Address}.
+     */
+    @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)));
+    }
+
+    /**
+     * Tests object string representation.
+     */
+    @Test
+    public void testToString() {
+        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("::/0");
+        assertThat(ip6prefix.toString(), is("::/0"));
+
+        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"));
+    }
+}
diff --git a/web/gui/src/main/webapp/img/device.png b/web/gui/src/main/webapp/img/device.png
new file mode 100644
index 0000000..d608153
--- /dev/null
+++ b/web/gui/src/main/webapp/img/device.png
Binary files differ
diff --git a/web/gui/src/main/webapp/img/host.png b/web/gui/src/main/webapp/img/host.png
new file mode 100644
index 0000000..cacde17
--- /dev/null
+++ b/web/gui/src/main/webapp/img/host.png
Binary files differ
diff --git a/web/gui/src/main/webapp/img/onos-logo.png b/web/gui/src/main/webapp/img/onos-logo.png
new file mode 100644
index 0000000..afd87e1
--- /dev/null
+++ b/web/gui/src/main/webapp/img/onos-logo.png
Binary files differ
diff --git a/web/gui/src/main/webapp/opt.png b/web/gui/src/main/webapp/img/opt.png
similarity index 100%
rename from web/gui/src/main/webapp/opt.png
rename to web/gui/src/main/webapp/img/opt.png
Binary files differ
diff --git a/web/gui/src/main/webapp/pkt.png b/web/gui/src/main/webapp/img/pkt.png
similarity index 100%
rename from web/gui/src/main/webapp/pkt.png
rename to web/gui/src/main/webapp/img/pkt.png
Binary files differ
diff --git a/web/gui/src/main/webapp/index.html b/web/gui/src/main/webapp/index.html
index ebf25c5..19c9204 100644
--- a/web/gui/src/main/webapp/index.html
+++ b/web/gui/src/main/webapp/index.html
@@ -15,19 +15,18 @@
     <link rel="stylesheet" href="onos.css">
 
     <script src="geometry.js"></script>
-    <script src="onosui.js"></script>
+    <script src="onos.js"></script>
 
 </head>
 <body>
     <div id="frame">
         <div id="mast">
-            <span class="title">
-                ONOS Web UI
-            </span>
+            <img id="logo" src="img/onos-logo.png" width="60" height="38">
+            <span class="title">Open Network Operating System</span>
             <span class="right">
-                <span class="radio">[one]</span>
-                <span class="radio">[two]</span>
-                <span class="radio">[three]</span>
+                <span class="radio">[All Layers]</span>
+                <span class="radio">[Packet Only]</span>
+                <span class="radio">[Optical Only]</span>
             </span>
         </div>
         <div id="view"></div>
diff --git a/web/gui/src/main/webapp/network.js b/web/gui/src/main/webapp/network.js
index 80d11b7..c5145ad 100644
--- a/web/gui/src/main/webapp/network.js
+++ b/web/gui/src/main/webapp/network.js
@@ -10,17 +10,32 @@
     var api = onos.api;
 
     var config = {
-            layering: false,
+            options: {
+                layering: true,
+                collisionPrevention: true
+            },
             jsonUrl: 'network.json',
             iconUrl: {
-                pkt: 'pkt.png',
-                opt: 'opt.png'
+                device: 'img/device.png',
+                host: 'img/host.png',
+                pkt: 'img/pkt.png',
+                opt: 'img/opt.png'
             },
-            mastHeight: 32,
+            mastHeight: 36,
             force: {
-                linkDistance: 240,
-                linkStrength: 0.8,
-                charge: -400,
+                note: 'node.class or link.class is used to differentiate',
+                linkDistance: {
+                    infra: 240,
+                    host: 100
+                },
+                linkStrength: {
+                    infra: 1.0,
+                    host: 0.4
+                },
+                charge: {
+                    device: -800,
+                    host: -400
+                },
                 ticksWithoutCollisions: 50,
                 marginLR: 20,
                 marginTB: 20,
@@ -31,18 +46,27 @@
                 }
             },
             labels: {
-                imgPad: 22,
+                imgPad: 16,
                 padLR: 8,
                 padTB: 6,
                 marginLR: 3,
                 marginTB: 2
             },
+            icons: {
+                w: 32,
+                h: 32,
+                xoff: -12,
+                yoff: -8
+            },
             constraints: {
                 ypos: {
-                    pkt: 0.3,
-                    opt: 0.7
+                    host: 0.15,
+                    switch: 0.3,
+                    roadm: 0.7
                 }
-            }
+            },
+            hostLinkWidth: 1.0,
+            mouseOutTimerDelayMs: 120
         },
         view = {},
         network = {},
@@ -104,14 +128,23 @@
         var nw = network.forceWidth,
             nh = network.forceHeight;
 
-        network.data.nodes.forEach(function(n) {
+        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,
-                    status: n.status,
                     x: ix,
                     y: iy,
                     constraint: {
@@ -123,21 +156,61 @@
             network.nodes.push(node);
         });
 
-        function yPosConstraintForNode(n) {
-            return config.constraints.ypos[n.type] || 0.5;
-        }
+        // 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(n) {
             var src = network.lookup[n.src],
                 dst = network.lookup[n.dst],
                 id = src.id + "~" + dst.id;
 
             var link = {
+                class: 'infra',
                 id: id,
+                type: n.type,
+                width: n.linkWidth,
                 source: src,
                 target: dst,
-                strength: config.force.linkStrength
+                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);
         });
@@ -145,13 +218,15 @@
 
     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 d.strength; })
-            .size([network.forceWidth, network.forceHeight])
-            .linkDistance(config.force.linkDistance)
-            .charge(config.force.charge)
+            .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')
@@ -205,9 +280,10 @@
         network.link = network.svg.append('g').selectAll('.link')
             .data(network.force.links(), function(d) {return d.id})
             .enter().append('line')
-            .attr('class', 'link');
+            .attr('class', function(d) {return 'link ' + d.class});
 
-        // TODO: drag behavior
+
+        // == define node drag behavior...
         network.draggedThreshold = d3.scale.linear()
             .domain([0, 0.1])
             .range([5, 20])
@@ -258,7 +334,11 @@
             .data(network.force.nodes(), function(d) {return d.id})
             .enter().append('g')
             .attr('class', function(d) {
-                return 'node ' + d.type;
+                var cls = 'node ' + d.class;
+                if (d.type) {
+                    cls += ' ' + d.type;
+                }
+                return cls;
             })
             .attr('transform', function(d) {
                 return translate(d.x, d.y);
@@ -281,29 +361,33 @@
                     }
                     network.mouseoutTimeout = setTimeout(function() {
                         highlightObject(null);
-                    }, 160);
+                    }, config.mouseOutTimerDelayMs);
                 }
             });
 
         network.nodeRect = network.node.append('rect')
             .attr('rx', 5)
-            .attr('ry', 5)
-            .attr('width', 126)
-            .attr('height', 40);
+            .attr('ry', 5);
+        // note that width/height are adjusted to fit the label text
 
         network.node.each(function(d) {
             var node = d3.select(this),
                 rect = node.select('rect'),
-                img = node.append('svg:image')
-                    .attr('x', -16)
-                    .attr('y', -16)
-                    .attr('width', 32)
-                    .attr('height', 32)
-                    .attr('xlink:href', iconUrl(d)),
+                icon = iconUrl(d),
                 text = node.append('text')
+                    // TODO: add label cycle behavior
                     .text(d.id)
-                    .attr('dy', '1.1em'),
-                dummy;
+                    .attr('dy', '1.1em');
+
+            if (icon) {
+                var cfg = config.icons;
+                node.append('svg:image')
+                    .attr('width', cfg.w)
+                    .attr('height', cfg.h)
+                    .attr('xlink:href', icon);
+                // note, icon relative positioning (x,y) is done after we have
+                // adjusted the bounds of the rectangle...
+            }
 
         });
 
@@ -352,7 +436,8 @@
                     .attr('height', bounds.y2 - bounds.y1);
 
                 node.select('image')
-                    .attr('x', bounds.x1);
+                    .attr('x', bounds.x1 + config.icons.xoff)
+                    .attr('y', bounds.y1 + config.icons.yoff);
 
                 d.extent = {
                     left: bounds.x1 - lab.marginLR,
@@ -384,7 +469,7 @@
     }
 
     function iconUrl(d) {
-        return config.iconUrl[d.type];
+        return config.iconUrl[d.icon];
     }
 
     function translate(x, y) {
@@ -440,7 +525,7 @@
     function tick(e) {
         network.numTicks++;
 
-        if (config.layering) {
+        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;
@@ -450,7 +535,7 @@
             });
         }
 
-        if (network.preventCollisions) {
+        if (config.options.collisionPrevention && network.preventCollisions) {
             preventCollisions();
         }
 
diff --git a/web/gui/src/main/webapp/network.json b/web/gui/src/main/webapp/network.json
index b4f1b6e..c43e0ef 100644
--- a/web/gui/src/main/webapp/network.json
+++ b/web/gui/src/main/webapp/network.json
@@ -1,56 +1,163 @@
 {
-    "id": "network-v1",
     "meta": {
-        "__comment_1__": "This is sample data for developing the ONOS UI",
-        "foo": "bar",
-        "zoo": "goo"
+        "comments": [
+            "This is sample data for developing the ONOS UI (network view)",
+            " in a standalone mode (no server required).",
+            " Eventually, we will wire this up to live data",
+            " from the server, via a websocket.",
+            "",
+            "Note that this is just a first-draft of the data --",
+            " additional fields will be added when they are needed."
+        ],
+        "otherMetaData": "can go here..."
     },
-    "nodes": [
+    "devices": [
         {
-            "id": "sample1",
-            "type": "opt",
-            "status": "good"
+            "id": "of:0000000000000001",
+            "labels": ["00:00:00:00:00:00:00:01", "of/::01", "opt-1"],
+            "type": "roadm"
         },
         {
-            "id": "00:00:00:00:00:00:00:02",
-            "type": "opt",
-            "status": "good"
+            "id": "of:0000000000000002",
+            "labels": ["00:00:00:00:00:00:00:02", "of/::02", "opt-2"],
+            "type": "roadm"
         },
         {
-            "id": "00:00:00:00:00:00:00:03",
-            "type": "opt",
-            "status": "good"
+            "id": "of:0000000000000003",
+            "labels": ["00:00:00:00:00:00:00:03", "of/::03", "opt-3"],
+            "type": "roadm"
         },
         {
-            "id": "00:00:00:00:00:00:00:04",
-            "type": "opt",
-            "status": "good"
+            "id": "of:0000000000000004",
+            "labels": ["00:00:00:00:00:00:00:04", "of/::04", "opt-4"],
+            "type": "roadm"
         },
         {
-            "id": "00:00:00:00:00:00:00:11",
-            "type": "pkt",
-            "status": "good"
+            "id": "of:0000000000000011",
+            "labels": ["00:00:00:00:00:00:00:11", "of/::11", "pkt-11"],
+            "type": "switch"
         },
         {
-            "id": "00:00:00:00:00:00:00:12",
-            "type": "pkt",
-            "status": "good"
+            "id": "of:0000000000000012",
+            "labels": ["00:00:00:00:00:00:00:12", "of/::12", "pkt-12"],
+            "type": "switch"
         },
         {
-            "id": "00:00:00:00:00:00:00:13",
-            "type": "pkt",
-            "status": "good"
+            "id": "of:0000000000000013",
+            "labels": ["00:00:00:00:00:00:00:13", "of/::13", "pkt-13"],
+            "type": "switch"
         }
     ],
+    "linkNotes": [
+        "even though we have 'directionality' (src/dst), each link is",
+        " considered to be bi-directional. Note that links between hosts",
+        " and edge switches are inferred from host information, and not",
+        " explicitly represented here."
+    ],
     "links": [
-        { "src": "sample1", "dst": "00:00:00:00:00:00:00:02" },
-        { "src": "sample1", "dst": "00:00:00:00:00:00:00:03" },
-        { "src": "sample1", "dst": "00:00:00:00:00:00:00:04" },
-        { "src": "00:00:00:00:00:00:00:02", "dst": "00:00:00:00:00:00:00:03" },
-        { "src": "00:00:00:00:00:00:00:02", "dst": "00:00:00:00:00:00:00:04" },
-        { "src": "00:00:00:00:00:00:00:03", "dst": "00:00:00:00:00:00:00:04" },
-        { "src": "00:00:00:00:00:00:00:13", "dst": "00:00:00:00:00:00:00:03" },
-        { "src": "00:00:00:00:00:00:00:12", "dst": "00:00:00:00:00:00:00:02" },
-        { "src": "00:00:00:00:00:00:00:11", "dst": "sample1" }
+        {
+            "src": "of:0000000000000001",
+            "dst": "of:0000000000000002",
+            "type": "optical",
+            "linkWidth": 1.5
+        },
+        {
+            "src": "of:0000000000000001",
+            "dst": "of:0000000000000003",
+            "type": "optical",
+            "linkWidth": 1.5
+        },
+        {
+            "src": "of:0000000000000001",
+            "dst": "of:0000000000000004",
+            "type": "optical",
+            "linkWidth": 1.5
+        },
+        {
+            "src": "of:0000000000000002",
+            "dst": "of:0000000000000003",
+            "type": "optical",
+            "linkWidth": 1.5
+        },
+        {
+            "src": "of:0000000000000002",
+            "dst": "of:0000000000000004",
+            "type": "optical",
+            "linkWidth": 1.5
+        },
+        {
+            "src": "of:0000000000000003",
+            "dst": "of:0000000000000004",
+            "type": "optical",
+            "linkWidth": 1.5
+        },
+        {
+            "src": "of:0000000000000013",
+            "dst": "of:0000000000000003",
+            "type": "direct",
+            "linkWidth": 1.0
+        },
+        {
+            "src": "of:0000000000000012",
+            "dst": "of:0000000000000002",
+            "type": "direct",
+            "linkWidth": 1.0
+        },
+        {
+            "src": "of:0000000000000011",
+            "dst": "of:0000000000000001",
+            "type": "direct",
+            "linkWidth": 1.0
+        }
+    ],
+    "hosts": [
+        {
+            "id": "00:60:d3:00:11:01/7",
+            "labels": ["00:60:d3:00:11:01/7", "Host-11-A"],
+            "cp" : {
+                "device": "of:0000000000000011",
+                "port": 6
+            }
+        },
+        {
+            "id": "00:60:d3:00:11:02/7",
+            "labels": ["00:60:d3:00:11:02/7", "Host-11-B"],
+            "cp" : {
+                "device": "of:0000000000000011",
+                "port": 8
+            }
+        },
+        {
+            "id": "00:60:d3:00:12:01/4",
+            "labels": ["00:60:d3:00:12:01/4", "Host-12-C"],
+            "cp" : {
+                "device": "of:0000000000000012",
+                "port": 12
+            }
+        },
+        {
+            "id": "00:60:d3:00:12:02/4",
+            "labels": ["00:60:d3:00:12:02/4", "Host-12-D"],
+            "cp" : {
+                "device": "of:0000000000000012",
+                "port": 13
+            }
+        },
+        {
+            "id": "00:60:d3:00:13:01/19",
+            "labels": ["00:60:d3:00:13:01/19", "Host-13-E"],
+            "cp" : {
+                "device": "of:0000000000000013",
+                "port": 7
+            }
+        },
+        {
+            "id": "00:60:d3:00:13:02/19",
+            "labels": ["00:60:d3:00:13:02/19", "Host-13-F"],
+            "cp" : {
+                "device": "of:0000000000000013",
+                "port": 8
+            }
+        }
     ]
 }
diff --git a/web/gui/src/main/webapp/onos.css b/web/gui/src/main/webapp/onos.css
index 340ae79..7f387eb 100644
--- a/web/gui/src/main/webapp/onos.css
+++ b/web/gui/src/main/webapp/onos.css
@@ -13,13 +13,15 @@
  */
 
 span.title {
-    color: darkblue;
-    font-size: 16pt;
+    color: #37b;
+    font-size: 14pt;
     font-style: italic;
+    vertical-align: 10px;
 }
 
 span.radio {
     color: darkslateblue;
+    font-size: 10pt;
 }
 
 span.right {
@@ -38,7 +40,7 @@
  * Network Graph elements ======================================
  */
 
-.link {
+svg .link {
     fill: none;
     stroke: #666;
     stroke-width: 1.5px;
@@ -56,7 +58,7 @@
     stroke-width: 1.5px;
 }
 
-.node rect {
+svg .node rect {
     stroke-width: 1.5px;
 
     transition: opacity 250ms;
@@ -64,13 +66,15 @@
     -moz-transition: opacity 250ms;
 }
 
-/*differentiate between packet and optical nodes*/
-svg .node.pkt rect {
-    fill: #77a;
+svg .node.device.roadm rect {
+    fill: #229;
+}
+svg .node.device.switch rect {
+    fill: #55f;
 }
 
-svg .node.opt rect {
-    fill: #7a7;
+svg .node.host rect {
+    fill: #787;
 }
 
 svg .node text {
@@ -121,15 +125,13 @@
 #frame {
     width: 100%;
     height: 100%;
-    background-color: #cdf;
+    background-color: #fff;
 }
 
 #mast {
-    height: 32px;
-    background-color: #abe;
+    height: 36px;
+    padding: 4px;
+    background-color: #ccc;
     vertical-align: baseline;
 }
 
-#main {
-    background-color: #99c;
-}
diff --git a/web/gui/src/main/webapp/onosui.js b/web/gui/src/main/webapp/onos.js
similarity index 100%
rename from web/gui/src/main/webapp/onosui.js
rename to web/gui/src/main/webapp/onos.js