Merge branch 'master' of ssh://gerrit.onlab.us:29418/onos-next
diff --git a/core/api/src/main/java/org/onlab/onos/net/DefaultAnnotations.java b/core/api/src/main/java/org/onlab/onos/net/DefaultAnnotations.java
index 001518e..0c0f375 100644
--- a/core/api/src/main/java/org/onlab/onos/net/DefaultAnnotations.java
+++ b/core/api/src/main/java/org/onlab/onos/net/DefaultAnnotations.java
@@ -1,5 +1,6 @@
 package org.onlab.onos.net;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
@@ -71,9 +72,33 @@
         return new DefaultAnnotations(merged);
     }
 
+    /**
+     * Convert Annotations to DefaultAnnotations if needed and merges.
+     *
+     * @see #merge(DefaultAnnotations, SparseAnnotations)
+     *
+     * @param annotations       base annotations
+     * @param sparseAnnotations additional sparse annotations
+     * @return combined annotations or the original base annotations if there
+     * are not additional annotations
+     */
+    public static DefaultAnnotations merge(Annotations annotations,
+                                    SparseAnnotations sparseAnnotations) {
+        if (annotations instanceof DefaultAnnotations) {
+            return merge((DefaultAnnotations) annotations, sparseAnnotations);
+        }
+
+        DefaultAnnotations.Builder builder = DefaultAnnotations.builder();
+        for (String key : annotations.keys()) {
+            builder.set(key, annotations.value(key));
+        }
+        return merge(builder.build(), sparseAnnotations);
+    }
+
     @Override
     public Set<String> keys() {
-        return map.keySet();
+        // TODO: unmodifiable to be removed after switching to ImmutableMap;
+        return Collections.unmodifiableSet(map.keySet());
     }
 
     @Override
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DefaultDeviceDescription.java b/core/api/src/main/java/org/onlab/onos/net/device/DefaultDeviceDescription.java
index fd04e5c..788d23a 100644
--- a/core/api/src/main/java/org/onlab/onos/net/device/DefaultDeviceDescription.java
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DefaultDeviceDescription.java
@@ -45,6 +45,18 @@
         this.serialNumber = serialNumber;
     }
 
+    /**
+     * Creates a device description using the supplied information.
+     * @param base DeviceDescription to basic information
+     * @param annotations Annotations to use.
+     */
+    public DefaultDeviceDescription(DeviceDescription base,
+                                    SparseAnnotations... annotations) {
+        this(base.deviceURI(), base.type(), base.manufacturer(),
+             base.hwVersion(), base.swVersion(), base.serialNumber(),
+             annotations);
+    }
+
     @Override
     public URI deviceURI() {
         return uri;
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/DefaultPortDescription.java b/core/api/src/main/java/org/onlab/onos/net/device/DefaultPortDescription.java
index 1d52ac9..eb75ede 100644
--- a/core/api/src/main/java/org/onlab/onos/net/device/DefaultPortDescription.java
+++ b/core/api/src/main/java/org/onlab/onos/net/device/DefaultPortDescription.java
@@ -1,20 +1,43 @@
 package org.onlab.onos.net.device;
 
+import org.onlab.onos.net.AbstractDescription;
 import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.SparseAnnotations;
 
 /**
  * Default implementation of immutable port description.
  */
-public class DefaultPortDescription implements PortDescription {
+public class DefaultPortDescription extends AbstractDescription
+        implements PortDescription {
 
     private final PortNumber number;
     private final boolean isEnabled;
 
-    public DefaultPortDescription(PortNumber number, boolean isEnabled) {
+    /**
+     * Creates a port description using the supplied information.
+     *
+     * @param number       port number
+     * @param isEnabled    port enabled state
+     * @param annotations  optional key/value annotations map
+     */
+    public DefaultPortDescription(PortNumber number, boolean isEnabled,
+                SparseAnnotations... annotations) {
+        super(annotations);
         this.number = number;
         this.isEnabled = isEnabled;
     }
 
+    /**
+     * Creates a port description using the supplied information.
+     *
+     * @param base         PortDescription to get basic information from
+     * @param annotations  optional key/value annotations map
+     */
+    public DefaultPortDescription(PortDescription base,
+            SparseAnnotations annotations) {
+        this(base.portNumber(), base.isEnabled(), annotations);
+    }
+
     @Override
     public PortNumber portNumber() {
         return number;
diff --git a/core/api/src/main/java/org/onlab/onos/net/device/PortDescription.java b/core/api/src/main/java/org/onlab/onos/net/device/PortDescription.java
index f0dc8ee..f01b49c 100644
--- a/core/api/src/main/java/org/onlab/onos/net/device/PortDescription.java
+++ b/core/api/src/main/java/org/onlab/onos/net/device/PortDescription.java
@@ -1,11 +1,12 @@
 package org.onlab.onos.net.device;
 
+import org.onlab.onos.net.Description;
 import org.onlab.onos.net.PortNumber;
 
 /**
  * Information about a port.
  */
-public interface PortDescription {
+public interface PortDescription extends Description {
 
     // TODO: possibly relocate this to a common ground so that this can also used by host tracking if required
 
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/AbstractIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/AbstractIntent.java
new file mode 100644
index 0000000..eefe750
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/AbstractIntent.java
@@ -0,0 +1,49 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Base intent implementation.
+ */
+public abstract class AbstractIntent implements Intent {
+
+    private final IntentId id;
+
+    /**
+     * Creates a base intent with the specified identifier.
+     *
+     * @param id intent identifier
+     */
+    protected AbstractIntent(IntentId id) {
+        this.id = id;
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected AbstractIntent() {
+        this.id = null;
+    }
+
+    @Override
+    public IntentId getId() {
+        return id;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        AbstractIntent that = (AbstractIntent) o;
+        return id.equals(that.id);
+    }
+
+    @Override
+    public int hashCode() {
+        return id.hashCode();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperation.java b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperation.java
new file mode 100644
index 0000000..12bd02b
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperation.java
@@ -0,0 +1,98 @@
+package org.onlab.onos.net.intent;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A list of BatchOperationEntry.
+ *
+ * @param <T> the enum of operators <br>
+ *        This enum must be defined in each sub-classes.
+ *
+ */
+public abstract class BatchOperation<T extends BatchOperationEntry<?, ?>> {
+    private List<T> ops;
+
+    /**
+     * Creates new {@link BatchOperation} object.
+     */
+    public BatchOperation() {
+        ops = new LinkedList<>();
+    }
+
+    /**
+     * Creates {@link BatchOperation} object from a list of batch operation
+     * entries.
+     *
+     * @param batchOperations the list of batch operation entries.
+     */
+    public BatchOperation(List<T> batchOperations) {
+        ops = new LinkedList<>(checkNotNull(batchOperations));
+    }
+
+    /**
+     * Removes all operations maintained in this object.
+     */
+    public void clear() {
+        ops.clear();
+    }
+
+    /**
+     * Returns the number of operations in this object.
+     *
+     * @return the number of operations in this object
+     */
+    public int size() {
+        return ops.size();
+    }
+
+    /**
+     * Returns the operations in this object.
+     *
+     * @return the operations in this object
+     */
+    public List<T> getOperations() {
+        return Collections.unmodifiableList(ops);
+    }
+
+    /**
+     * Adds an operation.
+     *
+     * @param entry the operation to be added
+     * @return this object if succeeded, null otherwise
+     */
+    public BatchOperation<T> addOperation(T entry) {
+        return ops.add(entry) ? this : null;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (o == null) {
+            return false;
+        }
+
+        if (getClass() != o.getClass()) {
+            return false;
+        }
+        BatchOperation<?> other = (BatchOperation<?>) o;
+
+        return this.ops.equals(other.ops);
+    }
+
+    @Override
+    public int hashCode() {
+        return ops.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return ops.toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationEntry.java b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationEntry.java
new file mode 100644
index 0000000..781e3d1
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationEntry.java
@@ -0,0 +1,81 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Objects;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * A super class for batch operation entry classes.
+ * <p>
+ * This is the interface to classes which are maintained by BatchOperation as
+ * its entries.
+ */
+public class BatchOperationEntry<T extends Enum<?>, U extends BatchOperationTarget> {
+    private final T operator;
+    private final U target;
+
+    /**
+     * Default constructor for serializer.
+     */
+    @Deprecated
+    protected BatchOperationEntry() {
+        this.operator = null;
+        this.target = null;
+    }
+
+    /**
+     * Constructs new instance for the entry of the BatchOperation.
+     *
+     * @param operator the operator of this operation
+     * @param target the target object of this operation
+     */
+    public BatchOperationEntry(T operator, U target) {
+        this.operator = operator;
+        this.target = target;
+    }
+
+    /**
+     * Gets the target object of this operation.
+     *
+     * @return the target object of this operation
+     */
+    public U getTarget() {
+        return target;
+    }
+
+    /**
+     * Gets the operator of this operation.
+     *
+     * @return the operator of this operation
+     */
+    public T getOperator() {
+        return operator;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        BatchOperationEntry<?, ?> other = (BatchOperationEntry<?, ?>) o;
+        return (this.operator == other.operator) &&
+            Objects.equals(this.target, other.target);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(operator, target);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+            .add("operator", operator)
+            .add("target", target)
+            .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationTarget.java b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationTarget.java
new file mode 100644
index 0000000..5583240
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/BatchOperationTarget.java
@@ -0,0 +1,8 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * An interface of the class which is assigned to BatchOperation.
+ */
+public interface BatchOperationTarget {
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java
new file mode 100644
index 0000000..629a9d1
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/ConnectivityIntent.java
@@ -0,0 +1,84 @@
+package org.onlab.onos.net.intent;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import com.google.common.base.Objects;
+
+/**
+ * Abstraction of connectivity intent for traffic matching some criteria.
+ */
+public abstract class ConnectivityIntent extends AbstractIntent {
+
+    // TODO: other forms of intents should be considered for this family:
+    //   point-to-point with constraints (waypoints/obstacles)
+    //   multi-to-single point with constraints (waypoints/obstacles)
+    //   single-to-multi point with constraints (waypoints/obstacles)
+    //   concrete path (with alternate)
+    //   ...
+
+    private final TrafficSelector selector;
+    // TODO: should consider which is better for multiple actions,
+    // defining compound action class or using list of actions.
+    private final TrafficTreatment treatment;
+
+    /**
+     * Creates a connectivity intent that matches on the specified intent
+     * and applies the specified action.
+     *
+     * @param id    intent identifier
+     * @param match traffic match
+     * @param action action
+     * @throws NullPointerException if the match or action is null
+     */
+    protected ConnectivityIntent(IntentId id, TrafficSelector match, TrafficTreatment action) {
+        super(id);
+        this.selector = checkNotNull(match);
+        this.treatment = checkNotNull(action);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected ConnectivityIntent() {
+        super();
+        this.selector = null;
+        this.treatment = null;
+    }
+
+    /**
+     * Returns the match specifying the type of traffic.
+     *
+     * @return traffic match
+     */
+    public TrafficSelector getTrafficSelector() {
+        return selector;
+    }
+
+    /**
+     * Returns the action applied to the traffic.
+     *
+     * @return applied action
+     */
+    public TrafficTreatment getTrafficTreatment() {
+        return treatment;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!super.equals(o)) {
+            return false;
+        }
+        ConnectivityIntent that = (ConnectivityIntent) o;
+        return Objects.equal(this.selector, that.selector)
+                && Objects.equal(this.treatment, that.treatment);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(super.hashCode(), selector, treatment);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/InstallableIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/InstallableIntent.java
new file mode 100644
index 0000000..66bc759
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/InstallableIntent.java
@@ -0,0 +1,8 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Abstraction of an intent that can be installed into
+ * the underlying system without additional compilation.
+ */
+public interface InstallableIntent extends Intent {
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java b/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java
new file mode 100644
index 0000000..b239ede
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/Intent.java
@@ -0,0 +1,15 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Abstraction of an application level intent.
+ *
+ * Make sure that an Intent should be immutable when a new type is defined.
+ */
+public interface Intent extends BatchOperationTarget {
+    /**
+     * Returns the intent identifier.
+     *
+     * @return intent identifier
+     */
+    IntentId getId();
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentBatchOperation.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentBatchOperation.java
new file mode 100644
index 0000000..b450d71
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentBatchOperation.java
@@ -0,0 +1,39 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * A list of intent operations.
+ */
+public class IntentBatchOperation extends
+        BatchOperation<BatchOperationEntry<IntentBatchOperation.Operator, ?>> {
+    /**
+     * The intent operators.
+     */
+    public enum Operator {
+        ADD,
+        REMOVE,
+    }
+
+    /**
+     * Adds an add-intent operation.
+     *
+     * @param intent the intent to be added
+     * @return the IntentBatchOperation object if succeeded, null otherwise
+     */
+    public IntentBatchOperation addAddIntentOperation(Intent intent) {
+        return (null == super.addOperation(
+                new BatchOperationEntry<Operator, Intent>(Operator.ADD, intent)))
+                ? null : this;
+    }
+
+    /**
+     * Adds a remove-intent operation.
+     *
+     * @param id the ID of intent to be removed
+     * @return the IntentBatchOperation object if succeeded, null otherwise
+     */
+    public IntentBatchOperation addRemoveIntentOperation(IntentId id) {
+        return (null == super.addOperation(
+                new BatchOperationEntry<Operator, IntentId>(Operator.REMOVE, id)))
+                ? null : this;
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentCompiler.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentCompiler.java
new file mode 100644
index 0000000..dbc3cc4
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentCompiler.java
@@ -0,0 +1,20 @@
+package org.onlab.onos.net.intent;
+
+import java.util.List;
+
+/**
+ * Abstraction of a compiler which is capable of taking an intent
+ * and translating it to other, potentially installable, intents.
+ *
+ * @param <T> the type of intent
+ */
+public interface IntentCompiler<T extends Intent> {
+    /**
+     * Compiles the specified intent into other intents.
+     *
+     * @param intent intent to be compiled
+     * @return list of resulting intents
+     * @throws IntentException if issues are encountered while compiling the intent
+     */
+    List<Intent> compile(T intent);
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentEvent.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentEvent.java
new file mode 100644
index 0000000..27ae834
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentEvent.java
@@ -0,0 +1,113 @@
+package org.onlab.onos.net.intent;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * A class to represent an intent related event.
+ */
+public class IntentEvent {
+
+    // TODO: determine a suitable parent class; if one does not exist, consider introducing one
+
+    private final long time;
+    private final Intent intent;
+    private final IntentState state;
+    private final IntentState previous;
+
+    /**
+     * Creates an event describing a state change of an intent.
+     *
+     * @param intent subject intent
+     * @param state new intent state
+     * @param previous previous intent state
+     * @param time time the event created in milliseconds since start of epoch
+     * @throws NullPointerException if the intent or state is null
+     */
+    public IntentEvent(Intent intent, IntentState state, IntentState previous, long time) {
+        this.intent = checkNotNull(intent);
+        this.state = checkNotNull(state);
+        this.previous = previous;
+        this.time = time;
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected IntentEvent() {
+        this.intent = null;
+        this.state = null;
+        this.previous = null;
+        this.time = 0;
+    }
+
+    /**
+     * Returns the state of the intent which caused the event.
+     *
+     * @return the state of the intent
+     */
+    public IntentState getState() {
+        return state;
+    }
+
+    /**
+     * Returns the previous state of the intent which caused the event.
+     *
+     * @return the previous state of the intent
+     */
+    public IntentState getPreviousState() {
+        return previous;
+    }
+
+    /**
+     * Returns the intent associated with the event.
+     *
+     * @return the intent
+     */
+    public Intent getIntent() {
+        return intent;
+    }
+
+    /**
+     * Returns the time at which the event was created.
+     *
+     * @return the time in milliseconds since start of epoch
+     */
+    public long getTime() {
+        return time;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        IntentEvent that = (IntentEvent) o;
+        return Objects.equals(this.intent, that.intent)
+                && Objects.equals(this.state, that.state)
+                && Objects.equals(this.previous, that.previous)
+                && Objects.equals(this.time, that.time);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(intent, state, previous, time);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("intent", intent)
+                .add("state", state)
+                .add("previous", previous)
+                .add("time", time)
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentEventListener.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentEventListener.java
new file mode 100644
index 0000000..f59ecfc
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentEventListener.java
@@ -0,0 +1,13 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Listener for {@link IntentEvent intent events}.
+ */
+public interface IntentEventListener {
+    /**
+     * Processes the specified intent event.
+     *
+     * @param event the event to process
+     */
+    void event(IntentEvent event);
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentException.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentException.java
new file mode 100644
index 0000000..4148dea
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentException.java
@@ -0,0 +1,35 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Represents an intent related error.
+ */
+public class IntentException extends RuntimeException {
+
+    private static final long serialVersionUID = 1907263634145241319L;
+
+    /**
+     * Constructs an exception with no message and no underlying cause.
+     */
+    public IntentException() {
+    }
+
+    /**
+     * Constructs an exception with the specified message.
+     *
+     * @param message the message describing the specific nature of the error
+     */
+    public IntentException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs an exception with the specified message and the underlying cause.
+     *
+     * @param message the message describing the specific nature of the error
+     * @param cause the underlying cause of this error
+     */
+    public IntentException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentExtensionService.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentExtensionService.java
new file mode 100644
index 0000000..c6338a7f
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentExtensionService.java
@@ -0,0 +1,57 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Map;
+
+/**
+ * Service for extending the capability of intent framework by
+ * adding additional compilers or/and installers.
+ */
+public interface IntentExtensionService {
+    /**
+     * Registers the specified compiler for the given intent class.
+     *
+     * @param cls intent class
+     * @param compiler intent compiler
+     * @param <T> the type of intent
+     */
+    <T extends Intent> void registerCompiler(Class<T> cls, IntentCompiler<T> compiler);
+
+    /**
+     * Unregisters the compiler for the specified intent class.
+     *
+     * @param cls intent class
+     * @param <T> the type of intent
+     */
+    <T extends Intent> void unregisterCompiler(Class<T> cls);
+
+    /**
+     * Returns immutable set of bindings of currently registered intent compilers.
+     *
+     * @return the set of compiler bindings
+     */
+    Map<Class<? extends Intent>, IntentCompiler<? extends Intent>> getCompilers();
+
+    /**
+     * Registers the specified installer for the given installable intent class.
+     *
+     * @param cls installable intent class
+     * @param installer intent installer
+     * @param <T> the type of installable intent
+     */
+    <T extends InstallableIntent> void registerInstaller(Class<T> cls, IntentInstaller<T> installer);
+
+    /**
+     * Unregisters the installer for the given installable intent class.
+     *
+     * @param cls installable intent class
+     * @param <T> the type of installable intent
+     */
+    <T extends InstallableIntent> void unregisterInstaller(Class<T> cls);
+
+    /**
+     * Returns immutable set of bindings of currently registered intent installers.
+     *
+     * @return the set of installer bindings
+     */
+    Map<Class<? extends InstallableIntent>, IntentInstaller<? extends InstallableIntent>> getInstallers();
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java
new file mode 100644
index 0000000..798e00c
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentId.java
@@ -0,0 +1,68 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Intent identifier suitable as an external key.
+ *
+ * This class is immutable.
+ */
+public final class IntentId implements BatchOperationTarget {
+
+    private static final int DEC = 10;
+    private static final int HEX = 16;
+
+    private final long id;
+
+    /**
+     * Creates an intent identifier from the specified string representation.
+     *
+     * @param value long value
+     * @return intent identifier
+     */
+    public static IntentId valueOf(String value) {
+        long id = value.toLowerCase().startsWith("0x")
+                ? Long.parseLong(value.substring(2), HEX)
+                : Long.parseLong(value, DEC);
+        return new IntentId(id);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected IntentId() {
+        this.id = 0;
+    }
+
+    /**
+     * Constructs the ID corresponding to a given long value.
+     *
+     * @param id the underlying value of this ID
+     */
+    public IntentId(long id) {
+        this.id = id;
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) (id ^ (id >>> 32));
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof IntentId)) {
+            return false;
+        }
+
+        IntentId that = (IntentId) obj;
+        return this.id == that.id;
+    }
+
+    @Override
+    public String toString() {
+        return "0x" + Long.toHexString(id);
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java
new file mode 100644
index 0000000..738be04
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentInstaller.java
@@ -0,0 +1,22 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Abstraction of entity capable of installing intents to the environment.
+ */
+public interface IntentInstaller<T extends InstallableIntent> {
+    /**
+     * Installs the specified intent to the environment.
+     *
+     * @param intent intent to be installed
+     * @throws IntentException if issues are encountered while installing the intent
+     */
+    void install(T intent);
+
+    /**
+     * Uninstalls the specified intent from the environment.
+     *
+     * @param intent intent to be uninstalled
+     * @throws IntentException if issues are encountered while uninstalling the intent
+     */
+    void uninstall(T intent);
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperations.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperations.java
new file mode 100644
index 0000000..470d98b
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentOperations.java
@@ -0,0 +1,10 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * Abstraction of a batch of intent submit/withdraw operations.
+ */
+public interface IntentOperations {
+
+    // TODO: elaborate once the revised BatchOperation scheme is in place
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java
new file mode 100644
index 0000000..2b4fb59
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentService.java
@@ -0,0 +1,76 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Set;
+
+/**
+ * Service for application submitting or withdrawing their intents.
+ */
+public interface IntentService {
+    /**
+     * Submits an intent into the system.
+     *
+     * This is an asynchronous request meaning that any compiling
+     * or installation activities may be done at later time.
+     *
+     * @param intent intent to be submitted
+     */
+    void submit(Intent intent);
+
+    /**
+     * Withdraws an intent from the system.
+     *
+     * This is an asynchronous request meaning that the environment
+     * may be affected at later time.
+     *
+     * @param intent intent to be withdrawn
+     */
+    void withdraw(Intent intent);
+
+    /**
+     * Submits a batch of submit &amp; withdraw operations. Such a batch is
+     * assumed to be processed together.
+     *
+     * This is an asynchronous request meaning that the environment
+     * may be affected at later time.
+     *
+     * @param operations batch of intent operations
+     */
+    void execute(IntentOperations operations);
+
+    /**
+     * Returns immutable set of intents currently in the system.
+     *
+     * @return set of intents
+     */
+    Set<Intent> getIntents();
+
+    /**
+     * Retrieves the intent specified by its identifier.
+     *
+     * @param id intent identifier
+     * @return the intent or null if one with the given identifier is not found
+     */
+    Intent getIntent(IntentId id);
+
+    /**
+     * Retrieves the state of an intent by its identifier.
+     *
+     * @param id intent identifier
+     * @return the intent state or null if one with the given identifier is not found
+     */
+    IntentState getIntentState(IntentId id);
+
+    /**
+     * Adds the specified listener for intent events.
+     *
+     * @param listener listener to be added
+     */
+    void addListener(IntentEventListener listener);
+
+    /**
+     * Removes the specified listener for intent events.
+     *
+     * @param listener listener to be removed
+     */
+    void removeListener(IntentEventListener listener);
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/IntentState.java b/core/api/src/main/java/org/onlab/onos/net/intent/IntentState.java
new file mode 100644
index 0000000..20476e5
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/IntentState.java
@@ -0,0 +1,55 @@
+package org.onlab.onos.net.intent;
+
+/**
+ * This class represents the states of an intent.
+ *
+ * <p>
+ * Note: The state is expressed as enum, but there is possibility
+ * in the future that we define specific class instead of enum to improve
+ * the extensibility of state definition.
+ * </p>
+ */
+public enum IntentState {
+    // FIXME: requires discussion on State vs. EventType and a solid state-transition diagram
+    // TODO: consider the impact of conflict detection
+    // TODO: consider the impact that external events affect an installed intent
+    /**
+     * The beginning state.
+     *
+     * All intent in the runtime take this state first.
+     */
+    SUBMITTED,
+
+    /**
+     * The intent compilation has been completed.
+     *
+     * An intent translation graph (tree) is completely created.
+     * Leaves of the graph are installable intent type.
+     */
+    COMPILED,
+
+    /**
+     * The intent has been successfully installed.
+     */
+    INSTALLED,
+
+    /**
+     * The intent is being withdrawn.
+     *
+     * When {@link IntentService#withdraw(Intent)} is called,
+     * the intent takes this state first.
+     */
+    WITHDRAWING,
+
+    /**
+     * The intent has been successfully withdrawn.
+     */
+    WITHDRAWN,
+
+    /**
+     * The intent has failed to be compiled, installed, or withdrawn.
+     *
+     * When the intent failed to be withdrawn, it is still, at least partially installed.
+     */
+    FAILED,
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java
new file mode 100644
index 0000000..1e421ab
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/MultiPointToSinglePointIntent.java
@@ -0,0 +1,110 @@
+package org.onlab.onos.net.intent;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+import java.util.Set;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Sets;
+
+/**
+ * Abstraction of multiple source to single destination connectivity intent.
+ */
+public class MultiPointToSinglePointIntent extends ConnectivityIntent {
+
+    private final Set<ConnectPoint> ingressPorts;
+    private final ConnectPoint egressPort;
+
+    /**
+     * Creates a new multi-to-single point connectivity intent for the specified
+     * traffic match and action.
+     *
+     * @param id           intent identifier
+     * @param match        traffic match
+     * @param action       action
+     * @param ingressPorts set of ports from which ingress traffic originates
+     * @param egressPort   port to which traffic will egress
+     * @throws NullPointerException if {@code ingressPorts} or
+     * {@code egressPort} is null.
+     * @throws IllegalArgumentException if the size of {@code ingressPorts} is
+     * not more than 1
+     */
+    public MultiPointToSinglePointIntent(IntentId id, TrafficSelector match, TrafficTreatment action,
+            Set<ConnectPoint> ingressPorts, ConnectPoint egressPort) {
+        super(id, match, action);
+
+        checkNotNull(ingressPorts);
+        checkArgument(!ingressPorts.isEmpty(),
+                "there should be at least one ingress port");
+
+        this.ingressPorts = Sets.newHashSet(ingressPorts);
+        this.egressPort = checkNotNull(egressPort);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected MultiPointToSinglePointIntent() {
+        super();
+        this.ingressPorts = null;
+        this.egressPort = null;
+    }
+
+    /**
+     * Returns the set of ports on which ingress traffic should be connected to
+     * the egress port.
+     *
+     * @return set of ingress ports
+     */
+    public Set<ConnectPoint> getIngressPorts() {
+        return ingressPorts;
+    }
+
+    /**
+     * Returns the port on which the traffic should egress.
+     *
+     * @return egress port
+     */
+    public ConnectPoint getEgressPort() {
+        return egressPort;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        MultiPointToSinglePointIntent that = (MultiPointToSinglePointIntent) o;
+        return Objects.equals(this.ingressPorts, that.ingressPorts)
+                && Objects.equals(this.egressPort, that.egressPort);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), ingressPorts, egressPort);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getTrafficSelector())
+                .add("action", getTrafficTreatment())
+                .add("ingressPorts", getIngressPorts())
+                .add("egressPort", getEgressPort())
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/OpticalConnectivityIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/OpticalConnectivityIntent.java
new file mode 100644
index 0000000..d11dc7c
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/OpticalConnectivityIntent.java
@@ -0,0 +1,58 @@
+package org.onlab.onos.net.intent;
+
+import org.onlab.onos.net.ConnectPoint;
+
+// TODO: consider if this intent should be sub-class of ConnectivityIntent
+/**
+ * An optical layer Intent for a connectivity from a transponder port to another
+ * transponder port.
+ * <p>
+ * This class doesn't accepts lambda specifier. This class computes path between
+ * ports and assign lambda automatically. The lambda can be specified using
+ * OpticalPathFlow class.
+ */
+public class OpticalConnectivityIntent extends AbstractIntent {
+    protected ConnectPoint srcConnectPoint;
+    protected ConnectPoint dstConnectPoint;
+
+    /**
+     * Constructor.
+     *
+     * @param id ID for this new Intent object.
+     * @param srcConnectPoint The source transponder port.
+     * @param dstConnectPoint The destination transponder port.
+     */
+    public OpticalConnectivityIntent(IntentId id,
+            ConnectPoint srcConnectPoint, ConnectPoint dstConnectPoint) {
+        super(id);
+        this.srcConnectPoint = srcConnectPoint;
+        this.dstConnectPoint = dstConnectPoint;
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected OpticalConnectivityIntent() {
+        super();
+        this.srcConnectPoint = null;
+        this.dstConnectPoint = null;
+    }
+
+    /**
+     * Gets source transponder port.
+     *
+     * @return The source transponder port.
+     */
+    public ConnectPoint getSrcConnectPoint() {
+        return srcConnectPoint;
+    }
+
+    /**
+     * Gets destination transponder port.
+     *
+     * @return The source transponder port.
+     */
+    public ConnectPoint getDstConnectPoint() {
+        return dstConnectPoint;
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/PacketConnectivityIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/PacketConnectivityIntent.java
new file mode 100644
index 0000000..893d70e
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/PacketConnectivityIntent.java
@@ -0,0 +1,177 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.flow.TrafficSelector;
+
+// TODO: consider if this intent should be sub-class of Connectivity intent
+/**
+ * A packet layer Intent for a connectivity from a set of ports to a set of
+ * ports.
+ * <p>
+ * TODO: Design methods to support the ReactiveForwarding and the SDN-IP. <br>
+ * NOTE: Should this class support modifier methods? Should this object a
+ * read-only object?
+ */
+public class PacketConnectivityIntent extends AbstractIntent {
+    protected Set<ConnectPoint> srcConnectPoints;
+    protected TrafficSelector selector;
+    protected Set<ConnectPoint> dstConnectPoints;
+    protected boolean canSetupOpticalFlow;
+    protected int idleTimeoutValue;
+    protected int hardTimeoutValue;
+
+    /**
+     * Creates a connectivity intent for the packet layer.
+     * <p>
+     * When the "canSetupOpticalFlow" option is true, this intent will compute
+     * the packet/optical converged path, decompose it to the OpticalPathFlow
+     * and the PacketPathFlow objects, and execute the operations to add them
+     * considering the dependency between the packet and optical layers.
+     *
+     * @param id ID for this new Intent object.
+     * @param srcConnectPoints The set of source switch ports.
+     * @param match Traffic specifier for this object.
+     * @param dstConnectPoints The set of destination switch ports.
+     * @param canSetupOpticalFlow The flag whether this intent can create
+     *        optical flows if needed.
+     */
+    public PacketConnectivityIntent(IntentId id,
+            Collection<ConnectPoint> srcConnectPoints, TrafficSelector match,
+            Collection<ConnectPoint> dstConnectPoints, boolean canSetupOpticalFlow) {
+        super(id);
+        this.srcConnectPoints = new HashSet<ConnectPoint>(srcConnectPoints);
+        this.selector = match;
+        this.dstConnectPoints = new HashSet<ConnectPoint>(dstConnectPoints);
+        this.canSetupOpticalFlow = canSetupOpticalFlow;
+        this.idleTimeoutValue = 0;
+        this.hardTimeoutValue = 0;
+
+        // TODO: check consistency between these parameters.
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected PacketConnectivityIntent() {
+        super();
+        this.srcConnectPoints = null;
+        this.selector = null;
+        this.dstConnectPoints = null;
+        this.canSetupOpticalFlow = false;
+        this.idleTimeoutValue = 0;
+        this.hardTimeoutValue = 0;
+    }
+
+    /**
+     * Gets the set of source switch ports.
+     *
+     * @return the set of source switch ports.
+     */
+    public Collection<ConnectPoint> getSrcConnectPoints() {
+        return Collections.unmodifiableCollection(srcConnectPoints);
+    }
+
+    /**
+     * Gets the traffic specifier.
+     *
+     * @return The traffic specifier.
+     */
+    public TrafficSelector getMatch() {
+        return selector;
+    }
+
+    /**
+     * Gets the set of destination switch ports.
+     *
+     * @return the set of destination switch ports.
+     */
+    public Collection<ConnectPoint> getDstConnectPoints() {
+        return Collections.unmodifiableCollection(dstConnectPoints);
+    }
+
+    /**
+     * Adds the specified port to the set of source ports.
+     *
+     * @param port ConnectPoint object to be added
+     */
+    public void addSrcConnectPoint(ConnectPoint port) {
+        // TODO implement it.
+    }
+
+    /**
+     * Adds the specified port to the set of destination ports.
+     *
+     * @param port ConnectPoint object to be added
+     */
+    public void addDstConnectPoint(ConnectPoint port) {
+        // TODO implement it.
+    }
+
+    /**
+     * Removes the specified port from the set of source ports.
+     *
+     * @param port ConnectPoint object to be removed
+     */
+    public void removeSrcConnectPoint(ConnectPoint port) {
+        // TODO implement it.
+    }
+
+    /**
+     * Removes the specified port from the set of destination ports.
+     *
+     * @param port ConnectPoint object to be removed
+     */
+    public void removeDstConnectPoint(ConnectPoint port) {
+        // TODO implement it.
+    }
+
+    /**
+     * Sets idle-timeout value.
+     *
+     * @param timeout Idle-timeout value (seconds)
+     */
+    public void setIdleTimeout(int timeout) {
+        idleTimeoutValue = timeout;
+    }
+
+    /**
+     * Sets hard-timeout value.
+     *
+     * @param timeout Hard-timeout value (seconds)
+     */
+    public void setHardTimeout(int timeout) {
+        hardTimeoutValue = timeout;
+    }
+
+    /**
+     * Gets idle-timeout value.
+     *
+     * @return Idle-timeout value (seconds)
+     */
+    public int getIdleTimeout() {
+        return idleTimeoutValue;
+    }
+
+    /**
+     * Gets hard-timeout value.
+     *
+     * @return Hard-timeout value (seconds)
+     */
+    public int getHardTimeout() {
+        return hardTimeoutValue;
+    }
+
+    /**
+     * Returns whether this intent can create optical flows if needed.
+     *
+     * @return whether this intent can create optical flows.
+     */
+    public boolean canSetupOpticalFlow() {
+        return canSetupOpticalFlow;
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java
new file mode 100644
index 0000000..39ad011
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/PathIntent.java
@@ -0,0 +1,89 @@
+package org.onlab.onos.net.intent;
+
+import java.util.Objects;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.Path;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Abstraction of explicitly path specified connectivity intent.
+ */
+public class PathIntent extends PointToPointIntent {
+
+    private final Path path;
+
+    /**
+     * Creates a new point-to-point intent with the supplied ingress/egress
+     * ports and using the specified explicit path.
+     *
+     * @param id          intent identifier
+     * @param match       traffic match
+     * @param action      action
+     * @param ingressPort ingress port
+     * @param egressPort  egress port
+     * @param path        traversed links
+     * @throws NullPointerException {@code path} is null
+     */
+    public PathIntent(IntentId id, TrafficSelector match, TrafficTreatment action,
+                      ConnectPoint ingressPort, ConnectPoint egressPort,
+                      Path path) {
+        super(id, match, action, ingressPort, egressPort);
+        this.path = path;
+    }
+
+    protected PathIntent() {
+        super();
+        this.path = null;
+    }
+
+    /**
+     * Returns the links which the traffic goes along.
+     *
+     * @return traversed links
+     */
+    public Path getPath() {
+        return path;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        PathIntent that = (PathIntent) o;
+
+        if (!path.equals(that.path)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), path);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getTrafficSelector())
+                .add("action", getTrafficTreatment())
+                .add("ingressPort", getIngressPort())
+                .add("egressPort", getEgressPort())
+                .add("path", path)
+                .toString();
+    }
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java
new file mode 100644
index 0000000..b1d18ee
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/PointToPointIntent.java
@@ -0,0 +1,100 @@
+package org.onlab.onos.net.intent;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import com.google.common.base.MoreObjects;
+
+/**
+ * Abstraction of point-to-point connectivity.
+ */
+public class PointToPointIntent extends ConnectivityIntent {
+
+    private final ConnectPoint ingressPort;
+    private final ConnectPoint egressPort;
+
+    /**
+     * Creates a new point-to-point intent with the supplied ingress/egress
+     * ports.
+     *
+     * @param id          intent identifier
+     * @param match       traffic match
+     * @param action      action
+     * @param ingressPort ingress port
+     * @param egressPort  egress port
+     * @throws NullPointerException if {@code ingressPort} or {@code egressPort} is null.
+     */
+    public PointToPointIntent(IntentId id, TrafficSelector match, TrafficTreatment action,
+                              ConnectPoint ingressPort, ConnectPoint egressPort) {
+        super(id, match, action);
+        this.ingressPort = checkNotNull(ingressPort);
+        this.egressPort = checkNotNull(egressPort);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected PointToPointIntent() {
+        super();
+        this.ingressPort = null;
+        this.egressPort = null;
+    }
+
+    /**
+     * Returns the port on which the ingress traffic should be connected to
+     * the egress.
+     *
+     * @return ingress port
+     */
+    public ConnectPoint getIngressPort() {
+        return ingressPort;
+    }
+
+    /**
+     * Returns the port on which the traffic should egress.
+     *
+     * @return egress port
+     */
+    public ConnectPoint getEgressPort() {
+        return egressPort;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        PointToPointIntent that = (PointToPointIntent) o;
+        return Objects.equals(this.ingressPort, that.ingressPort)
+                && Objects.equals(this.egressPort, that.egressPort);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), ingressPort, egressPort);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getTrafficSelector())
+                .add("action", getTrafficTreatment())
+                .add("ingressPort", ingressPort)
+                .add("egressPort", egressPort)
+                .toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntent.java b/core/api/src/main/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntent.java
new file mode 100644
index 0000000..e69a740
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/SinglePointToMultiPointIntent.java
@@ -0,0 +1,110 @@
+package org.onlab.onos.net.intent;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import java.util.Objects;
+import java.util.Set;
+
+import org.onlab.onos.net.ConnectPoint;
+import org.onlab.onos.net.flow.TrafficSelector;
+import org.onlab.onos.net.flow.TrafficTreatment;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Sets;
+
+/**
+ * Abstraction of single source, multiple destination connectivity intent.
+ */
+public class SinglePointToMultiPointIntent extends ConnectivityIntent {
+
+    private final ConnectPoint ingressPort;
+    private final Set<ConnectPoint> egressPorts;
+
+    /**
+     * Creates a new single-to-multi point connectivity intent.
+     *
+     * @param id          intent identifier
+     * @param match       traffic match
+     * @param action      action
+     * @param ingressPort port on which traffic will ingress
+     * @param egressPorts set of ports on which traffic will egress
+     * @throws NullPointerException if {@code ingressPort} or
+     * {@code egressPorts} is null
+     * @throws IllegalArgumentException if the size of {@code egressPorts} is
+     * not more than 1
+     */
+    public SinglePointToMultiPointIntent(IntentId id, TrafficSelector match, TrafficTreatment action,
+                                         ConnectPoint ingressPort,
+                                         Set<ConnectPoint> egressPorts) {
+        super(id, match, action);
+
+        checkNotNull(egressPorts);
+        checkArgument(!egressPorts.isEmpty(),
+                "there should be at least one egress port");
+
+        this.ingressPort = checkNotNull(ingressPort);
+        this.egressPorts = Sets.newHashSet(egressPorts);
+    }
+
+    /**
+     * Constructor for serializer.
+     */
+    protected SinglePointToMultiPointIntent() {
+        super();
+        this.ingressPort = null;
+        this.egressPorts = null;
+    }
+
+    /**
+     * Returns the port on which the ingress traffic should be connected to the egress.
+     *
+     * @return ingress port
+     */
+    public ConnectPoint getIngressPort() {
+        return ingressPort;
+    }
+
+    /**
+     * Returns the set of ports on which the traffic should egress.
+     *
+     * @return set of egress ports
+     */
+    public Set<ConnectPoint> getEgressPorts() {
+        return egressPorts;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        if (!super.equals(o)) {
+            return false;
+        }
+
+        SinglePointToMultiPointIntent that = (SinglePointToMultiPointIntent) o;
+        return Objects.equals(this.ingressPort, that.ingressPort)
+                && Objects.equals(this.egressPorts, that.egressPorts);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), ingressPort, egressPorts);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getTrafficSelector())
+                .add("action", getTrafficTreatment())
+                .add("ingressPort", ingressPort)
+                .add("egressPort", egressPorts)
+                .toString();
+    }
+
+}
diff --git a/core/api/src/main/java/org/onlab/onos/net/intent/package-info.java b/core/api/src/main/java/org/onlab/onos/net/intent/package-info.java
new file mode 100644
index 0000000..e1e6782
--- /dev/null
+++ b/core/api/src/main/java/org/onlab/onos/net/intent/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Intent Package. TODO
+ */
+
+package org.onlab.onos.net.intent;
\ No newline at end of file
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/KryoSerializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/KryoSerializer.java
new file mode 100644
index 0000000..5d809a4
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/KryoSerializer.java
@@ -0,0 +1,47 @@
+package org.onlab.onos.store.messaging.impl;
+
+import org.onlab.util.KryoPool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Kryo Serializer.
+ */
+public class KryoSerializer implements Serializer {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    private KryoPool serializerPool;
+
+    public KryoSerializer() {
+        setupKryoPool();
+    }
+
+    /**
+     * Sets up the common serialzers pool.
+     */
+    protected void setupKryoPool() {
+        // FIXME Slice out types used in common to separate pool/namespace.
+        serializerPool = KryoPool.newBuilder()
+                .register(ArrayList.class,
+                          HashMap.class,
+                          ArrayList.class
+                )
+                .build()
+                .populate(1);
+    }
+
+
+    @Override
+    public Object decode(byte[] data) {
+        return serializerPool.deserialize(data);
+    }
+
+    @Override
+    public byte[] encode(Object payload) {
+        return serializerPool.serialize(payload);
+    }
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageDecoder.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageDecoder.java
index 7f94015..59790f6 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageDecoder.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageDecoder.java
@@ -5,7 +5,6 @@
 
 import static com.google.common.base.Preconditions.checkState;
 
-import org.onlab.onos.store.cluster.messaging.SerializationService;
 import org.onlab.onos.store.messaging.Endpoint;
 
 import io.netty.buffer.ByteBuf;
@@ -18,11 +17,11 @@
 public class MessageDecoder extends ByteToMessageDecoder {
 
     private final NettyMessagingService messagingService;
-    private final SerializationService serializationService;
+    private final Serializer serializer;
 
-    public MessageDecoder(NettyMessagingService messagingService, SerializationService serializationService) {
+    public MessageDecoder(NettyMessagingService messagingService, Serializer serializer) {
         this.messagingService = messagingService;
-        this.serializationService = serializationService;
+        this.serializer = serializer;
     }
 
     @Override
@@ -47,7 +46,7 @@
         Endpoint sender = new Endpoint(host, port);
 
         // read message payload; first read size and then bytes.
-        Object payload = serializationService.decode(in.readBytes(in.readInt()).array());
+        Object payload = serializer.decode(in.readBytes(in.readInt()).array());
 
         InternalMessage message = new InternalMessage.Builder(messagingService)
                 .withId(id)
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageEncoder.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageEncoder.java
index b1c660c..501b70c 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageEncoder.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/MessageEncoder.java
@@ -1,7 +1,5 @@
 package org.onlab.onos.store.messaging.impl;
 
-import org.onlab.onos.store.cluster.messaging.SerializationService;
-
 import io.netty.buffer.ByteBuf;
 import io.netty.channel.ChannelHandlerContext;
 import io.netty.handler.codec.MessageToByteEncoder;
@@ -14,10 +12,10 @@
     // onosiscool in ascii
     public static final byte[] PREAMBLE = "onosiscool".getBytes();
 
-    private final SerializationService serializationService;
+    private final Serializer serializer;
 
-    public MessageEncoder(SerializationService serializationService) {
-        this.serializationService = serializationService;
+    public MessageEncoder(Serializer serializer) {
+        this.serializer = serializer;
     }
 
     @Override
@@ -46,12 +44,12 @@
         out.writeInt(message.sender().port());
 
         try {
-            serializationService.encode(message.payload());
+            serializer.encode(message.payload());
         } catch (Exception e) {
             e.printStackTrace();
         }
 
-        byte[] payload = serializationService.encode(message.payload());
+        byte[] payload = serializer.encode(message.payload());
 
         // write payload length.
         out.writeInt(payload.length);
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/NettyMessagingService.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/NettyMessagingService.java
index b6b3857..321e0ef 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/NettyMessagingService.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/NettyMessagingService.java
@@ -31,7 +31,6 @@
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
 import org.apache.felix.scr.annotations.Service;
-import org.onlab.onos.store.cluster.messaging.SerializationService;
 import org.onlab.onos.store.messaging.Endpoint;
 import org.onlab.onos.store.messaging.MessageHandler;
 import org.onlab.onos.store.messaging.MessagingService;
@@ -61,7 +60,7 @@
     private final Endpoint localEp;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
-    protected SerializationService serializationService;
+    protected Serializer serializer;
 
     public NettyMessagingService() {
         // TODO: Default port should be configurable.
@@ -213,8 +212,8 @@
         @Override
         protected void initChannel(SocketChannel channel) throws Exception {
             channel.pipeline()
-                .addLast(new MessageEncoder(serializationService))
-                .addLast(new MessageDecoder(NettyMessagingService.this, serializationService))
+                .addLast(new MessageEncoder(serializer))
+                .addLast(new MessageDecoder(NettyMessagingService.this, serializer))
                 .addLast(new NettyMessagingService.InboundMessageDispatcher());
         }
     }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/Serializer.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/Serializer.java
new file mode 100644
index 0000000..d2da7cf
--- /dev/null
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/Serializer.java
@@ -0,0 +1,24 @@
+package org.onlab.onos.store.messaging.impl;
+
+/**
+ * Interface for encoding/decoding message payloads.
+ */
+public interface Serializer {
+
+    /**
+     * Decodes the specified byte array to a POJO.
+     *
+     * @param data byte array.
+     * @return POJO
+     */
+    Object decode(byte[] data);
+
+    /**
+     * Encodes the specified POJO into a byte array.
+     *
+     * @param data POJO to be encoded
+     * @return byte array.
+     */
+    byte[] encode(Object message);
+
+}
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleClient.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleClient.java
index 95753e7..746ecb2 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleClient.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleClient.java
@@ -2,7 +2,6 @@
 
 import java.util.concurrent.TimeUnit;
 
-import org.onlab.onos.store.cluster.impl.MessageSerializer;
 import org.onlab.onos.store.messaging.Endpoint;
 import org.onlab.onos.store.messaging.Response;
 
@@ -21,9 +20,8 @@
     public static class TestNettyMessagingService extends NettyMessagingService {
         public TestNettyMessagingService(int port) throws Exception {
             super(port);
-            MessageSerializer mgr = new MessageSerializer();
-            mgr.activate();
-            this.serializationService = mgr;
+            Serializer serializer = new KryoSerializer();
+            this.serializer = serializer;
         }
     }
 }
diff --git a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleServer.java b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleServer.java
index 1b331ba..96094b7 100644
--- a/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleServer.java
+++ b/core/store/dist/src/main/java/org/onlab/onos/store/messaging/impl/SimpleServer.java
@@ -1,7 +1,5 @@
 package org.onlab.onos.store.messaging.impl;
 
-import org.onlab.onos.store.cluster.impl.MessageSerializer;
-
 public final class SimpleServer {
     private SimpleServer() {}
 
@@ -14,9 +12,8 @@
 
     public static class TestNettyMessagingService extends NettyMessagingService {
         protected TestNettyMessagingService() {
-            MessageSerializer mgr = new MessageSerializer();
-            mgr.activate();
-            this.serializationService = mgr;
+            Serializer serializer = new KryoSerializer();
+            this.serializer = serializer;
         }
     }
 }
diff --git a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
index bc0a055..0b0ae37 100644
--- a/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
+++ b/core/store/trivial/src/main/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStore.java
@@ -9,6 +9,8 @@
 import org.apache.felix.scr.annotations.Component;
 import org.apache.felix.scr.annotations.Deactivate;
 import org.apache.felix.scr.annotations.Service;
+import org.onlab.onos.net.Annotations;
+import org.onlab.onos.net.DefaultAnnotations;
 import org.onlab.onos.net.DefaultDevice;
 import org.onlab.onos.net.DefaultPort;
 import org.onlab.onos.net.Device;
@@ -16,6 +18,9 @@
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.Port;
 import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.SparseAnnotations;
+import org.onlab.onos.net.device.DefaultDeviceDescription;
+import org.onlab.onos.net.device.DefaultPortDescription;
 import org.onlab.onos.net.device.DeviceDescription;
 import org.onlab.onos.net.device.DeviceEvent;
 import org.onlab.onos.net.device.DeviceStore;
@@ -45,6 +50,7 @@
 import static org.onlab.onos.net.device.DeviceEvent.Type.*;
 import static org.slf4j.LoggerFactory.getLogger;
 import static org.apache.commons.lang3.concurrent.ConcurrentUtils.createIfAbsentUnchecked;
+import static org.onlab.onos.net.DefaultAnnotations.merge;
 
 // TODO: synchronization should be done in more fine-grained manner.
 /**
@@ -112,8 +118,8 @@
             = createIfAbsentUnchecked(providerDescs, providerId,
                     new InitDeviceDescs(deviceDescription));
 
+        // update description
         descs.putDeviceDesc(deviceDescription);
-
         Device newDevice = composeDevice(deviceId, providerDescs);
 
         if (oldDevice == null) {
@@ -144,7 +150,8 @@
 
         // We allow only certain attributes to trigger update
         if (!Objects.equals(oldDevice.hwVersion(), newDevice.hwVersion()) ||
-            !Objects.equals(oldDevice.swVersion(), newDevice.swVersion())) {
+            !Objects.equals(oldDevice.swVersion(), newDevice.swVersion()) ||
+            !isAnnotationsEqual(oldDevice.annotations(), newDevice.annotations())) {
 
             synchronized (this) {
                 devices.replace(newDevice.id(), oldDevice, newDevice);
@@ -203,7 +210,7 @@
                 PortNumber number = portDescription.portNumber();
                 Port oldPort = ports.get(number);
                 // update description
-                descs.putPortDesc(number, portDescription);
+                descs.putPortDesc(portDescription);
                 Port newPort = composePort(device, number, descsMap);
 
                 events.add(oldPort == null ?
@@ -225,12 +232,14 @@
         return new DeviceEvent(PORT_ADDED, device, newPort);
     }
 
-    // CHecks if the specified port requires update and if so, it replaces the
+    // Checks if the specified port requires update and if so, it replaces the
     // existing entry in the map and returns corresponding event.
     private DeviceEvent updatePort(Device device, Port oldPort,
                                    Port newPort,
                                    ConcurrentMap<PortNumber, Port> ports) {
-        if (oldPort.isEnabled() != newPort.isEnabled()) {
+        if (oldPort.isEnabled() != newPort.isEnabled() ||
+            !isAnnotationsEqual(oldPort.annotations(), newPort.annotations())) {
+
             ports.put(oldPort.number(), newPort);
             return new DeviceEvent(PORT_UPDATED, device, newPort);
         }
@@ -272,17 +281,17 @@
         checkArgument(descsMap != null, DEVICE_NOT_FOUND, deviceId);
 
         DeviceDescriptions descs = descsMap.get(providerId);
+        // assuming all providers must to give DeviceDescription
         checkArgument(descs != null,
                 "Device description for Device ID %s from Provider %s was not found",
                 deviceId, providerId);
 
-        // TODO: implement multi-provider
         synchronized (this) {
             ConcurrentMap<PortNumber, Port> ports = getPortMap(deviceId);
             final PortNumber number = portDescription.portNumber();
             Port oldPort = ports.get(number);
             // update description
-            descs.putPortDesc(number, portDescription);
+            descs.putPortDesc(portDescription);
             Port newPort = composePort(device, number, descsMap);
             if (oldPort == null) {
                 return createPort(device, newPort, ports);
@@ -321,6 +330,26 @@
         }
     }
 
+    private static boolean isAnnotationsEqual(Annotations lhs, Annotations rhs) {
+        if (lhs == rhs) {
+            return true;
+        }
+        if (lhs == null || rhs == null) {
+            return false;
+        }
+
+        if (!lhs.keys().equals(rhs.keys())) {
+            return false;
+        }
+
+        for (String key : lhs.keys()) {
+            if (!lhs.value(key).equals(rhs.value(key))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Returns a Device, merging description given from multiple Providers.
      *
@@ -336,46 +365,67 @@
         ProviderId primary = pickPrimaryPID(providerDescs);
 
         DeviceDescriptions desc = providerDescs.get(primary);
+
+        // base
         Type type = desc.getDeviceDesc().type();
         String manufacturer = desc.getDeviceDesc().manufacturer();
         String hwVersion = desc.getDeviceDesc().hwVersion();
         String swVersion = desc.getDeviceDesc().swVersion();
         String serialNumber = desc.getDeviceDesc().serialNumber();
+        DefaultAnnotations annotations = DefaultAnnotations.builder().build();
+        annotations = merge(annotations, desc.getDeviceDesc().annotations());
 
         for (Entry<ProviderId, DeviceDescriptions> e : providerDescs.entrySet()) {
             if (e.getKey().equals(primary)) {
                 continue;
             }
-            // FIXME: implement attribute merging once we have K-V attributes
+            // TODO: should keep track of Description timestamp
+            // and only merge conflicting keys when timestamp is newer
+            // Currently assuming there will never be a key conflict between
+            // providers
+
+            // annotation merging. not so efficient, should revisit later
+            annotations = merge(annotations, e.getValue().getDeviceDesc().annotations());
         }
 
-        return new DefaultDevice(primary, deviceId , type, manufacturer, hwVersion, swVersion, serialNumber);
+        return new DefaultDevice(primary, deviceId , type, manufacturer,
+                            hwVersion, swVersion, serialNumber, annotations);
     }
 
-    // probably want composePorts
+    // probably want composePort"s" also
     private Port composePort(Device device, PortNumber number,
                 ConcurrentMap<ProviderId, DeviceDescriptions> providerDescs) {
 
         ProviderId primary = pickPrimaryPID(providerDescs);
         DeviceDescriptions primDescs = providerDescs.get(primary);
+        // if no primary, assume not enabled
+        // TODO: revisit this default port enabled/disabled behavior
+        boolean isEnabled = false;
+        DefaultAnnotations annotations = DefaultAnnotations.builder().build();
+
         final PortDescription portDesc = primDescs.getPortDesc(number);
-        boolean isEnabled;
         if (portDesc != null) {
             isEnabled = portDesc.isEnabled();
-        } else {
-            // if no primary, assume not enabled
-            // TODO: revisit this port enabled/disabled behavior
-            isEnabled = false;
+            annotations = merge(annotations, portDesc.annotations());
         }
 
         for (Entry<ProviderId, DeviceDescriptions> e : providerDescs.entrySet()) {
             if (e.getKey().equals(primary)) {
                 continue;
             }
-            // FIXME: implement attribute merging once we have K-V attributes
+            // TODO: should keep track of Description timestamp
+            // and only merge conflicting keys when timestamp is newer
+            // Currently assuming there will never be a key conflict between
+            // providers
+
+            // annotation merging. not so efficient, should revisit later
+            final PortDescription otherPortDesc = e.getValue().getPortDesc(number);
+            if (otherPortDesc != null) {
+                annotations = merge(annotations, otherPortDesc.annotations());
+            }
         }
 
-        return new DefaultPort(device, number, isEnabled);
+        return new DefaultPort(device, number, isEnabled, annotations);
     }
 
     /**
@@ -428,7 +478,7 @@
         private final ConcurrentMap<PortNumber, PortDescription> portDescs;
 
         public DeviceDescriptions(DeviceDescription desc) {
-            this.deviceDesc = new AtomicReference<>(desc);
+            this.deviceDesc = new AtomicReference<>(checkNotNull(desc));
             this.portDescs = new ConcurrentHashMap<>();
         }
 
@@ -444,12 +494,38 @@
             return Collections.unmodifiableCollection(portDescs.values());
         }
 
-        public DeviceDescription putDeviceDesc(DeviceDescription newDesc) {
-            return deviceDesc.getAndSet(newDesc);
+        /**
+         * Puts DeviceDescription, merging annotations as necessary.
+         *
+         * @param newDesc new DeviceDescription
+         * @return previous DeviceDescription
+         */
+        public synchronized DeviceDescription putDeviceDesc(DeviceDescription newDesc) {
+            DeviceDescription oldOne = deviceDesc.get();
+            DeviceDescription newOne = newDesc;
+            if (oldOne != null) {
+                SparseAnnotations merged = merge(oldOne.annotations(),
+                                                 newDesc.annotations());
+                newOne = new DefaultDeviceDescription(newOne, merged);
+            }
+            return deviceDesc.getAndSet(newOne);
         }
 
-        public PortDescription putPortDesc(PortNumber number, PortDescription newDesc) {
-            return portDescs.put(number, newDesc);
+        /**
+         * Puts PortDescription, merging annotations as necessary.
+         *
+         * @param newDesc new PortDescription
+         * @return previous PortDescription
+         */
+        public synchronized PortDescription putPortDesc(PortDescription newDesc) {
+            PortDescription oldOne = portDescs.get(newDesc.portNumber());
+            PortDescription newOne = newDesc;
+            if (oldOne != null) {
+                SparseAnnotations merged = merge(oldOne.annotations(),
+                                                 newDesc.annotations());
+                newOne = new DefaultPortDescription(newOne, merged);
+            }
+            return portDescs.put(newOne.portNumber(), newOne);
         }
     }
 }
diff --git a/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java b/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java
index 431fba3..a0d6e1c 100644
--- a/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java
+++ b/core/store/trivial/src/test/java/org/onlab/onos/store/trivial/impl/SimpleDeviceStoreTest.java
@@ -22,10 +22,13 @@
 import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
+import org.onlab.onos.net.Annotations;
+import org.onlab.onos.net.DefaultAnnotations;
 import org.onlab.onos.net.Device;
 import org.onlab.onos.net.DeviceId;
 import org.onlab.onos.net.Port;
 import org.onlab.onos.net.PortNumber;
+import org.onlab.onos.net.SparseAnnotations;
 import org.onlab.onos.net.device.DefaultDeviceDescription;
 import org.onlab.onos.net.device.DefaultPortDescription;
 import org.onlab.onos.net.device.DeviceDescription;
@@ -57,6 +60,23 @@
     private static final PortNumber P2 = PortNumber.portNumber(2);
     private static final PortNumber P3 = PortNumber.portNumber(3);
 
+    private static final SparseAnnotations A1 = DefaultAnnotations.builder()
+            .set("A1", "a1")
+            .set("B1", "b1")
+            .build();
+    private static final SparseAnnotations A1_2 = DefaultAnnotations.builder()
+            .remove("A1")
+            .set("B3", "b3")
+            .build();
+    private static final SparseAnnotations A2 = DefaultAnnotations.builder()
+            .set("A2", "a2")
+            .set("B2", "b2")
+            .build();
+    private static final SparseAnnotations A2_2 = DefaultAnnotations.builder()
+            .remove("A2")
+            .set("B4", "b4")
+            .build();
+
     private SimpleDeviceStore simpleDeviceStore;
     private DeviceStore deviceStore;
 
@@ -106,6 +126,24 @@
         assertEquals(SN, device.serialNumber());
     }
 
+    /**
+     * Verifies that Annotations created by merging {@code annotations} is
+     * equal to actual Annotations.
+     *
+     * @param actual Annotations to check
+     * @param annotations
+     */
+    private static void assertAnnotationsEquals(Annotations actual, SparseAnnotations... annotations) {
+        DefaultAnnotations expected = DefaultAnnotations.builder().build();
+        for (SparseAnnotations a : annotations) {
+            expected = DefaultAnnotations.merge(expected, a);
+        }
+        assertEquals(expected.keys(), actual.keys());
+        for (String key : expected.keys()) {
+            assertEquals(expected.value(key), actual.value(key));
+        }
+    }
+
     @Test
     public final void testGetDeviceCount() {
         assertEquals("initialy empty", 0, deviceStore.getDeviceCount());
@@ -171,26 +209,41 @@
     public final void testCreateOrUpdateDeviceAncillary() {
         DeviceDescription description =
                 new DefaultDeviceDescription(DID1.uri(), SWITCH, MFR,
-                        HW, SW1, SN);
+                        HW, SW1, SN, A2);
         DeviceEvent event = deviceStore.createOrUpdateDevice(PIDA, DID1, description);
         assertEquals(DEVICE_ADDED, event.type());
         assertDevice(DID1, SW1, event.subject());
         assertEquals(PIDA, event.subject().providerId());
+        assertAnnotationsEquals(event.subject().annotations(), A2);
         assertFalse("Ancillary will not bring device up", deviceStore.isAvailable(DID1));
 
         DeviceDescription description2 =
                 new DefaultDeviceDescription(DID1.uri(), SWITCH, MFR,
-                        HW, SW2, SN);
+                        HW, SW2, SN, A1);
         DeviceEvent event2 = deviceStore.createOrUpdateDevice(PID, DID1, description2);
         assertEquals(DEVICE_UPDATED, event2.type());
         assertDevice(DID1, SW2, event2.subject());
         assertEquals(PID, event2.subject().providerId());
+        assertAnnotationsEquals(event2.subject().annotations(), A1, A2);
         assertTrue(deviceStore.isAvailable(DID1));
 
         assertNull("No change expected", deviceStore.createOrUpdateDevice(PID, DID1, description2));
 
         // For now, Ancillary is ignored once primary appears
         assertNull("No change expected", deviceStore.createOrUpdateDevice(PIDA, DID1, description));
+
+        // But, Ancillary annotations will be in effect
+        DeviceDescription description3 =
+                new DefaultDeviceDescription(DID1.uri(), SWITCH, MFR,
+                        HW, SW1, SN, A2_2);
+        DeviceEvent event3 = deviceStore.createOrUpdateDevice(PIDA, DID1, description3);
+        assertEquals(DEVICE_UPDATED, event3.type());
+        // basic information will be the one from Primary
+        assertDevice(DID1, SW2, event3.subject());
+        assertEquals(PID, event3.subject().providerId());
+        // but annotation from Ancillary will be merged
+        assertAnnotationsEquals(event3.subject().annotations(), A1, A2, A2_2);
+        assertTrue(deviceStore.isAvailable(DID1));
     }
 
 
@@ -299,27 +352,40 @@
         putDeviceAncillary(DID1, SW1);
         putDevice(DID1, SW1);
         List<PortDescription> pds = Arrays.<PortDescription>asList(
-                new DefaultPortDescription(P1, true)
+                new DefaultPortDescription(P1, true, A1)
                 );
         deviceStore.updatePorts(PID, DID1, pds);
 
         DeviceEvent event = deviceStore.updatePortStatus(PID, DID1,
-                new DefaultPortDescription(P1, false));
+                new DefaultPortDescription(P1, false, A1_2));
         assertEquals(PORT_UPDATED, event.type());
         assertDevice(DID1, SW1, event.subject());
         assertEquals(P1, event.port().number());
+        assertAnnotationsEquals(event.port().annotations(), A1, A1_2);
         assertFalse("Port is disabled", event.port().isEnabled());
 
         DeviceEvent event2 = deviceStore.updatePortStatus(PIDA, DID1,
                 new DefaultPortDescription(P1, true));
         assertNull("Ancillary is ignored if primary exists", event2);
 
+        // but, Ancillary annotation update will be notified
         DeviceEvent event3 = deviceStore.updatePortStatus(PIDA, DID1,
-                new DefaultPortDescription(P2, true));
-        assertEquals(PORT_ADDED, event3.type());
+                new DefaultPortDescription(P1, true, A2));
+        assertEquals(PORT_UPDATED, event3.type());
         assertDevice(DID1, SW1, event3.subject());
-        assertEquals(P2, event3.port().number());
-        assertFalse("Port is disabled if not given from provider", event3.port().isEnabled());
+        assertEquals(P1, event3.port().number());
+        assertAnnotationsEquals(event3.port().annotations(), A1, A1_2, A2);
+        assertFalse("Port is disabled", event3.port().isEnabled());
+
+        // port only reported from Ancillary will be notified as down
+        DeviceEvent event4 = deviceStore.updatePortStatus(PIDA, DID1,
+                new DefaultPortDescription(P2, true));
+        assertEquals(PORT_ADDED, event4.type());
+        assertDevice(DID1, SW1, event4.subject());
+        assertEquals(P2, event4.port().number());
+        assertAnnotationsEquals(event4.port().annotations());
+        assertFalse("Port is disabled if not given from primary provider",
+                        event4.port().isEnabled());
     }
 
     @Test