Add intent error handling features.

- added ErrorIntent class to express intent processing errors
- added ERROR to IntentOperation.Operator enum
- made IntentMap to change intents' state automatically when it processes ErrorIntent instances
- made PathCalcRuntime to create ErrorIntent instance for failed intents
- made PathCalcRuntimeModule to process ErrorIntent and reflect them to high-level intents' states

Change-Id: Ib0cf9c284a37812695864d75d3cde2499a76e2a7
diff --git a/src/main/java/net/onrc/onos/intent/ErrorIntent.java b/src/main/java/net/onrc/onos/intent/ErrorIntent.java
new file mode 100644
index 0000000..3365350
--- /dev/null
+++ b/src/main/java/net/onrc/onos/intent/ErrorIntent.java
@@ -0,0 +1,30 @@
+package net.onrc.onos.intent;
+
+/**
+ * This class is instantiated by Run-times to express intent calculation error
+ * @author Toshio Koide (t-koide@onlab.us)
+ */
+public class ErrorIntent extends Intent {
+	public enum ErrorType {
+		UNSUPPORTED_INTENT,
+		SWITCH_NOT_FOUND,
+		PATH_NOT_FOUND,
+	}
+
+	public ErrorType errorType;
+	public String message;
+	public Intent parentIntent;
+
+	/**
+	 * Default constructor for Kryo deserialization
+	 */
+	protected ErrorIntent() {
+	}
+
+	public ErrorIntent(ErrorType errorType, String message, Intent parentIntent) {
+		this.id = parentIntent.getId();
+		this.errorType = errorType;
+		this.message = message;
+		this.parentIntent = parentIntent;
+	}
+}
diff --git a/src/main/java/net/onrc/onos/intent/IntentMap.java b/src/main/java/net/onrc/onos/intent/IntentMap.java
index 4657706..53667ad 100644
--- a/src/main/java/net/onrc/onos/intent/IntentMap.java
+++ b/src/main/java/net/onrc/onos/intent/IntentMap.java
@@ -5,6 +5,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
+import java.util.Map;
 import java.util.Map.Entry;
 
 import net.onrc.onos.intent.Intent.IntentState;
@@ -13,6 +14,10 @@
  * @author Toshio Koide (t-koide@onlab.us)
  */
 public class IntentMap {
+	private HashSet<ChangedListener> listeners = new HashSet<>();
+	private HashMap<String, Intent> intents = new HashMap<>();
+	private	LinkedList<ChangedEvent> events = new LinkedList<>();
+
 	public enum ChangedEventType {
 		/**
 		 * Added new intent.
@@ -47,47 +52,25 @@
 		void intentsChange(LinkedList<ChangedEvent> events);
 	}
 
-	private HashSet<ChangedListener> listeners = new HashSet<>();
-	private HashMap<String, Intent> intents = new HashMap<>();
-
-	protected void putIntent(Intent intent) {
-		if (intents.containsKey(intent.getId()))
-			removeIntent(intent.getId());
-		intents.put(intent.getId(), intent);
-	}
-
-	protected void removeIntent(String intentId) {
-		intents.remove(intentId);		
-	}
-
-	public Intent getIntent(String intentId) {
-		return intents.get(intentId);
-	}
+	//================================================================================
+	// public methods
+	//================================================================================
 
 	public void executeOperations(IntentOperationList operations) {
-		LinkedList<ChangedEvent> events = new LinkedList<>();
 		for (IntentOperation operation: operations) {
 			switch (operation.operator) {
 			case ADD:
-				putIntent(operation.intent);
-				events.add(new ChangedEvent(ChangedEventType.ADDED, operation.intent));
+				handleAddOperation(operation);
 				break;
 			case REMOVE:
-				Intent intent = getIntent(operation.intent.getId());
-				if (intent == null) {
-					// TODO throw exception
-				}
-				else {
-					intent.setState(Intent.IntentState.DEL_REQ);
-					events.add(new ChangedEvent(ChangedEventType.STATE_CHANGED,
-							new Intent(intent.getId(), Intent.IntentState.DEL_REQ)));
-				}
+				handleRemoveOperation(operation);
+				break;
+			case ERROR:
+				handleErrorOperation(operation);
 				break;
 			}
 		}
-		for (ChangedListener listener: listeners) {
-			listener.intentsChange(events);
-		}
+		notifyEvents();
 	}
 
 	public void purge() {
@@ -97,11 +80,23 @@
 			if (intent.getState() == IntentState.DEL_ACK
 					|| intent.getState() == IntentState.INST_NACK) {
 				removeIds.add(intent.getId());
-			}			
+			}
 		}
 		for (String intentId: removeIds) {
 			removeIntent(intentId);
 		}
+		notifyEvents();
+	}
+
+	public void changeStates(Map<String, IntentState> states) {
+		for (Entry<String, IntentState> state: states.entrySet()) {
+			setState(state.getKey(), state.getValue());
+		}
+		notifyEvents();
+	}
+
+	public Intent getIntent(String intentId) {
+		return intents.get(intentId);
 	}
 
 	public Collection<Intent> getAllIntents() {
@@ -115,4 +110,80 @@
 	public void removeChangedListener(ChangedListener listener) {
 		listeners.remove(listener);
 	}
+
+	//================================================================================
+	// methods that affect intents map (protected)
+	//================================================================================
+
+	protected void putIntent(Intent intent) {
+		if (intents.containsKey(intent.getId()))
+			removeIntent(intent.getId());
+		intents.put(intent.getId(), intent);
+		events.add(new ChangedEvent(ChangedEventType.ADDED, intent));
+	}
+
+	protected void removeIntent(String intentId) {
+		Intent intent = intents.remove(intentId);
+		if (intent == null) return;
+		events.add(new ChangedEvent(ChangedEventType.REMOVED, intent));
+	}
+
+	protected void setState(String intentId, IntentState state) {
+		Intent intent = intents.get(intentId);
+		if (intent == null) return;
+		intent.setState(state);
+		events.add(new ChangedEvent(ChangedEventType.STATE_CHANGED, intent));
+	}
+
+	//================================================================================
+	// helper methods (protected)
+	//================================================================================
+
+	protected void handleAddOperation(IntentOperation operation) {
+		putIntent(operation.intent);
+	}
+
+	protected void handleRemoveOperation(IntentOperation operation) {
+		Intent intent = getIntent(operation.intent.getId());
+		if (intent == null) {
+			// TODO error handling
+		}
+		else {
+			setState(intent.getId(), IntentState.DEL_REQ);
+		}
+	}
+
+	protected void handleErrorOperation(IntentOperation operation) {
+		//TODO put error message into the intent
+
+		ErrorIntent errorIntent = (ErrorIntent) operation.intent;
+		Intent targetIntent = intents.get(errorIntent.getId());
+		if (targetIntent == null) {
+			// TODO error handling
+			return;
+		}
+
+		switch (targetIntent.getState()) {
+		case CREATED:
+		case INST_REQ:
+		case INST_ACK:
+			setState(targetIntent.getId(), IntentState.INST_NACK);
+			break;
+		case DEL_REQ:
+			setState(targetIntent.getId(), IntentState.DEL_PENDING);
+			break;
+		case INST_NACK:
+		case DEL_PENDING:
+		case DEL_ACK:
+			// do nothing
+			break;
+		}
+	}
+
+	protected void notifyEvents() {
+		for (ChangedListener listener: listeners) {
+			listener.intentsChange(events);
+		}
+		events.clear();
+	}
 }
diff --git a/src/main/java/net/onrc/onos/intent/IntentOperation.java b/src/main/java/net/onrc/onos/intent/IntentOperation.java
index 506dd52..93a9f88 100644
--- a/src/main/java/net/onrc/onos/intent/IntentOperation.java
+++ b/src/main/java/net/onrc/onos/intent/IntentOperation.java
@@ -6,15 +6,21 @@
 public class IntentOperation {
 	public enum Operator {
 		/**
-		 * Add new intent specified by intent field
+		 * Add new intent specified by intent field.
 		 */
 		ADD,
 
 		/**
 		 * Remove existing intent specified by intent field.
-		 * The specified intent should be an instance of Intent class (not a child class)
+		 * The instance of intent field should be an instance of Intent class (not a child class)
 		 */
 		REMOVE,
+
+		/**
+		 * Do error handling.
+		 * The instance of intent field should be an instance of ErrorIntent
+		 */
+		ERROR,
 	}
 
 	public IntentOperation() {}
diff --git a/src/main/java/net/onrc/onos/intent/IntentOperationList.java b/src/main/java/net/onrc/onos/intent/IntentOperationList.java
index 05e5b5c..881120a 100644
--- a/src/main/java/net/onrc/onos/intent/IntentOperationList.java
+++ b/src/main/java/net/onrc/onos/intent/IntentOperationList.java
@@ -1,11 +1,11 @@
 package net.onrc.onos.intent;
 
-import java.util.ArrayList;
+import java.util.LinkedList;
 
 /**
  * @author Toshio Koide (t-koide@onlab.us)
  */
-public class IntentOperationList extends ArrayList<IntentOperation> {
+public class IntentOperationList extends LinkedList<IntentOperation> {
 	private static final long serialVersionUID = -3894081461861052610L;
 
 	public boolean add(IntentOperation.Operator op, Intent intent) {
diff --git a/src/main/java/net/onrc/onos/intent/runtime/PathCalcRuntime.java b/src/main/java/net/onrc/onos/intent/runtime/PathCalcRuntime.java
index 5600e0c..201e97d 100644
--- a/src/main/java/net/onrc/onos/intent/runtime/PathCalcRuntime.java
+++ b/src/main/java/net/onrc/onos/intent/runtime/PathCalcRuntime.java
@@ -5,6 +5,8 @@
 import net.floodlightcontroller.core.module.IFloodlightService;
 import net.onrc.onos.intent.ConstrainedBFSTree;
 import net.onrc.onos.intent.ConstrainedShortestPathIntent;
+import net.onrc.onos.intent.ErrorIntent;
+import net.onrc.onos.intent.ErrorIntent.ErrorType;
 import net.onrc.onos.intent.Intent;
 import net.onrc.onos.intent.IntentOperation;
 import net.onrc.onos.intent.IntentOperation.Operator;
@@ -33,9 +35,9 @@
 	 * calculate shortest-path and constrained-shortest-path intents into low-level path intents
 	 * @param intentOpList IntentOperationList having instances of ShortestPathIntent/ConstrainedShortestPathIntent
 	 * @param pathIntents a set of current low-level intents
-	 * @return IntentOperationList for PathIntent instances
+	 * @return IntentOperationList. PathIntent and/or ErrorIntent instances.
 	 */
-	public IntentOperationList calcPathIntents(IntentOperationList intentOpList, PathIntentMap pathIntents) {
+	public IntentOperationList calcPathIntents(final IntentOperationList intentOpList, final PathIntentMap pathIntents) {
 		IntentOperationList pathIntentOpList = new IntentOperationList();
 		HashMap<Switch, ConstrainedBFSTree> spfTrees = new HashMap<>();
 
@@ -43,8 +45,11 @@
 			switch (intentOp.operator) {
 			case ADD:
 				if (!(intentOp.intent instanceof ShortestPathIntent)) {
-					log.error("unsupported intent type: {}", intentOp.intent.getClass().getName());
-					// TODO should push back the intent to caller
+					log.error("Unsupported intent type: {}", intentOp.intent.getClass().getName());
+					pathIntentOpList.add(Operator.ERROR, new ErrorIntent(
+							ErrorType.UNSUPPORTED_INTENT,
+							"Unsupported intent type.",
+							intentOp.intent));
 					continue;
 				}
 
@@ -55,7 +60,10 @@
 					log.error("Switch not found: {}, {}",
 							spIntent.getSrcSwitchDpid(),
 							spIntent.getDstSwitchDpid());
-					// TODO should push back the intent to caller
+					pathIntentOpList.add(Operator.ERROR, new ErrorIntent(
+							ErrorType.SWITCH_NOT_FOUND,
+							"Switch not found.",
+							intentOp.intent));
 					continue;
 				}
 
@@ -75,15 +83,21 @@
 				Path path = tree.getPath(dstSwitch);
 				if (path == null) {
 					log.error("Path not found: {}", intentOp.intent.toString());
-					// TODO should push back the intent to caller
+					pathIntentOpList.add(Operator.ERROR, new ErrorIntent(
+							ErrorType.PATH_NOT_FOUND,
+							"Path not found.",
+							intentOp.intent));
 					continue;
 				}
 				PathIntent pathIntent = new PathIntent("pi" + intentOp.intent.getId(), path, bandwidth, intentOp.intent);
-				pathIntentOpList.add(new IntentOperation(IntentOperation.Operator.ADD, pathIntent));
+				pathIntentOpList.add(Operator.ADD, pathIntent);
 				break;
 			case REMOVE:
 				pathIntentOpList.add(Operator.REMOVE, new Intent("pi" + intentOp.intent.getId()));
 				break;
+			case ERROR:
+				// just ignore
+				break;
 			}
 		}
 		return pathIntentOpList;
diff --git a/src/main/java/net/onrc/onos/intent/runtime/PathCalcRuntimeModule.java b/src/main/java/net/onrc/onos/intent/runtime/PathCalcRuntimeModule.java
index 3a9c607..df927f6 100755
--- a/src/main/java/net/onrc/onos/intent/runtime/PathCalcRuntimeModule.java
+++ b/src/main/java/net/onrc/onos/intent/runtime/PathCalcRuntimeModule.java
@@ -3,6 +3,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
 
 import net.floodlightcontroller.core.module.FloodlightModuleContext;
@@ -12,18 +13,20 @@
 import net.onrc.onos.datagrid.IDatagridService;
 import net.onrc.onos.datagrid.IEventChannel;
 import net.onrc.onos.intent.Intent;
+import net.onrc.onos.intent.Intent.IntentState;
 import net.onrc.onos.intent.IntentMap;
+import net.onrc.onos.intent.IntentOperation;
 import net.onrc.onos.intent.IntentOperation.Operator;
 import net.onrc.onos.intent.IntentOperationList;
 import net.onrc.onos.intent.PathIntent;
 import net.onrc.onos.intent.PathIntentMap;
+import net.onrc.onos.intent.persist.PersistIntent;
 import net.onrc.onos.ofcontroller.networkgraph.DeviceEvent;
 import net.onrc.onos.ofcontroller.networkgraph.INetworkGraphListener;
 import net.onrc.onos.ofcontroller.networkgraph.INetworkGraphService;
 import net.onrc.onos.ofcontroller.networkgraph.LinkEvent;
 import net.onrc.onos.ofcontroller.networkgraph.PortEvent;
 import net.onrc.onos.ofcontroller.networkgraph.SwitchEvent;
-import net.onrc.onos.intent.persist.PersistIntent;
 import net.onrc.onos.registry.controller.IControllerRegistryService;
 
 /**
@@ -93,18 +96,42 @@
 				IntentOperationList.class);
 		networkGraphService.registerNetworkGraphListener(this);
                 persistIntent = new PersistIntent(controllerRegistry, networkGraphService);
-                
+
 	}
 
 	@Override
 	public IntentOperationList executeIntentOperations(IntentOperationList list) {
+		// update the map of high-level intents
 		highLevelIntents.executeOperations(list);
+
+		// prepare high-level intents' state changes
+		HashMap<String, IntentState> states = new HashMap<>();
+		for (IntentOperation op: list) {
+			String id = op.intent.getId();
+			states.put(id, IntentState.INST_REQ);
+		}
+
+		// calculate path-intents (low-level operations)
 		IntentOperationList pathIntentOperations = runtime.calcPathIntents(list, pathIntents);
+
+		// persist calculated low-level operations
 		long key = persistIntent.getKey();
-		System.out.println(pathIntentOperations);
+		persistIntent.persistIfLeader(key, pathIntentOperations);
+
+		// remove error-intents and reflect them to high-level intents
+		Iterator<IntentOperation> i = pathIntentOperations.iterator();
+		while (i.hasNext()) {
+			IntentOperation op = i.next();
+			if (op.operator.equals(Operator.ERROR)) {
+				states.put(op.intent.getId(), IntentState.INST_NACK);
+				i.remove();
+			}
+		}
+		highLevelIntents.changeStates(states);
+
+		// update the map of low-level intents and publish the low-level operations
 		pathIntents.executeOperations(pathIntentOperations);
 		eventChannel.addEntry(key, pathIntentOperations);
-                persistIntent.persistIfLeader(key, pathIntentOperations);
 		return pathIntentOperations;
 	}
 
diff --git a/src/main/java/net/onrc/onos/ofcontroller/util/serializers/KryoFactory.java b/src/main/java/net/onrc/onos/ofcontroller/util/serializers/KryoFactory.java
index f65a830..07b173d 100644
--- a/src/main/java/net/onrc/onos/ofcontroller/util/serializers/KryoFactory.java
+++ b/src/main/java/net/onrc/onos/ofcontroller/util/serializers/KryoFactory.java
@@ -6,6 +6,7 @@
 
 import net.floodlightcontroller.util.MACAddress;
 import net.onrc.onos.intent.ConstrainedShortestPathIntent;
+import net.onrc.onos.intent.ErrorIntent;
 import net.onrc.onos.intent.Intent;
 import net.onrc.onos.intent.IntentOperation;
 import net.onrc.onos.intent.IntentOperationList;
@@ -179,6 +180,7 @@
 	kryo.register(IntentOperation.class);
 	kryo.register(PathIntent.class);
 	kryo.register(Intent.class);
+	kryo.register(ErrorIntent.class);
 	kryo.register(ShortestPathIntent.class);
 	kryo.register(ConstrainedShortestPathIntent.class);
 	kryo.register(Intent.IntentState.class);