CLI options: flows (remove), intents (remove/purge; details)

Change-Id: Id084c28451389a46826eced30f03dcce2c1afe86
diff --git a/cli/src/main/java/org/onosproject/cli/net/FlowsListCommand.java b/cli/src/main/java/org/onosproject/cli/net/FlowsListCommand.java
index bd7a246..7cbd586 100644
--- a/cli/src/main/java/org/onosproject/cli/net/FlowsListCommand.java
+++ b/cli/src/main/java/org/onosproject/cli/net/FlowsListCommand.java
@@ -24,7 +24,6 @@
 import org.apache.karaf.shell.commands.Option;
 import org.onlab.util.StringFilter;
 import org.onosproject.cli.AbstractShellCommand;
-import org.onosproject.utils.Comparators;
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.CoreService;
 import org.onosproject.net.Device;
@@ -34,7 +33,11 @@
 import org.onosproject.net.flow.FlowEntry.FlowEntryState;
 import org.onosproject.net.flow.FlowRuleService;
 import org.onosproject.net.flow.TrafficTreatment;
+import org.onosproject.utils.Comparators;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -93,10 +96,15 @@
     private boolean countOnly = false;
 
     @Option(name = "-f", aliases = "--filter",
-            description = "Filter flows by specific key",
+            description = "Filter flows by specific keyword",
             required = false, multiValued = true)
     private List<String> filter = new ArrayList<>();
 
+    @Option(name = "-r", aliases = "--remove",
+            description = "Remove flows by specific keyword",
+            required = false, multiValued = false)
+    private String remove = null;
+
     private Predicate<FlowEntry> predicate = TRUE_PREDICATE;
 
     private StringFilter contentFilter;
@@ -112,6 +120,22 @@
 
         SortedMap<Device, List<FlowEntry>> flows = getSortedFlows(deviceService, service, coreService);
 
+        // Remove flows
+        if (remove != null) {
+            flows.values().forEach(flowList -> {
+                if (!remove.isEmpty()) {
+                    filter.add(remove);
+                    contentFilter = new StringFilter(filter, StringFilter.Strategy.AND);
+                }
+                if (!filter.isEmpty() || (remove != null && !remove.isEmpty())) {
+                    flowList = filterFlows(flowList);
+                    this.removeFlowsInteractive(flowList, service, coreService);
+                }
+            });
+            return;
+        }
+
+        // Show flows
         if (outputJson()) {
             print("%s", json(flows.keySet(), flows));
         } else {
@@ -120,6 +144,36 @@
     }
 
     /**
+     * Removes the flows passed as argument after confirmation is provided
+     * for each of them.
+     * If no explicit confirmation is provided, the flow is not removed.
+     *
+     * @param flows       list of flows to remove
+     * @param flowService FlowRuleService object
+     * @param coreService CoreService object
+     */
+    public void removeFlowsInteractive(Iterable<FlowEntry> flows,
+                                       FlowRuleService flowService, CoreService coreService) {
+        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
+        flows.forEach(flow -> {
+            ApplicationId appId = coreService.getAppId(flow.appId());
+            System.out.print(String.format("Id=%s, AppId=%s. Remove? [y/N]: ",
+                                           flow.id(), appId != null ? appId.name() : "<none>"));
+            String response;
+            try {
+                response = br.readLine();
+                response = response.trim().replace("\n", "");
+                if (response.equals("y")) {
+                    flowService.removeFlowRules(flow);
+                }
+            } catch (IOException e) {
+                response = "";
+            }
+            print(response);
+        });
+    }
+
+    /**
      * Produces a JSON array of flows grouped by the each device.
      *
      * @param devices     collection of devices to group flow by
@@ -213,6 +267,17 @@
     }
 
     /**
+     * Filter a given list of flows based on the existing content filter.
+     *
+     * @param flows list of flows to filter
+     * @return further filtered list of flows
+     */
+    private List<FlowEntry> filterFlows(List<FlowEntry> flows) {
+        return flows.stream().
+                filter(f -> contentFilter.filter(f)).collect(Collectors.toList());
+    }
+
+    /**
      * Prints flows.
      *
      * @param d     the device
@@ -221,8 +286,7 @@
      */
     protected void printFlows(Device d, List<FlowEntry> flows,
                               CoreService coreService) {
-        List<FlowEntry> filteredFlows = flows.stream().
-                filter(f -> contentFilter.filter(f)).collect(Collectors.toList());
+        List<FlowEntry> filteredFlows = filterFlows(flows);
         boolean empty = filteredFlows == null || filteredFlows.isEmpty();
         print("deviceId=%s, flowRuleCount=%d", d.id(), empty ? 0 : filteredFlows.size());
         if (empty || countOnly) {
@@ -271,5 +335,4 @@
         builder.append("]");
         return builder.toString();
     }
-
 }
diff --git a/cli/src/main/java/org/onosproject/cli/net/IntentDetailsCommand.java b/cli/src/main/java/org/onosproject/cli/net/IntentDetailsCommand.java
index 1cd0a1b..8bc87fe 100644
--- a/cli/src/main/java/org/onosproject/cli/net/IntentDetailsCommand.java
+++ b/cli/src/main/java/org/onosproject/cli/net/IntentDetailsCommand.java
@@ -45,8 +45,17 @@
 
     @Override
     protected void execute() {
-        if (idsStr != null) {
-            ids = idsStr.stream()
+        detailIntents(idsStr);
+    }
+
+    /**
+     * Print detailed data for intents, given a list of IDs.
+     *
+     * @param intentsIds List of intent IDs
+     */
+    public void detailIntents(List<String> intentsIds) {
+        if (intentsIds != null) {
+            ids = intentsIds.stream()
                     .map(IntentId::valueOf)
                     .collect(Collectors.toSet());
         }
@@ -54,8 +63,8 @@
         IntentService service = get(IntentService.class);
 
         Tools.stream(service.getIntentData())
-            .filter(this::filter)
-            .forEach(this::printIntentData);
+                .filter(this::filter)
+                .forEach(this::printIntentData);
     }
 
     private boolean filter(IntentData data) {
diff --git a/cli/src/main/java/org/onosproject/cli/net/IntentRemoveCommand.java b/cli/src/main/java/org/onosproject/cli/net/IntentRemoveCommand.java
index ea2c67c..741de20 100644
--- a/cli/src/main/java/org/onosproject/cli/net/IntentRemoveCommand.java
+++ b/cli/src/main/java/org/onosproject/cli/net/IntentRemoveCommand.java
@@ -15,6 +15,7 @@
  */
 package org.onosproject.cli.net;
 
+import com.google.common.collect.ImmutableList;
 import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
 import org.apache.karaf.shell.commands.Option;
@@ -22,12 +23,15 @@
 import org.onosproject.core.ApplicationId;
 import org.onosproject.core.CoreService;
 import org.onosproject.net.intent.Intent;
-import org.onosproject.net.intent.IntentState;
-import org.onosproject.net.intent.IntentService;
-import org.onosproject.net.intent.IntentListener;
 import org.onosproject.net.intent.IntentEvent;
+import org.onosproject.net.intent.IntentListener;
+import org.onosproject.net.intent.IntentService;
+import org.onosproject.net.intent.IntentState;
 import org.onosproject.net.intent.Key;
 
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
 import java.math.BigInteger;
 import java.util.EnumSet;
 import java.util.Objects;
@@ -70,8 +74,72 @@
     @Override
     protected void execute() {
         IntentService intentService = get(IntentService.class);
-        CoreService coreService = get(CoreService.class);
+        removeIntent(intentService.getIntents(),
+             applicationIdString, keyString,
+             purgeAfterRemove, sync);
+    }
 
+    /**
+     * Purges the intents passed as argument.
+     *
+     * @param intents list of intents to purge
+     */
+    private void purgeIntents(Iterable<Intent> intents) {
+        IntentService intentService = get(IntentService.class);
+        this.purgeAfterRemove = true;
+        removeIntentsByAppId(intentService, intents, null);
+    }
+
+    /**
+     * Purges the intents passed as argument after confirmation is provided
+     * for each of them.
+     * If no explicit confirmation is provided, the intent is not purged.
+     *
+     * @param intents list of intents to purge
+     */
+    public void purgeIntentsInteractive(Iterable<Intent> intents) {
+        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
+        intents.forEach(intent -> {
+            System.out.print(String.format("Id=%s, Key=%s, AppId=%s. Remove? [y/N]: ",
+                                           intent.id(), intent.key(), intent.appId().name()));
+            String response;
+            try {
+                response = br.readLine();
+                response = response.trim().replace("\n", "");
+                if (response.equals("y")) {
+                    this.purgeIntents(ImmutableList.of(intent));
+                }
+            } catch (IOException e) {
+                response = "";
+            }
+            print(response);
+        });
+    }
+
+    /**
+     * Removes the intents passed as argument, assuming these
+     * belong to the application's ID provided (if any) and
+     * contain a key string.
+     *
+     * If an application ID is provided, it will be used to further
+     * filter the intents to be removed.
+     *
+     * @param intents list of intents to remove
+     * @param applicationIdString application ID to filter intents
+     * @param keyString string to filter intents
+     * @param purgeAfterRemove states whether the intents should be also purged
+     * @param sync states whether the cli should wait for the operation to finish
+     *             before returning
+     */
+    private void removeIntent(Iterable<Intent> intents,
+                             String applicationIdString, String keyString,
+                             boolean purgeAfterRemove, boolean sync) {
+        IntentService intentService = get(IntentService.class);
+        CoreService coreService = get(CoreService.class);
+        this.applicationIdString = applicationIdString;
+        this.keyString = keyString;
+        this.purgeAfterRemove = purgeAfterRemove;
+        this.sync = sync;
         if (purgeAfterRemove || sync) {
             print("Using \"sync\" to remove/purge intents - this may take a while...");
             print("Check \"summary\" to see remove/purge progress.");
@@ -87,11 +155,7 @@
         }
 
         if (isNullOrEmpty(keyString)) {
-            for (Intent intent : intentService.getIntents()) {
-                if (intent.appId().equals(appId)) {
-                    removeIntent(intentService, intent);
-                }
-            }
+            removeIntentsByAppId(intentService, intents, appId);
 
         } else {
             final Key key;
@@ -111,6 +175,32 @@
         }
     }
 
+    /**
+     * Removes the intents passed as argument.
+     *
+     * If an application ID is provided, it will be used to further
+     * filter the intents to be removed.
+     *
+     * @param intentService IntentService object
+     * @param intents intents to remove
+     * @param appId application ID to filter intents
+     */
+    private void removeIntentsByAppId(IntentService intentService,
+                                     Iterable<Intent> intents,
+                                     ApplicationId appId) {
+        for (Intent intent : intents) {
+            if (appId == null || intent.appId().equals(appId)) {
+                removeIntent(intentService, intent);
+            }
+        }
+    }
+
+    /**
+     * Removes the intent passed as argument.
+     *
+     * @param intentService IntentService object
+     * @param intent intent to remove
+     */
     private void removeIntent(IntentService intentService, Intent intent) {
         IntentListener listener = null;
         Key key = intent.key();
diff --git a/cli/src/main/java/org/onosproject/cli/net/IntentsListCommand.java b/cli/src/main/java/org/onosproject/cli/net/IntentsListCommand.java
index cb48396..e90041f 100644
--- a/cli/src/main/java/org/onosproject/cli/net/IntentsListCommand.java
+++ b/cli/src/main/java/org/onosproject/cli/net/IntentsListCommand.java
@@ -56,7 +56,6 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
 
 /**
  * Lists the inventory of intents and their states.
@@ -155,11 +154,21 @@
             required = false, multiValued = false)
     private boolean pending = false;
 
+    @Option(name = "-d", aliases = "--details",
+            description = "Show details for intents, filtered by ID",
+            required = false, multiValued = true)
+    private List<String> intentIds = new ArrayList<>();
+
     @Option(name = "-f", aliases = "--filter",
             description = "Filter intents by specific keyword",
             required = false, multiValued = true)
     private List<String> filter = new ArrayList<>();
 
+    @Option(name = "-r", aliases = "--remove",
+            description = "Remove and purge intents by specific keyword",
+            required = false, multiValued = false)
+    private String remove = null;
+
     private StringFilter contentFilter;
     private IntentService service;
 
@@ -175,6 +184,25 @@
             intents = service.getIntents();
         }
 
+        // Remove intents
+        if (remove != null && !remove.isEmpty()) {
+            filter.add(remove);
+            contentFilter = new StringFilter(filter, StringFilter.Strategy.AND);
+            IntentRemoveCommand intentRemoveCmd = new IntentRemoveCommand();
+            if (!remove.isEmpty()) {
+                intentRemoveCmd.purgeIntentsInteractive(filterIntents(service));
+            }
+            return;
+        }
+
+        // Show detailed intents
+        if (!intentIds.isEmpty()) {
+            IntentDetailsCommand intentDetailsCmd = new IntentDetailsCommand();
+            intentDetailsCmd.detailIntents(intentIds);
+            return;
+        }
+
+        // Show brief intents
         if (intentsSummary || miniSummary) {
             Map<String, IntentSummary> summarized = summarize(intents);
             if (outputJson()) {
@@ -197,6 +225,7 @@
             return;
         }
 
+        // JSON or default output
         if (outputJson()) {
             print("%s", json(intents));
         } else {
@@ -213,6 +242,27 @@
     }
 
     /**
+     * Filter a given list of intents based on the existing content filter.
+     *
+     * @param service IntentService object
+     * @return further filtered list of intents
+     */
+    private List<Intent> filterIntents(IntentService service) {
+        return filterIntents(service.getIntents());
+    }
+
+    /**
+     * Filter a given list of intents based on the existing content filter.
+     *
+     * @param intents Iterable of intents
+     * @return further filtered list of intents
+     */
+    private List<Intent> filterIntents(Iterable<Intent> intents) {
+        return Tools.stream(intents)
+                .filter(i -> contentFilter.filter(i)).collect(Collectors.toList());
+    }
+
+    /**
      * Internal local class to keep track of a single type Intent summary.
      */
     private class IntentSummary {
@@ -246,7 +296,9 @@
         IntentSummary(Intent intent) {
             // remove "Intent" from intentType label
             this(intentType(intent));
-            update(service.getIntentState(intent.key()));
+            if (contentFilter.filter(intent)) {
+                update(service.getIntentState(intent.key()));
+            }
         }
 
         // for identity element, when reducing
@@ -412,7 +464,7 @@
      */
     private Map<String, IntentSummary> summarize(Iterable<Intent> intents) {
         Map<String, List<Intent>> perIntent = Tools.stream(intents)
-            .collect(Collectors.groupingBy(i -> intentType(i)));
+            .collect(Collectors.groupingBy(IntentsListCommand::intentType));
 
         List<IntentSummary> collect = perIntent.values().stream()
             .map(il ->
@@ -571,10 +623,9 @@
     private JsonNode json(Iterable<Intent> intents) {
         ObjectMapper mapper = new ObjectMapper();
         ArrayNode result = mapper.createArrayNode();
-        StreamSupport.stream(intents.spliterator(), false)
+        Tools.stream(intents)
                 .filter(intent -> contentFilter.filter(jsonForEntity(intent, Intent.class).toString()))
                 .forEach(intent -> result.add(jsonForEntity(intent, Intent.class)));
         return result;
     }
-
 }