IntentManager refactoring for flow objectives.

Change-Id: I220682dbace03908b25ba86332664238dafaef90
diff --git a/core/net/src/main/java/org/onosproject/net/intent/impl/IntentInstaller.java b/core/net/src/main/java/org/onosproject/net/intent/impl/IntentInstaller.java
index 772ab7a..a612f21 100644
--- a/core/net/src/main/java/org/onosproject/net/intent/impl/IntentInstaller.java
+++ b/core/net/src/main/java/org/onosproject/net/intent/impl/IntentInstaller.java
@@ -16,23 +16,32 @@
 
 package org.onosproject.net.intent.impl;
 
+import com.google.common.collect.Sets;
+import org.onosproject.net.DeviceId;
 import org.onosproject.net.flow.FlowRule;
 import org.onosproject.net.flow.FlowRuleOperations;
 import org.onosproject.net.flow.FlowRuleOperationsContext;
 import org.onosproject.net.flow.FlowRuleService;
 import org.onosproject.net.flowobjective.FlowObjectiveService;
+import org.onosproject.net.flowobjective.Objective;
+import org.onosproject.net.flowobjective.ObjectiveContext;
+import org.onosproject.net.flowobjective.ObjectiveError;
+import org.onosproject.net.intent.FlowObjectiveIntent;
 import org.onosproject.net.intent.FlowRuleIntent;
 import org.onosproject.net.intent.Intent;
 import org.onosproject.net.intent.IntentData;
 import org.onosproject.net.intent.IntentStore;
 import org.slf4j.Logger;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
+import static com.google.common.base.Preconditions.checkState;
 import static org.onosproject.net.intent.IntentState.*;
 import static org.slf4j.LoggerFactory.getLogger;
 
@@ -69,51 +78,10 @@
         this.flowObjectiveService = flowObjectiveService;
     }
 
-    private void applyIntentData(Optional<IntentData> intentData,
-                                 FlowRuleOperations.Builder builder,
-                                 Direction direction) {
-        if (!intentData.isPresent()) {
-            return;
-        }
-        IntentData data = intentData.get();
-
-        List<Intent> intentsToApply = data.installables();
-        if (!intentsToApply.stream().allMatch(x -> x instanceof FlowRuleIntent)) {
-            throw new IllegalStateException("installable intents must be FlowRuleIntent");
-        }
-
-        if (direction == Direction.ADD) {
-            trackerService.addTrackedResources(data.key(), data.intent().resources());
-            intentsToApply.forEach(installable ->
-                                           trackerService.addTrackedResources(data.key(), installable.resources()));
-        } else {
-            trackerService.removeTrackedResources(data.key(), data.intent().resources());
-            intentsToApply.forEach(installable ->
-                                           trackerService.removeTrackedResources(data.intent().key(),
-                                                                                 installable.resources()));
-        }
-
-        // FIXME do FlowRuleIntents have stages??? Can we do uninstall work in parallel? I think so.
-        builder.newStage();
-
-        List<Collection<FlowRule>> stages = intentsToApply.stream()
-                .map(x -> (FlowRuleIntent) x)
-                .map(FlowRuleIntent::flowRules)
-                .collect(Collectors.toList());
-
-        for (Collection<FlowRule> rules : stages) {
-            if (direction == Direction.ADD) {
-                rules.forEach(builder::add);
-            } else {
-                rules.forEach(builder::remove);
-            }
-        }
-
-    }
 
     // FIXME: Refactor to accept both FlowObjectiveIntent and FlowRuleIntents
-    // Note: Intent Manager should have never become dependent on a specific
-    // intent type.
+    // FIXME: Intent Manager should have never become dependent on a specific intent type(s).
+    // This will be addressed in intent domains work; not now.
 
     /**
      * Applies the specified intent updates to the environment by uninstalling
@@ -123,67 +91,319 @@
      * @param toInstall   optional intent to install
      */
     void apply(Optional<IntentData> toUninstall, Optional<IntentData> toInstall) {
-        // need to consider if FlowRuleIntent is only one as installable intent or not
-
-        FlowRuleOperations.Builder builder = FlowRuleOperations.builder();
-        applyIntentData(toUninstall, builder, Direction.REMOVE);
-        applyIntentData(toInstall, builder, Direction.ADD);
-
-        FlowRuleOperations operations = builder.build(new FlowRuleOperationsContext() {
-            @Override
-            public void onSuccess(FlowRuleOperations ops) {
-                if (toInstall.isPresent()) {
-                    IntentData installData = toInstall.get();
-                    log.debug("Completed installing: {}", installData.key());
-                    installData.setState(INSTALLED);
-                    store.write(installData);
-                } else if (toUninstall.isPresent()) {
-                    IntentData uninstallData = toUninstall.get();
-                    log.debug("Completed withdrawing: {}", uninstallData.key());
-                    switch (uninstallData.request()) {
-                        case INSTALL_REQ:
-                            uninstallData.setState(FAILED);
-                            break;
-                        case WITHDRAW_REQ:
-                        default: //TODO "default" case should not happen
-                            uninstallData.setState(WITHDRAWN);
-                            break;
-                    }
-                    store.write(uninstallData);
+        // Hook for handling success
+        Consumer<OperationContext> successConsumer = (ctx) -> {
+            if (toInstall.isPresent()) {
+                IntentData installData = toInstall.get();
+                log.debug("Completed installing: {}", installData.key());
+                installData.setState(INSTALLED);
+                store.write(installData);
+            } else if (toUninstall.isPresent()) {
+                IntentData uninstallData = toUninstall.get();
+                log.debug("Completed withdrawing: {}", uninstallData.key());
+                switch (uninstallData.request()) {
+                    case INSTALL_REQ:
+                        uninstallData.setState(FAILED);
+                        break;
+                    case WITHDRAW_REQ:
+                    default: //TODO "default" case should not happen
+                        uninstallData.setState(WITHDRAWN);
+                        break;
                 }
+                store.write(uninstallData);
             }
+        };
 
-            @Override
-            public void onError(FlowRuleOperations ops) {
-                // if toInstall was cause of error, then recompile (manage/increment counter, when exceeded -> CORRUPT)
-                if (toInstall.isPresent()) {
-                    IntentData installData = toInstall.get();
-                    log.warn("Failed installation: {} {} on {}",
-                             installData.key(), installData.intent(), ops);
-                    installData.setState(CORRUPT);
-                    installData.incrementErrorCount();
-                    store.write(installData);
-                }
-                // if toUninstall was cause of error, then CORRUPT (another job will clean this up)
-                if (toUninstall.isPresent()) {
-                    IntentData uninstallData = toUninstall.get();
-                    log.warn("Failed withdrawal: {} {} on {}",
-                             uninstallData.key(), uninstallData.intent(), ops);
-                    uninstallData.setState(CORRUPT);
-                    uninstallData.incrementErrorCount();
-                    store.write(uninstallData);
-                }
+        // Hook for handling errors
+        Consumer<OperationContext> errorConsumer = (ctx) -> {
+            // if toInstall was cause of error, then recompile (manage/increment counter, when exceeded -> CORRUPT)
+            if (toInstall.isPresent()) {
+                IntentData installData = toInstall.get();
+                log.warn("Failed installation: {} {} on {}",
+                         installData.key(), installData.intent(), ctx.error());
+                installData.setState(CORRUPT);
+                installData.incrementErrorCount();
+                store.write(installData);
             }
-        });
+            // if toUninstall was cause of error, then CORRUPT (another job will clean this up)
+            if (toUninstall.isPresent()) {
+                IntentData uninstallData = toUninstall.get();
+                log.warn("Failed withdrawal: {} {} on {}",
+                         uninstallData.key(), uninstallData.intent(), ctx.error());
+                uninstallData.setState(CORRUPT);
+                uninstallData.incrementErrorCount();
+                store.write(uninstallData);
+            }
+        };
 
-        if (log.isTraceEnabled()) {
-            log.trace("applying intent {} -> {} with {} rules: {}",
-                      toUninstall.map(x -> x.key().toString()).orElse("<empty>"),
-                      toInstall.map(x -> x.key().toString()).orElse("<empty>"),
-                      operations.stages().stream().mapToLong(Set::size).sum(),
-                      operations.stages());
+        // Create a context for tracking the backing operations for applying
+        // the intents to the environment.
+        OperationContext context = createContext(toUninstall, toInstall);
+
+        context.prepare(toUninstall, toInstall, successConsumer, errorConsumer);
+        context.apply();
+    }
+
+    // ------ Utilities to support FlowRule vs. FlowObjective behavior -------
+
+    // Creates the context appropriate for tracking operations of the
+    // the specified intents.
+    private OperationContext createContext(Optional<IntentData> toUninstall,
+                                           Optional<IntentData> toInstall) {
+        if (isInstallable(toUninstall, toInstall, FlowRuleIntent.class)) {
+            return new FlowRuleOperationContext();
+        }
+        if (isInstallable(toUninstall, toInstall, FlowObjectiveIntent.class)) {
+            return new FlowObjectiveOperationContext();
+        }
+        return new ErrorContext();
+    }
+
+    private boolean isInstallable(Optional<IntentData> toUninstall, Optional<IntentData> toInstall,
+                                  Class<? extends Intent> intentClass) {
+        boolean notBothNull = false;
+        if (toInstall.isPresent()) {
+            notBothNull = true;
+            if (!toInstall.get().installables().stream()
+                    .allMatch(i -> intentClass.isAssignableFrom(i.getClass()))) {
+                return false;
+            }
+        }
+        if (toUninstall.isPresent()) {
+            notBothNull = true;
+            if (!toUninstall.get().installables().stream()
+                    .allMatch(i -> intentClass.isAssignableFrom(i.getClass()))) {
+                return false;
+            }
+        }
+        return notBothNull;
+    }
+
+    // Base context for applying and tracking operations related to installable intents.
+    private abstract class OperationContext {
+        protected Optional<IntentData> toUninstall;
+        protected Optional<IntentData> toInstall;
+        protected Consumer<OperationContext> successConsumer;
+        protected Consumer<OperationContext> errorConsumer;
+
+        abstract void apply();
+
+        abstract Object error();
+
+        abstract void prepareIntents(List<Intent> intentsToApply, Direction direction);
+
+        void prepare(Optional<IntentData> toUninstall, Optional<IntentData> toInstall,
+                     Consumer<OperationContext> successConsumer,
+                     Consumer<OperationContext> errorConsumer) {
+            this.toUninstall = toUninstall;
+            this.toInstall = toInstall;
+            this.successConsumer = successConsumer;
+            this.errorConsumer = errorConsumer;
+            prepareIntentData(toUninstall, Direction.REMOVE);
+            prepareIntentData(toInstall, Direction.ADD);
         }
 
-        flowRuleService.apply(operations);
+        /**
+         * Applies the specified intent data, if present, to the network using the
+         * specified context.
+         *
+         * @param intentData optional intent data; no-op if not present
+         * @param direction  indicates adding or removal
+         */
+        private void prepareIntentData(Optional<IntentData> intentData, Direction direction) {
+            if (!intentData.isPresent()) {
+                return;
+            }
+
+            IntentData data = intentData.get();
+            List<Intent> intentsToApply = data.installables();
+            checkState(intentsToApply.stream().allMatch(this::isSupported),
+                       "Unsupported installable intents detected");
+
+            if (direction == Direction.ADD) {
+                trackerService.addTrackedResources(data.key(), data.intent().resources());
+                intentsToApply.forEach(installable ->
+                                               trackerService.addTrackedResources(data.key(),
+                                                                                  installable.resources()));
+            } else {
+                trackerService.removeTrackedResources(data.key(), data.intent().resources());
+                intentsToApply.forEach(installable ->
+                                               trackerService.removeTrackedResources(data.intent().key(),
+                                                                                     installable.resources()));
+            }
+
+            prepareIntents(intentsToApply, direction);
+        }
+
+        private boolean isSupported(Intent intent) {
+            return intent instanceof FlowRuleIntent || intent instanceof FlowObjectiveIntent;
+        }
+    }
+
+
+    // Context for applying and tracking operations related to flow rule intent.
+    private class FlowRuleOperationContext extends OperationContext {
+        FlowRuleOperations.Builder builder = FlowRuleOperations.builder();
+        FlowRuleOperationsContext flowRuleOperationsContext;
+
+        void apply() {
+            flowRuleOperationsContext = new FlowRuleOperationsContext() {
+                @Override
+                public void onSuccess(FlowRuleOperations ops) {
+                    successConsumer.accept(FlowRuleOperationContext.this);
+                }
+
+                @Override
+                public void onError(FlowRuleOperations ops) {
+                    errorConsumer.accept(FlowRuleOperationContext.this);
+                }
+            };
+            FlowRuleOperations operations = builder.build(flowRuleOperationsContext);
+
+            if (log.isTraceEnabled()) {
+                log.trace("applying intent {} -> {} with {} rules: {}",
+                          toUninstall.map(x -> x.key().toString()).orElse("<empty>"),
+                          toInstall.map(x -> x.key().toString()).orElse("<empty>"),
+                          operations.stages().stream().mapToLong(Set::size).sum(),
+                          operations.stages());
+            }
+
+            flowRuleService.apply(operations);
+        }
+
+        @Override
+        public void prepareIntents(List<Intent> intentsToApply, Direction direction) {
+            // FIXME do FlowRuleIntents have stages??? Can we do uninstall work in parallel? I think so.
+            builder.newStage();
+
+            List<Collection<FlowRule>> stages = intentsToApply.stream()
+                    .map(x -> (FlowRuleIntent) x)
+                    .map(FlowRuleIntent::flowRules)
+                    .collect(Collectors.toList());
+
+            for (Collection<FlowRule> rules : stages) {
+                if (direction == Direction.ADD) {
+                    rules.forEach(builder::add);
+                } else {
+                    rules.forEach(builder::remove);
+                }
+            }
+
+        }
+
+        @Override
+        public Object error() {
+            return flowRuleOperationsContext;
+        }
+    }
+
+    // Context for applying and tracking operations related to flow objective intents.
+    private class FlowObjectiveOperationContext extends OperationContext {
+        List<FlowObjectiveInstallationContext> contexts;
+        final Set<ObjectiveContext> pendingContexts = Sets.newHashSet();
+        final Set<ObjectiveContext> errorContexts = Sets.newConcurrentHashSet();
+
+        @Override
+        public void prepareIntents(List<Intent> intentsToApply, Direction direction) {
+            contexts = intentsToApply.stream()
+                    .flatMap(x -> buildObjectiveContexts((FlowObjectiveIntent) x, direction).stream())
+                    .collect(Collectors.toList());
+        }
+
+        // Builds the specified objective in the appropriate direction
+        private List<FlowObjectiveInstallationContext> buildObjectiveContexts(FlowObjectiveIntent intent,
+                                                                              Direction direction) {
+            int size = intent.objectives().size();
+            List<FlowObjectiveInstallationContext> contexts = new ArrayList<>(size);
+            for (int i = 0; i < size; i++) {
+                DeviceId deviceId = intent.devices().get(i);
+                Objective.Builder builder = intent.objectives().get(i).copy();
+                FlowObjectiveInstallationContext context = new FlowObjectiveInstallationContext();
+
+                final Objective objective;
+                switch (direction) {
+                    case ADD:
+                        objective = builder.add(context);
+                        break;
+                    case REMOVE:
+                        objective = builder.remove(context);
+                        break;
+                    default:
+                        throw new UnsupportedOperationException("Unsupported direction " + direction);
+                }
+                context.setObjective(objective, deviceId);
+                contexts.add(context);
+            }
+            return contexts;
+        }
+
+        @Override
+        void apply() {
+            contexts.forEach(objectiveContext -> {
+                pendingContexts.add(objectiveContext);
+                flowObjectiveService.apply(objectiveContext.deviceId,
+                                           objectiveContext.objective);
+            });
+        }
+
+        @Override
+        public Object error() {
+            return errorContexts;
+        }
+
+        private class FlowObjectiveInstallationContext implements ObjectiveContext {
+            Objective objective;
+            DeviceId deviceId;
+
+            void setObjective(Objective objective, DeviceId deviceId) {
+                this.objective = objective;
+                this.deviceId = deviceId;
+            }
+
+            @Override
+            public void onSuccess(Objective objective) {
+                finish();
+            }
+
+            @Override
+            public void onError(Objective objective, ObjectiveError error) {
+                errorContexts.add(this);
+                finish();
+            }
+
+            private void finish() {
+                synchronized (pendingContexts) {
+                    pendingContexts.remove(this);
+                    if (pendingContexts.isEmpty()) {
+                        if (errorContexts.isEmpty()) {
+                            successConsumer.accept(FlowObjectiveOperationContext.this);
+                        } else {
+                            errorConsumer.accept(FlowObjectiveOperationContext.this);
+                        }
+                    }
+                }
+            }
+
+            @Override
+            public String toString() {
+                return String.format("(%s, %s)", deviceId, objective);
+            }
+        }
+    }
+
+    private class ErrorContext extends OperationContext {
+        @Override
+        void apply() {
+            throw new UnsupportedOperationException("Unsupported installable intent");
+        }
+
+        @Override
+        Object error() {
+            return null;
+        }
+
+        @Override
+        void prepareIntents(List<Intent> intentsToApply, Direction direction) {
+        }
     }
 }