Define the new Intent Framework APIs (Intent Service)

- Define the base Intent class and sub-classes required in SDN-IP
- IntentService provides the APIs for application developers
- IntentExtensionService enables to define application specific intent types
- Provide event handling mechanism via IntentEventListener

This is for ONOS-1654.

Change-Id: Id1705f1fbc1acd4862b33fd9ab97aafe2e84a685
diff --git a/src/main/java/net/onrc/onos/api/newintent/AbstractIntent.java b/src/main/java/net/onrc/onos/api/newintent/AbstractIntent.java
new file mode 100644
index 0000000..f2e3d8a
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/AbstractIntent.java
@@ -0,0 +1,42 @@
+package net.onrc.onos.api.newintent;
+
+/**
+ * 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;
+    }
+
+    @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/src/main/java/net/onrc/onos/api/newintent/ConnectivityIntent.java b/src/main/java/net/onrc/onos/api/newintent/ConnectivityIntent.java
new file mode 100644
index 0000000..ecb1d6b
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/ConnectivityIntent.java
@@ -0,0 +1,74 @@
+package net.onrc.onos.api.newintent;
+
+import com.google.common.base.Objects;
+import net.onrc.onos.core.matchaction.action.Action;
+import net.onrc.onos.core.matchaction.match.Match;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * 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 Match match;
+    // TODO: should consider which is better for multiple actions,
+    // defining compound action class or using list of actions.
+    private final Action action;
+
+    /**
+     * 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, Match match, Action action) {
+        super(id);
+        this.match = checkNotNull(match);
+        this.action = checkNotNull(action);
+    }
+
+    /**
+     * Returns the match specifying the type of traffic.
+     *
+     * @return traffic match
+     */
+    public Match getMatch() {
+        return match;
+    }
+
+    /**
+     * Returns the action applied to the traffic.
+     *
+     * @return applied action
+     */
+    public Action getAction() {
+        return action;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!super.equals(o)) {
+            return false;
+        }
+        ConnectivityIntent that = (ConnectivityIntent) o;
+        return Objects.equal(this.match, that.match)
+                && Objects.equal(this.action, that.action);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(super.hashCode(), match, action);
+    }
+
+}
diff --git a/src/main/java/net/onrc/onos/api/newintent/InstallableIntent.java b/src/main/java/net/onrc/onos/api/newintent/InstallableIntent.java
new file mode 100644
index 0000000..b31375d
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/InstallableIntent.java
@@ -0,0 +1,8 @@
+package net.onrc.onos.api.newintent;
+
+/**
+ * Abstraction of an intent that can be installed into
+ * the underlying system without additional compilation.
+ */
+public interface InstallableIntent extends Intent {
+}
diff --git a/src/main/java/net/onrc/onos/api/newintent/Intent.java b/src/main/java/net/onrc/onos/api/newintent/Intent.java
new file mode 100644
index 0000000..009725c
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/Intent.java
@@ -0,0 +1,15 @@
+package net.onrc.onos.api.newintent;
+
+/**
+ * Abstraction of an application level intent.
+ *
+ * Make sure that an Intent should be immutable when a new type is defined.
+ */
+public interface Intent {
+    /**
+     * Returns the intent identifier.
+     *
+     * @return intent identifier
+     */
+    IntentId getId();
+}
diff --git a/src/main/java/net/onrc/onos/api/newintent/IntentCompiler.java b/src/main/java/net/onrc/onos/api/newintent/IntentCompiler.java
new file mode 100644
index 0000000..c778c11
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/IntentCompiler.java
@@ -0,0 +1,20 @@
+package net.onrc.onos.api.newintent;
+
+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/src/main/java/net/onrc/onos/api/newintent/IntentEvent.java b/src/main/java/net/onrc/onos/api/newintent/IntentEvent.java
new file mode 100644
index 0000000..670e87e
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/IntentEvent.java
@@ -0,0 +1,101 @@
+package net.onrc.onos.api.newintent;
+
+import com.google.common.base.Objects;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * 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;
+    }
+
+    /**
+     * 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.equal(this.intent, that.intent)
+                && Objects.equal(this.state, that.state)
+                && Objects.equal(this.previous, that.previous)
+                && Objects.equal(this.time, that.time);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(intent, state, previous, time);
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(getClass())
+                .add("intent", intent)
+                .add("state", state)
+                .add("previous", previous)
+                .add("time", time)
+                .toString();
+    }
+}
diff --git a/src/main/java/net/onrc/onos/api/newintent/IntentEventListener.java b/src/main/java/net/onrc/onos/api/newintent/IntentEventListener.java
new file mode 100644
index 0000000..6d8bda2
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/IntentEventListener.java
@@ -0,0 +1,13 @@
+package net.onrc.onos.api.newintent;
+
+/**
+ * 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/src/main/java/net/onrc/onos/api/newintent/IntentException.java b/src/main/java/net/onrc/onos/api/newintent/IntentException.java
new file mode 100644
index 0000000..fc966ee
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/IntentException.java
@@ -0,0 +1,35 @@
+package net.onrc.onos.api.newintent;
+
+/**
+ * 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/src/main/java/net/onrc/onos/api/newintent/IntentExtensionService.java b/src/main/java/net/onrc/onos/api/newintent/IntentExtensionService.java
new file mode 100644
index 0000000..1686e5f
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/IntentExtensionService.java
@@ -0,0 +1,57 @@
+package net.onrc.onos.api.newintent;
+
+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/src/main/java/net/onrc/onos/api/newintent/IntentId.java b/src/main/java/net/onrc/onos/api/newintent/IntentId.java
new file mode 100644
index 0000000..6d10b35
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/IntentId.java
@@ -0,0 +1,62 @@
+package net.onrc.onos.api.newintent;
+
+/**
+ * Intent identifier suitable as an external key.
+ *
+ * This class is immutable.
+ */
+public final class IntentId {
+
+    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.startsWith("0x")
+                ? Long.parseLong(value.substring(2), HEX)
+                : Long.parseLong(value, DEC);
+        return new IntentId(id);
+    }
+
+
+    /**
+     * 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/src/main/java/net/onrc/onos/api/newintent/IntentInstaller.java b/src/main/java/net/onrc/onos/api/newintent/IntentInstaller.java
new file mode 100644
index 0000000..940d6ab
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/IntentInstaller.java
@@ -0,0 +1,22 @@
+package net.onrc.onos.api.newintent;
+
+/**
+ * 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);
+
+    /**
+     * Removes the specified intent from the environment.
+     *
+     * @param intent intent to be removed
+     * @throws IntentException if issues are encountered while removing the intent
+     */
+    void remove(T intent); // TODO: consider calling this uninstall for symmetry
+}
diff --git a/src/main/java/net/onrc/onos/api/newintent/IntentOperations.java b/src/main/java/net/onrc/onos/api/newintent/IntentOperations.java
new file mode 100644
index 0000000..a556048
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/IntentOperations.java
@@ -0,0 +1,10 @@
+package net.onrc.onos.api.newintent;
+
+/**
+ * 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/src/main/java/net/onrc/onos/api/newintent/IntentService.java b/src/main/java/net/onrc/onos/api/newintent/IntentService.java
new file mode 100644
index 0000000..14fa1dc
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/IntentService.java
@@ -0,0 +1,76 @@
+package net.onrc.onos.api.newintent;
+
+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/src/main/java/net/onrc/onos/api/newintent/IntentState.java b/src/main/java/net/onrc/onos/api/newintent/IntentState.java
new file mode 100644
index 0000000..f272320
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/IntentState.java
@@ -0,0 +1,55 @@
+package net.onrc.onos.api.newintent;
+
+/**
+ * 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 net.onrc.onos.api.newintent.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/src/main/java/net/onrc/onos/api/newintent/MultiPointToSinglePointIntent.java b/src/main/java/net/onrc/onos/api/newintent/MultiPointToSinglePointIntent.java
new file mode 100644
index 0000000..99c1c02
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/MultiPointToSinglePointIntent.java
@@ -0,0 +1,96 @@
+package net.onrc.onos.api.newintent;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableSet;
+import net.onrc.onos.core.matchaction.action.Action;
+import net.onrc.onos.core.matchaction.match.Match;
+import net.onrc.onos.core.util.SwitchPort;
+
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Abstraction of multiple source to single destination connectivity intent.
+ */
+public class MultiPointToSinglePointIntent extends ConnectivityIntent {
+
+    private final Set<SwitchPort> ingressPorts;
+    private final SwitchPort 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, Match match, Action action,
+                                         Set<SwitchPort> ingressPorts, SwitchPort egressPort) {
+        super(id, match, action);
+
+        checkNotNull(ingressPorts);
+        checkArgument(ingressPorts.size() > 1, "the number of ingress ports should be more than 1, " +
+                "but actually %s", ingressPorts.size());
+
+        this.ingressPorts = ImmutableSet.copyOf(ingressPorts);
+        this.egressPort = checkNotNull(egressPort);
+    }
+
+    /**
+     * Returns the set of ports on which ingress traffic should be connected to the egress port.
+     *
+     * @return set of ingress ports
+     */
+    public Set<SwitchPort> getIngressPorts() {
+        return ingressPorts;
+    }
+
+    /**
+     * Returns the port on which the traffic should egress.
+     *
+     * @return egress port
+     */
+    public SwitchPort 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.equal(this.ingressPorts, that.ingressPorts)
+                && Objects.equal(this.egressPort, that.egressPort);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(super.hashCode(), ingressPorts, egressPort);
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getMatch())
+                .add("aciton", getAction())
+                .add("ingressPorts", getIngressPorts())
+                .add("egressPort", getEgressPort())
+                .toString();
+    }
+}
diff --git a/src/main/java/net/onrc/onos/api/newintent/PathIntent.java b/src/main/java/net/onrc/onos/api/newintent/PathIntent.java
new file mode 100644
index 0000000..db11f4c
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/PathIntent.java
@@ -0,0 +1,86 @@
+package net.onrc.onos.api.newintent;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import net.onrc.onos.core.matchaction.action.Action;
+import net.onrc.onos.core.matchaction.match.Match;
+import net.onrc.onos.core.util.LinkTuple;
+import net.onrc.onos.core.util.SwitchPort;
+
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Abstraction of explicitly path specified connectivity intent.
+ */
+public class PathIntent extends PointToPointIntent {
+
+    private final List<LinkTuple> 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, Match match, Action action,
+                      SwitchPort ingressPort, SwitchPort egressPort,
+                      List<LinkTuple> path) {
+        super(id, match, action, ingressPort, egressPort);
+        this.path = ImmutableList.copyOf(checkNotNull(path));
+    }
+
+    /**
+     * Returns the links which the traffic goes along.
+     *
+     * @return traversed links
+     */
+    public List<LinkTuple> 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.hashCode(super.hashCode(), path);
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getMatch())
+                .add("action", getAction())
+                .add("ingressPort", getIngressPort())
+                .add("egressPort", getEgressPort())
+                .add("path", path)
+                .toString();
+    }
+}
diff --git a/src/main/java/net/onrc/onos/api/newintent/PointToPointIntent.java b/src/main/java/net/onrc/onos/api/newintent/PointToPointIntent.java
new file mode 100644
index 0000000..9672690
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/PointToPointIntent.java
@@ -0,0 +1,89 @@
+package net.onrc.onos.api.newintent;
+
+import com.google.common.base.Objects;
+import net.onrc.onos.core.matchaction.action.Action;
+import net.onrc.onos.core.matchaction.match.Match;
+import net.onrc.onos.core.util.SwitchPort;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Abstraction of point-to-point connectivity.
+ */
+public class PointToPointIntent extends ConnectivityIntent {
+
+    private final SwitchPort ingressPort;
+    private final SwitchPort 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, Match match, Action action,
+                              SwitchPort ingressPort, SwitchPort egressPort) {
+        super(id, match, action);
+        this.ingressPort = checkNotNull(ingressPort);
+        this.egressPort = checkNotNull(egressPort);
+    }
+
+
+    /**
+     * Returns the port on which the ingress traffic should be connected to
+     * the egress.
+     *
+     * @return ingress port
+     */
+    public SwitchPort getIngressPort() {
+        return ingressPort;
+    }
+
+    /**
+     * Returns the port on which the traffic should egress.
+     *
+     * @return egress port
+     */
+    public SwitchPort 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.equal(this.ingressPort, that.ingressPort)
+                && Objects.equal(this.egressPort, that.egressPort);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(super.hashCode(), ingressPort, egressPort);
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getMatch())
+                .add("action", getAction())
+                .add("ingressPort", ingressPort)
+                .add("egressPort", egressPort)
+                .toString();
+    }
+
+}
diff --git a/src/main/java/net/onrc/onos/api/newintent/SinglePointToMultiPointIntent.java b/src/main/java/net/onrc/onos/api/newintent/SinglePointToMultiPointIntent.java
new file mode 100644
index 0000000..e2ee22f
--- /dev/null
+++ b/src/main/java/net/onrc/onos/api/newintent/SinglePointToMultiPointIntent.java
@@ -0,0 +1,97 @@
+package net.onrc.onos.api.newintent;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableSet;
+import net.onrc.onos.core.matchaction.action.Action;
+import net.onrc.onos.core.matchaction.match.Match;
+import net.onrc.onos.core.util.SwitchPort;
+
+import java.util.Set;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Abstraction of single source, multiple destination connectivity intent.
+ */
+public class SinglePointToMultiPointIntent extends ConnectivityIntent {
+
+    private final SwitchPort ingressPort;
+    private final Set<SwitchPort> 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, Match match, Action action,
+                                         SwitchPort ingressPort,
+                                         Set<SwitchPort> egressPorts) {
+        super(id, match, action);
+
+        checkNotNull(egressPorts);
+        checkArgument(egressPorts.size() > 1, "the number of egress ports should be more than 1, " +
+                "but actually %s", egressPorts.size());
+
+        this.ingressPort = checkNotNull(ingressPort);
+        this.egressPorts = ImmutableSet.copyOf(egressPorts);
+    }
+
+    /**
+     * Returns the port on which the ingress traffic should be connected to the egress.
+     *
+     * @return ingress port
+     */
+    public SwitchPort getIngressPort() {
+        return ingressPort;
+    }
+
+    /**
+     * Returns the set of ports on which the traffic should egress.
+     *
+     * @return set of egress ports
+     */
+    public Set<SwitchPort> 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.equal(this.ingressPort, that.ingressPort)
+                && Objects.equal(this.egressPorts, that.egressPorts);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(super.hashCode(), ingressPort, egressPorts);
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(getClass())
+                .add("id", getId())
+                .add("match", getMatch())
+                .add("action", getAction())
+                .add("ingressPort", ingressPort)
+                .add("egressPort", egressPorts)
+                .toString();
+    }
+
+}
diff --git a/src/test/java/net/onrc/onos/api/newintent/ConnectivityIntentTest.java b/src/test/java/net/onrc/onos/api/newintent/ConnectivityIntentTest.java
new file mode 100644
index 0000000..bb05832
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/newintent/ConnectivityIntentTest.java
@@ -0,0 +1,85 @@
+package net.onrc.onos.api.newintent;
+
+import net.onrc.onos.core.matchaction.action.Action;
+import net.onrc.onos.core.matchaction.match.Match;
+import net.onrc.onos.core.matchaction.match.PacketMatch;
+import net.onrc.onos.core.util.SwitchPort;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.*;
+
+/**
+ * Base facilities to test various connectivity tests.
+ */
+public abstract class ConnectivityIntentTest {
+
+    public static final IntentId IID = new IntentId(123);
+    public static final Match MATCH = new PacketMatch();
+    public static final Action NOP = new NoAction();
+
+    public static final SwitchPort P1 = new SwitchPort(111, (short) 0x1);
+    public static final SwitchPort P2 = new SwitchPort(222, (short) 0x2);
+    public static final SwitchPort P3 = new SwitchPort(333, (short) 0x3);
+
+    public static final Set<SwitchPort> PS1 = itemSet(new SwitchPort[]{P1, P3});
+    public static final Set<SwitchPort> PS2 = itemSet(new SwitchPort[]{P2, P3});
+
+
+    @Test
+    public void equalsAndHashCode() {
+        Intent one = createOne();
+        Intent like = createOne();
+        Intent another = createAnother();
+
+        assertTrue("should be equal", one.equals(like));
+        assertEquals("incorrect hashCode", one.hashCode(), like.hashCode());
+
+        assertFalse("should not be equal", one.equals(another));
+
+        assertFalse("should not be equal", one.equals(null));
+        assertFalse("should not be equal", one.equals("foo"));
+    }
+
+    @Test
+    public void testToString() {
+        Intent one = createOne();
+        Intent like = createOne();
+        assertEquals("incorrect toString", one.toString(), like.toString());
+    }
+
+    /**
+     * Creates a new intent, but always a like intent, i.e. all instances will
+     * be equal, but should not be the same.
+     *
+     * @return intent
+     */
+    protected abstract Intent createOne();
+
+    /**
+     * Creates another intent, not equals to the one created by
+     * {@link #createOne()} and with a different hash code.
+     *
+     * @return another intent
+     */
+    protected abstract Intent createAnother();
+
+
+    /**
+     * Produces a set of items from the supplied items.
+     *
+     * @param items items to be placed in set
+     * @param <T>   item type
+     * @return set of items
+     */
+    private static <T> Set<T> itemSet(T[] items) {
+        return new HashSet<>(Arrays.asList(items));
+    }
+
+    // TODO: move to the match-action related package
+    private static class NoAction implements Action {
+    }
+}
diff --git a/src/test/java/net/onrc/onos/api/newintent/FakeIntentManager.java b/src/test/java/net/onrc/onos/api/newintent/FakeIntentManager.java
new file mode 100644
index 0000000..ebaf6af
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/newintent/FakeIntentManager.java
@@ -0,0 +1,271 @@
+package net.onrc.onos.api.newintent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * Fake implementation of the intent service to assist in developing tests
+ * of the interface contract.
+ */
+public class FakeIntentManager implements TestableIntentService {
+
+    private final Map<IntentId, Intent> intents = new HashMap<>();
+    private final Map<IntentId, IntentState> intentStates = new HashMap<>();
+    private final Map<IntentId, List<InstallableIntent>> installables = new HashMap<>();
+    private final Set<IntentEventListener> listeners = new HashSet<>();
+
+    private final Map<Class<? extends Intent>, IntentCompiler<? extends Intent>> compilers = new HashMap<>();
+    private final Map<Class<? extends InstallableIntent>,
+            IntentInstaller<? extends InstallableIntent>> installers = new HashMap<>();
+
+    private final ExecutorService executor = Executors.newSingleThreadExecutor();
+    private final List<IntentException> exceptions = new ArrayList<>();
+
+    @Override
+    public List<IntentException> getExceptions() {
+        return exceptions;
+    }
+
+    // Provides an out-of-thread simulation of intent submit life-cycle
+    private void executeSubmit(final Intent intent) {
+        registerSubclassCompilerIfNeeded(intent);
+        registerSubclassInstallerIfNeeded((InstallableIntent) intent);
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    List<InstallableIntent> installable = compileIntent(intent);
+                    installIntents(intent, installable);
+                } catch (IntentException e) {
+                    exceptions.add(e);
+                }
+            }
+        });
+    }
+
+    // Provides an out-of-thread simulation of intent withdraw life-cycle
+    private void executeWithdraw(final Intent intent) {
+        executor.execute(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    List<InstallableIntent> installable = getInstallable(intent.getId());
+                    uninstallIntents(intent, installable);
+                } catch (IntentException e) {
+                    exceptions.add(e);
+                }
+
+            }
+        });
+    }
+
+    private <T extends Intent> IntentCompiler<T> getCompiler(T intent) {
+        @SuppressWarnings("unchecked")
+        IntentCompiler<T> compiler = (IntentCompiler<T>) compilers.get(intent.getClass());
+        if (compiler == null) {
+            throw new IntentException("no compiler for class " + intent.getClass());
+        }
+        return compiler;
+    }
+
+    private <T extends InstallableIntent> IntentInstaller<T> getInstaller(T intent) {
+        @SuppressWarnings("unchecked")
+        IntentInstaller<T> installer = (IntentInstaller<T>) installers.get(intent.getClass());
+        if (installer == null) {
+            throw new IntentException("no installer for class " + intent.getClass());
+        }
+        return installer;
+    }
+
+    private <T extends Intent> List<InstallableIntent> compileIntent(T intent) {
+        try {
+            // For the fake, we compile using a single level pass
+            List<InstallableIntent> installable = new ArrayList<>();
+            for (Intent compiled : getCompiler(intent).compile(intent)) {
+                installable.add((InstallableIntent) compiled);
+            }
+            setState(intent, IntentState.COMPILED);
+            return installable;
+        } catch (IntentException e) {
+            setState(intent, IntentState.FAILED);
+            throw e;
+        }
+    }
+
+    private void installIntents(Intent intent, List<InstallableIntent> installable) {
+        try {
+            for (InstallableIntent ii : installable) {
+                getInstaller(ii).install(ii);
+            }
+            setState(intent, IntentState.INSTALLED);
+            putInstallable(intent.getId(), installable);
+        } catch (IntentException e) {
+            setState(intent, IntentState.FAILED);
+            throw e;
+        }
+    }
+
+    private void uninstallIntents(Intent intent, List<InstallableIntent> installable) {
+        try {
+            for (InstallableIntent ii : installable) {
+                getInstaller(ii).remove(ii);
+            }
+            setState(intent, IntentState.WITHDRAWN);
+            removeInstallable(intent.getId());
+        } catch (IntentException e) {
+            setState(intent, IntentState.FAILED);
+            throw e;
+        }
+    }
+
+
+    // Sets the internal state for the given intent and dispatches an event
+    private void setState(Intent intent, IntentState state) {
+        IntentState previous = intentStates.get(intent.getId());
+        intentStates.put(intent.getId(), state);
+        dispatch(new IntentEvent(intent, state, previous, System.currentTimeMillis()));
+    }
+
+    private void putInstallable(IntentId id, List<InstallableIntent> installable) {
+        installables.put(id, installable);
+    }
+
+    private void removeInstallable(IntentId id) {
+        installables.remove(id);
+    }
+
+    private List<InstallableIntent> getInstallable(IntentId id) {
+        List<InstallableIntent> installable = installables.get(id);
+        if (installable != null) {
+            return installable;
+        } else {
+            return Collections.emptyList();
+        }
+    }
+
+    @Override
+    public void submit(Intent intent) {
+        intents.put(intent.getId(), intent);
+        setState(intent, IntentState.SUBMITTED);
+        executeSubmit(intent);
+    }
+
+    @Override
+    public void withdraw(Intent intent) {
+        intents.remove(intent.getId());
+        setState(intent, IntentState.WITHDRAWING);
+        executeWithdraw(intent);
+    }
+
+    @Override
+    public void execute(IntentOperations operations) {
+        // TODO: implement later
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Set<Intent> getIntents() {
+        return Collections.unmodifiableSet(new HashSet<>(intents.values()));
+    }
+
+    @Override
+    public Intent getIntent(IntentId id) {
+        return intents.get(id);
+    }
+
+    @Override
+    public IntentState getIntentState(IntentId id) {
+        return intentStates.get(id);
+    }
+
+    @Override
+    public void addListener(IntentEventListener listener) {
+        listeners.add(listener);
+    }
+
+    @Override
+    public void removeListener(IntentEventListener listener) {
+        listeners.remove(listener);
+    }
+
+    private void dispatch(IntentEvent event) {
+        for (IntentEventListener listener : listeners) {
+            listener.event(event);
+        }
+    }
+
+    @Override
+    public <T extends Intent> void registerCompiler(Class<T> cls, IntentCompiler<T> compiler) {
+        compilers.put(cls, compiler);
+    }
+
+    @Override
+    public <T extends Intent> void unregisterCompiler(Class<T> cls) {
+        compilers.remove(cls);
+    }
+
+    @Override
+    public Map<Class<? extends Intent>, IntentCompiler<? extends Intent>> getCompilers() {
+        return Collections.unmodifiableMap(compilers);
+    }
+
+    @Override
+    public <T extends InstallableIntent> void registerInstaller(Class<T> cls, IntentInstaller<T> installer) {
+        installers.put(cls, installer);
+    }
+
+    @Override
+    public <T extends InstallableIntent> void unregisterInstaller(Class<T> cls) {
+        installers.remove(cls);
+    }
+
+    @Override
+    public Map<Class<? extends InstallableIntent>,
+            IntentInstaller<? extends InstallableIntent>> getInstallers() {
+        return Collections.unmodifiableMap(installers);
+    }
+
+    @SuppressWarnings("unchecked")
+    private void registerSubclassCompilerIfNeeded(Intent intent) {
+        if (!compilers.containsKey(intent.getClass())) {
+            Class<?> cls = intent.getClass();
+            while (cls != Object.class) {
+                // As long as we're within the Intent class descendants
+                if (Intent.class.isAssignableFrom(cls)) {
+                    IntentCompiler<?> compiler = compilers.get(cls);
+                    if (compiler != null) {
+                        compilers.put(intent.getClass(), compiler);
+                        return;
+                    }
+                }
+                cls = cls.getSuperclass();
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private void registerSubclassInstallerIfNeeded(InstallableIntent intent) {
+        if (!installers.containsKey(intent.getClass())) {
+            Class<?> cls = intent.getClass();
+            while (cls != Object.class) {
+                // As long as we're within the InstallableIntent class descendants
+                if (InstallableIntent.class.isAssignableFrom(cls)) {
+                    IntentInstaller<?> installer = installers.get(cls);
+                    if (installer != null) {
+                        installers.put(intent.getClass(), installer);
+                        return;
+                    }
+                }
+                cls = cls.getSuperclass();
+            }
+        }
+    }
+
+}
diff --git a/src/test/java/net/onrc/onos/api/newintent/IntentExceptionTest.java b/src/test/java/net/onrc/onos/api/newintent/IntentExceptionTest.java
new file mode 100644
index 0000000..574449b
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/newintent/IntentExceptionTest.java
@@ -0,0 +1,33 @@
+package net.onrc.onos.api.newintent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Test of the intent exception.
+ */
+public class IntentExceptionTest {
+
+    @Test
+    public void basics() {
+        validate(new IntentException(), null, null);
+        validate(new IntentException("foo"), "foo", null);
+
+        Throwable cause = new NullPointerException("bar");
+        validate(new IntentException("foo", cause), "foo", cause);
+    }
+
+    /**
+     * Validates that the specified exception has the correct message and cause.
+     *
+     * @param e       exception to test
+     * @param message expected message
+     * @param cause   expected cause
+     */
+    protected void validate(RuntimeException e, String message, Throwable cause) {
+        assertEquals("incorrect message", message, e.getMessage());
+        assertEquals("incorrect cause", cause, e.getCause());
+    }
+
+}
diff --git a/src/test/java/net/onrc/onos/api/newintent/IntentIdTest.java b/src/test/java/net/onrc/onos/api/newintent/IntentIdTest.java
new file mode 100644
index 0000000..01b65c0
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/newintent/IntentIdTest.java
@@ -0,0 +1,58 @@
+package net.onrc.onos.api.newintent;
+
+import org.junit.Test;
+
+import static net.onrc.onos.core.util.ImmutableClassChecker.assertThatClassIsImmutable;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * This class tests the immutability, equality, and non-equality of
+ * {@link net.onrc.onos.api.intent.IntentId}.
+ */
+public class IntentIdTest {
+    /**
+     * Tests the immutability of {@link net.onrc.onos.api.intent.IntentId}.
+     */
+    @Test
+    public void intentIdFollowsGuidelineForImmutableObject() {
+        assertThatClassIsImmutable(IntentId.class);
+    }
+
+    /**
+     * Tests equality of {@link net.onrc.onos.api.intent.IntentId}.
+     */
+    @Test
+    public void testEquality() {
+        IntentId id1 = new IntentId(1L);
+        IntentId id2 = new IntentId(1L);
+
+        assertThat(id1, is(id2));
+    }
+
+    /**
+     * Tests non-equality of {@link net.onrc.onos.api.intent.IntentId}.
+     */
+    @Test
+    public void testNonEquality() {
+        IntentId id1 = new IntentId(1L);
+        IntentId id2 = new IntentId(2L);
+
+        assertThat(id1, is(not(id2)));
+    }
+
+    @Test
+    public void valueOf() {
+        IntentId id = new IntentId(12345);
+        assertEquals("incorrect valueOf", id, IntentId.valueOf("12345"));
+    }
+
+    @Test
+    public void valueOfHex() {
+        IntentId id = new IntentId(0xdeadbeefL);
+        assertEquals("incorrect valueOf", id, IntentId.valueOf(id.toString()));
+    }
+
+}
diff --git a/src/test/java/net/onrc/onos/api/newintent/IntentServiceTest.java b/src/test/java/net/onrc/onos/api/newintent/IntentServiceTest.java
new file mode 100644
index 0000000..5fce2b6
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/newintent/IntentServiceTest.java
@@ -0,0 +1,313 @@
+package net.onrc.onos.api.newintent;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import static net.onrc.onos.api.newintent.IntentState.*;
+import static org.junit.Assert.*;
+
+// TODO: consider make it categorized as integration test when it become
+//   slow test or fragile test
+/**
+ * Suite of tests for the intent service contract.
+ */
+public class IntentServiceTest {
+
+    public static final IntentId IID = new IntentId(123);
+
+    protected static final int GRACE_MS = 500; // millis
+
+    protected TestableIntentService service;
+    protected TestListener listener = new TestListener();
+
+    @Before
+    public void setUp() {
+        service = createIntentService();
+        service.addListener(listener);
+    }
+
+    @After
+    public void tearDown() {
+        service.removeListener(listener);
+    }
+
+    /**
+     * Creates a service instance appropriately instrumented for testing.
+     *
+     * @return testable intent service
+     */
+    protected TestableIntentService createIntentService() {
+        return new FakeIntentManager();
+    }
+
+    @Test
+    public void basics() {
+        // Make sure there are no intents
+        assertEquals("incorrect intent count", 0, service.getIntents().size());
+
+        // Register a compiler and an installer both setup for success.
+        service.registerCompiler(TestIntent.class, new TestCompiler(false));
+        service.registerInstaller(TestIntent.class, new TestInstaller(false));
+
+        final Intent intent = new TestIntent(IID);
+        service.submit(intent);
+
+        // Allow a small window of time until the intent is in the expected state
+        TestTools.assertAfter(GRACE_MS, new Runnable() {
+            @Override
+            public void run() {
+                assertEquals("incorrect intent state", INSTALLED,
+                             service.getIntentState(intent.getId()));
+            }
+        });
+
+        // Make sure that all expected events have been emitted
+        validateEvents(intent, SUBMITTED, COMPILED, INSTALLED);
+
+        // Make sure there is just one intent (and is ours)
+        assertEquals("incorrect intent count", 1, service.getIntents().size());
+        assertEquals("incorrect intent", intent, service.getIntent(intent.getId()));
+
+        // Reset the listener events
+        listener.events.clear();
+
+        // Now withdraw the intent
+        service.withdraw(intent);
+
+        // Allow a small window of time until the event is in the expected state
+        TestTools.assertAfter(GRACE_MS, new Runnable() {
+            @Override
+            public void run() {
+                assertEquals("incorrect intent state", WITHDRAWN,
+                             service.getIntentState(intent.getId()));
+            }
+        });
+
+        // Make sure that all expected events have been emitted
+        validateEvents(intent, WITHDRAWING, WITHDRAWN);
+
+        // TODO: discuss what is the fate of intents after they have been withdrawn
+        // Make sure that the intent is no longer in the system
+//        assertEquals("incorrect intent count", 0, service.getIntents().size());
+//        assertNull("intent should not be found", service.getIntent(intent.getId()));
+//        assertNull("intent state should not be found", service.getIntentState(intent.getId()));
+    }
+
+    @Test
+    public void failedCompilation() {
+        // Register a compiler programmed for success
+        service.registerCompiler(TestIntent.class, new TestCompiler(true));
+
+        // Submit an intent
+        final Intent intent = new TestIntent(IID);
+        service.submit(intent);
+
+        // Allow a small window of time until the intent is in the expected state
+        TestTools.assertAfter(GRACE_MS, new Runnable() {
+            @Override
+            public void run() {
+                assertEquals("incorrect intent state", FAILED,
+                             service.getIntentState(intent.getId()));
+            }
+        });
+
+        // Make sure that all expected events have been emitted
+        validateEvents(intent, SUBMITTED, FAILED);
+    }
+
+    @Test
+    public void failedInstallation() {
+        // Register a compiler programmed for success and installer for failure
+        service.registerCompiler(TestIntent.class, new TestCompiler(false));
+        service.registerInstaller(TestIntent.class, new TestInstaller(true));
+
+        // Submit an intent
+        final Intent intent = new TestIntent(IID);
+        service.submit(intent);
+
+        // Allow a small window of time until the intent is in the expected state
+        TestTools.assertAfter(GRACE_MS, new Runnable() {
+            @Override
+            public void run() {
+                assertEquals("incorrect intent state", FAILED,
+                             service.getIntentState(intent.getId()));
+            }
+        });
+
+        // Make sure that all expected events have been emitted
+        validateEvents(intent, SUBMITTED, COMPILED, FAILED);
+    }
+
+    /**
+     * Validates that the test event listener has received the following events
+     * for the specified intent. Events received for other intents will not be
+     * considered.
+     *
+     * @param intent intent subject
+     * @param states list of states for which events are expected
+     */
+    protected void validateEvents(Intent intent, IntentState... states) {
+        Iterator<IntentEvent> events = listener.events.iterator();
+        for (IntentState state : states) {
+            IntentEvent event = events.hasNext() ? events.next() : null;
+            if (event == null) {
+                fail("expected event not found: " + state);
+            } else if (intent.equals(event.getIntent())) {
+                assertEquals("incorrect state", state, event.getState());
+            }
+        }
+
+        // Remainder of events should not apply to this intent; make sure.
+        while (events.hasNext()) {
+            assertFalse("unexpected event for intent",
+                        intent.equals(events.next().getIntent()));
+        }
+    }
+
+    @Test
+    public void compilerBasics() {
+        // Make sure there are no compilers
+        assertEquals("incorrect compiler count", 0, service.getCompilers().size());
+
+        // Add a compiler and make sure that it appears in the map
+        IntentCompiler<TestIntent> compiler = new TestCompiler(false);
+        service.registerCompiler(TestIntent.class, compiler);
+        assertEquals("incorrect compiler", compiler,
+                     service.getCompilers().get(TestIntent.class));
+
+        // Remove the same and make sure that it no longer appears in the map
+        service.unregisterCompiler(TestIntent.class);
+        assertNull("compiler should not be registered",
+                   service.getCompilers().get(TestIntent.class));
+    }
+
+    @Test
+    public void installerBasics() {
+        // Make sure there are no installers
+        assertEquals("incorrect installer count", 0, service.getInstallers().size());
+
+        // Add an installer and make sure that it appears in the map
+        IntentInstaller<TestIntent> installer = new TestInstaller(false);
+        service.registerInstaller(TestIntent.class, installer);
+        assertEquals("incorrect installer", installer,
+                     service.getInstallers().get(TestIntent.class));
+
+        // Remove the same and make sure that it no longer appears in the map
+        service.unregisterInstaller(TestIntent.class);
+        assertNull("installer should not be registered",
+                   service.getInstallers().get(TestIntent.class));
+    }
+
+    @Test
+    public void implicitRegistration() {
+        // Add a compiler and make sure that it appears in the map
+        IntentCompiler<TestIntent> compiler = new TestCompiler(false);
+        service.registerCompiler(TestIntent.class, compiler);
+        assertEquals("incorrect compiler", compiler,
+                     service.getCompilers().get(TestIntent.class));
+
+        // Add a installer and make sure that it appears in the map
+        IntentInstaller<TestIntent> installer = new TestInstaller(false);
+        service.registerInstaller(TestIntent.class, installer);
+        assertEquals("incorrect installer", installer,
+                     service.getInstallers().get(TestIntent.class));
+
+
+        // Submit an intent which is a subclass of the one we registered
+        final Intent intent = new TestSubclassIntent(IID);
+        service.submit(intent);
+
+        // Allow some time for the intent to be compiled and installed
+        TestTools.assertAfter(GRACE_MS, new Runnable() {
+            @Override
+            public void run() {
+                assertEquals("incorrect intent state", INSTALLED,
+                             service.getIntentState(intent.getId()));
+            }
+        });
+
+        // Make sure that now we have an implicit registration of the compiler
+        // under the intent subclass
+        assertEquals("incorrect compiler", compiler,
+                     service.getCompilers().get(TestSubclassIntent.class));
+
+        // Make sure that now we have an implicit registration of the installer
+        // under the intent subclass
+        assertEquals("incorrect installer", installer,
+                     service.getInstallers().get(TestSubclassIntent.class));
+
+        // TODO: discuss whether or if implicit registration should require implicit unregistration
+        // perhaps unregister by compiler or installer itself, rather than by class would be better
+    }
+
+
+    // Fixture to track emitted intent events
+    protected class TestListener implements IntentEventListener {
+        final List<IntentEvent> events = new ArrayList<>();
+
+        @Override
+        public void event(IntentEvent event) {
+            events.add(event);
+        }
+    }
+
+    private class TestIntent extends AbstractIntent implements InstallableIntent {
+        TestIntent(IntentId id) {
+            super(id);
+        }
+    }
+
+    private class TestSubclassIntent extends TestIntent {
+        TestSubclassIntent(IntentId id) {
+            super(id);
+        }
+    }
+
+    // Controllable compiler
+    private class TestCompiler implements IntentCompiler<TestIntent> {
+        private final boolean fail;
+
+        TestCompiler(boolean fail) {
+            this.fail = fail;
+        }
+
+        @Override
+        public List<Intent> compile(TestIntent intent) {
+            if (fail) {
+                throw new IntentException("compile failed by design");
+            }
+            List<Intent> compiled = new ArrayList<>(1);
+            compiled.add(intent);
+            return compiled;
+        }
+    }
+
+    // Controllable installer
+    private class TestInstaller implements IntentInstaller<TestIntent> {
+        private final boolean fail;
+
+        TestInstaller(boolean fail) {
+            this.fail = fail;
+        }
+
+        @Override
+        public void install(TestIntent intent) {
+            if (fail) {
+                throw new IntentException("install failed by design");
+            }
+        }
+
+        @Override
+        public void remove(TestIntent intent) {
+            if (fail) {
+                throw new IntentException("remove failed by design");
+            }
+        }
+    }
+
+}
diff --git a/src/test/java/net/onrc/onos/api/newintent/MultiPointToSinglePointIntentTest.java b/src/test/java/net/onrc/onos/api/newintent/MultiPointToSinglePointIntentTest.java
new file mode 100644
index 0000000..36a8aae
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/newintent/MultiPointToSinglePointIntentTest.java
@@ -0,0 +1,30 @@
+package net.onrc.onos.api.newintent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Suite of tests of the multi-to-single point intent descriptor.
+ */
+public class MultiPointToSinglePointIntentTest extends ConnectivityIntentTest {
+
+    @Test
+    public void basics() {
+        MultiPointToSinglePointIntent intent = createOne();
+        assertEquals("incorrect id", IID, intent.getId());
+        assertEquals("incorrect match", MATCH, intent.getMatch());
+        assertEquals("incorrect ingress", PS1, intent.getIngressPorts());
+        assertEquals("incorrect egress", P2, intent.getEgressPort());
+    }
+
+    @Override
+    protected MultiPointToSinglePointIntent createOne() {
+        return new MultiPointToSinglePointIntent(IID, MATCH, NOP, PS1, P2);
+    }
+
+    @Override
+    protected MultiPointToSinglePointIntent createAnother() {
+        return new MultiPointToSinglePointIntent(IID, MATCH, NOP, PS2, P1);
+    }
+}
diff --git a/src/test/java/net/onrc/onos/api/newintent/PathIntentTest.java b/src/test/java/net/onrc/onos/api/newintent/PathIntentTest.java
new file mode 100644
index 0000000..b68b506
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/newintent/PathIntentTest.java
@@ -0,0 +1,44 @@
+package net.onrc.onos.api.newintent;
+
+import net.onrc.onos.core.util.LinkTuple;
+import net.onrc.onos.core.util.SwitchPort;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public class PathIntentTest extends ConnectivityIntentTest {
+    public static final SwitchPort P1_2 = new SwitchPort(111, (short) 0x11);
+    public static final SwitchPort P2_2 = new SwitchPort(222, (short) 0x22);
+    public static final SwitchPort P3_2 = new SwitchPort(333, (short) 0x33);
+
+    private static final List<LinkTuple> PATH1 = Arrays.asList(
+            new LinkTuple(P1_2, P2_2)
+    );
+    private static final List<LinkTuple> PATH2 = Arrays.asList(
+            new LinkTuple(P1_2, P3_2)
+    );
+
+    @Test
+    public void basics() {
+        PathIntent intent = createOne();
+        assertEquals("incorrect id", IID, intent.getId());
+        assertEquals("incorrect match", MATCH, intent.getMatch());
+        assertEquals("incorrect action", NOP, intent.getAction());
+        assertEquals("incorrect ingress", P1, intent.getIngressPort());
+        assertEquals("incorrect egress", P2, intent.getEgressPort());
+        assertEquals("incorrect path", PATH1, intent.getPath());
+    }
+
+    @Override
+    protected PathIntent createOne() {
+        return new PathIntent(IID, MATCH, NOP, P1, P2, PATH1);
+    }
+
+    @Override
+    protected PathIntent createAnother() {
+        return new PathIntent(IID, MATCH, NOP, P1, P3, PATH2);
+    }
+}
diff --git a/src/test/java/net/onrc/onos/api/newintent/PointToPointIntentTest.java b/src/test/java/net/onrc/onos/api/newintent/PointToPointIntentTest.java
new file mode 100644
index 0000000..492b895
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/newintent/PointToPointIntentTest.java
@@ -0,0 +1,30 @@
+package net.onrc.onos.api.newintent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Suite of tests of the point-to-point intent descriptor.
+ */
+public class PointToPointIntentTest extends ConnectivityIntentTest {
+
+    @Test
+    public void basics() {
+        PointToPointIntent intent = createOne();
+        assertEquals("incorrect id", IID, intent.getId());
+        assertEquals("incorrect match", MATCH, intent.getMatch());
+        assertEquals("incorrect ingress", P1, intent.getIngressPort());
+        assertEquals("incorrect egress", P2, intent.getEgressPort());
+    }
+
+    @Override
+    protected PointToPointIntent createOne() {
+        return new PointToPointIntent(IID, MATCH, NOP, P1, P2);
+    }
+
+    @Override
+    protected PointToPointIntent createAnother() {
+        return new PointToPointIntent(IID, MATCH, NOP, P2, P1);
+    }
+}
diff --git a/src/test/java/net/onrc/onos/api/newintent/SinglePointToMultiPointIntentTest.java b/src/test/java/net/onrc/onos/api/newintent/SinglePointToMultiPointIntentTest.java
new file mode 100644
index 0000000..240ce47
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/newintent/SinglePointToMultiPointIntentTest.java
@@ -0,0 +1,30 @@
+package net.onrc.onos.api.newintent;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Suite of tests of the single-to-multi point intent descriptor.
+ */
+public class SinglePointToMultiPointIntentTest extends ConnectivityIntentTest {
+
+    @Test
+    public void basics() {
+        SinglePointToMultiPointIntent intent = createOne();
+        assertEquals("incorrect id", IID, intent.getId());
+        assertEquals("incorrect match", MATCH, intent.getMatch());
+        assertEquals("incorrect ingress", P1, intent.getIngressPort());
+        assertEquals("incorrect egress", PS2, intent.getEgressPorts());
+    }
+
+    @Override
+    protected SinglePointToMultiPointIntent createOne() {
+        return new SinglePointToMultiPointIntent(IID, MATCH, NOP, P1, PS2);
+    }
+
+    @Override
+    protected SinglePointToMultiPointIntent createAnother() {
+        return new SinglePointToMultiPointIntent(IID, MATCH, NOP, P2, PS1);
+    }
+}
diff --git a/src/test/java/net/onrc/onos/api/newintent/TestTools.java b/src/test/java/net/onrc/onos/api/newintent/TestTools.java
new file mode 100644
index 0000000..3e241f5
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/newintent/TestTools.java
@@ -0,0 +1,126 @@
+package net.onrc.onos.api.newintent;
+
+import static org.junit.Assert.fail;
+
+/**
+ * Set of test tools.
+ */
+public final class TestTools {
+
+    // Disallow construction
+    private TestTools() {
+    }
+
+    /**
+     * Utility method to pause the current thread for the specified number of
+     * milliseconds.
+     *
+     * @param ms number of milliseconds to pause
+     */
+    public static void delay(int ms) {
+        try {
+            Thread.sleep(ms);
+        } catch (InterruptedException e) {
+            fail("unexpected interrupt");
+        }
+    }
+
+    /**
+     * Periodically runs the given runnable, which should contain a series of
+     * test assertions until all the assertions succeed, in which case it will
+     * return, or until the the time expires, in which case it will throw the
+     * first failed assertion error.
+     *
+     * @param start start time, in millis since start of epoch from which the
+     *        duration will be measured
+     * @param delay initial delay (in milliseconds) before the first assertion
+     *        attempt
+     * @param step delay (in milliseconds) between successive assertion
+     *        attempts
+     * @param duration number of milliseconds beyond the given start time,
+     *        after which the failed assertions will be propagated and allowed
+     *        to fail the test
+     * @param assertions runnable housing the test assertions
+     */
+    public static void assertAfter(long start, int delay, int step,
+                                   int duration, Runnable assertions) {
+        delay(delay);
+        while (true) {
+            try {
+                assertions.run();
+                break;
+            } catch (AssertionError e) {
+                if (System.currentTimeMillis() - start > duration) {
+                    throw e;
+                }
+            }
+            delay(step);
+        }
+    }
+
+    /**
+     * Periodically runs the given runnable, which should contain a series of
+     * test assertions until all the assertions succeed, in which case it will
+     * return, or until the the time expires, in which case it will throw the
+     * first failed assertion error.
+     * <p>
+     * The start of the period is the current time.
+     *
+     * @param delay initial delay (in milliseconds) before the first assertion
+     *        attempt
+     * @param step delay (in milliseconds) between successive assertion
+     *        attempts
+     * @param duration number of milliseconds beyond the current time time,
+     *        after which the failed assertions will be propagated and allowed
+     *        to fail the test
+     * @param assertions runnable housing the test assertions
+     */
+    public static void assertAfter(int delay, int step, int duration,
+                                   Runnable assertions) {
+        assertAfter(System.currentTimeMillis(), delay, step, duration,
+                    assertions);
+    }
+
+    /**
+     * Periodically runs the given runnable, which should contain a series of
+     * test assertions until all the assertions succeed, in which case it will
+     * return, or until the the time expires, in which case it will throw the
+     * first failed assertion error.
+     * <p>
+     * The start of the period is the current time and the first assertion
+     * attempt is delayed by the value of {@code step} parameter.
+     *
+     * @param step delay (in milliseconds) between successive assertion
+     *        attempts
+     * @param duration number of milliseconds beyond the current time time,
+     *        after which the failed assertions will be propagated and allowed
+     *        to fail the test
+     * @param assertions runnable housing the test assertions
+     */
+    public static void assertAfter(int step, int duration,
+                                   Runnable assertions) {
+        assertAfter(step, step, duration, assertions);
+    }
+
+    /**
+     * Periodically runs the given runnable, which should contain a series of
+     * test assertions until all the assertions succeed, in which case it will
+     * return, or until the the time expires, in which case it will throw the
+     * first failed assertion error.
+     * <p>
+     * The start of the period is the current time and each successive
+     * assertion attempt is delayed by at least 10 milliseconds unless the
+     * {@code duration} is less than that, in which case the one and only
+     * assertion is made after that delay.
+     *
+     * @param duration number of milliseconds beyond the current time,
+     *        after which the failed assertions will be propagated and allowed
+     *        to fail the test
+     * @param assertions runnable housing the test assertions
+     */
+    public static void assertAfter(int duration, Runnable assertions) {
+        int step = Math.min(duration, Math.max(10, duration / 10));
+        assertAfter(step, duration, assertions);
+    }
+
+}
diff --git a/src/test/java/net/onrc/onos/api/newintent/TestableIntentService.java b/src/test/java/net/onrc/onos/api/newintent/TestableIntentService.java
new file mode 100644
index 0000000..27921de
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/newintent/TestableIntentService.java
@@ -0,0 +1,12 @@
+package net.onrc.onos.api.newintent;
+
+import java.util.List;
+
+/**
+ * Abstraction of an extensible intent service enabled for unit tests.
+ */
+public interface TestableIntentService extends IntentService, IntentExtensionService {
+
+    List<IntentException> getExceptions();
+
+}