ONOS-5726: augmented implementation of "showIntent" overlay support.
- added acceptIntent() callback hook, to allow overlays to declare which intent types they can display.

Change-Id: I18d0b6f05b0a348623bd5a90d58d996d389bdd95
diff --git a/core/api/src/main/java/org/onosproject/ui/topo/AbstractTopoMonitor.java b/core/api/src/main/java/org/onosproject/ui/topo/AbstractTopoMonitor.java
new file mode 100644
index 0000000..e6b7cfa
--- /dev/null
+++ b/core/api/src/main/java/org/onosproject/ui/topo/AbstractTopoMonitor.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017-present Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.onosproject.ui.topo;
+
+/**
+ * Base class for the business logic of topology overlay "monitors".
+ */
+public class AbstractTopoMonitor {
+
+    // TODO: pull common code up into this class
+
+    // Note to Andrea:
+    //  this class has to be defined in the core.api module, because
+    //  external applications may want to extend it.
+}
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java
index b2ba156..e97b7dd 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/TrafficMonitor.java
@@ -44,6 +44,7 @@
 import org.onosproject.ui.impl.topo.util.TrafficLink;
 import org.onosproject.ui.impl.topo.util.TrafficLink.StatsType;
 import org.onosproject.ui.impl.topo.util.TrafficLinkMap;
+import org.onosproject.ui.topo.AbstractTopoMonitor;
 import org.onosproject.ui.topo.DeviceHighlight;
 import org.onosproject.ui.topo.Highlights;
 import org.onosproject.ui.topo.Highlights.Amount;
@@ -74,7 +75,7 @@
 /**
  * Encapsulates the behavior of monitoring specific traffic patterns.
  */
-public class TrafficMonitor {
+public class TrafficMonitor extends AbstractTopoMonitor {
 
     // 4 Kilo Bytes as threshold
     private static final double BPS_THRESHOLD = 4 * TopoUtils.KILO;
diff --git a/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/TopoIntentFilter.java b/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/TopoIntentFilter.java
index 54a43d4..e43fbf2 100644
--- a/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/TopoIntentFilter.java
+++ b/web/gui/src/main/java/org/onosproject/ui/impl/topo/util/TopoIntentFilter.java
@@ -252,7 +252,7 @@
         return false;
     }
 
-    // Indicates whether the specified flow rules involvesthe given device.
+    // Indicates whether the specified flow rules involves the given device.
     private boolean rulesContainDevice(Collection<FlowRule> flowRules, DeviceId id) {
         for (FlowRule rule : flowRules) {
             if (rule.deviceId().equals(id)) {
diff --git a/web/gui/src/main/webapp/app/view/intent/intent.html b/web/gui/src/main/webapp/app/view/intent/intent.html
index 95fe5ca..d5f52d6 100644
--- a/web/gui/src/main/webapp/app/view/intent/intent.html
+++ b/web/gui/src/main/webapp/app/view/intent/intent.html
@@ -28,7 +28,7 @@
             <div class="separator"></div>
 
             <div class="show-intent-btn">
-                <div ng-class="{active: !!selId}"
+                <div ng-class="{active: canShowIntent()}"
                      icon icon-id="topo" icon-size="42"
                      tooltip tt-msg="topoTip"
                      ng-click="showIntent()">
diff --git a/web/gui/src/main/webapp/app/view/intent/intent.js b/web/gui/src/main/webapp/app/view/intent/intent.js
index a2375ba..9567119 100644
--- a/web/gui/src/main/webapp/app/view/intent/intent.js
+++ b/web/gui/src/main/webapp/app/view/intent/intent.js
@@ -21,52 +21,154 @@
 (function () {
     'use strict';
 
+    // constants and configuration
     var dialogId = 'remove-intent-dialog',
         dialogOpts = {
             edge: 'right'
-        },
-        dropdown;
+        };
+
+    // DOM elements
+    var dropdown;
+
+    // injected refs
+    var $log, $scope, ns, tov, tts, ds;
+
+
+    function initScope() {
+        $scope.topoTip = 'Show selected intent on topology view';
+        $scope.resubmitTip = 'Resubmit selected intent';
+        $scope.deactivateTip = 'Remove selected intent';
+        $scope.purgeTip = 'Purge selected intent';
+        $scope.purgeAllTip = 'Purge withdrawn intents';
+
+        $scope.briefTip = 'Switch to brief view';
+        $scope.detailTip = 'Switch to detailed view';
+
+        $scope.brief = true;
+        $scope.intentState = 'NA';
+        $scope.fired = false;
+    }
+
+    // === row selection and response callback functions:
+
+    function selCb($event, row) {
+        $log.debug('Got a click on:', row);
+        var m = /(\d+)\s:\s(.*)/.exec(row.appId),
+            id = m ? m[1] : null,
+            name = m ? m[2] : null;
+
+        $scope.intentData = ($scope.selId && m) ? {
+                appId: id,
+                appName: name,
+                key: row.key,
+                intentType: row.type
+            } : null;
+
+        $scope.intentState = row.state;
+        showDropdown(false);
+    }
+
+    function respCb() {
+        if ($scope.fired) {
+            if ($scope.changedData) {
+                $scope.intentState = $scope.changedData.state;
+            }
+            $scope.fired = false;
+        }
+    }
+
+
+    // === show-intent functions
+
+    function showDropdown(b) {
+        dropdown.style('display', b ? 'block' : 'none');
+    }
+
+    function showIntent () {
+        var d = $scope.intentData,
+            handlers,
+            nh;
+
+        if (!d) {
+            // no intent selected - nothing to do
+            return;
+        }
+
+        function setOvAndNavigate(info) {
+            d.overlayId = info.id;
+            ns.navTo('topo', d);
+        }
+
+        function clickMe(data) {
+            showDropdown(false);
+            setOvAndNavigate(data);
+        }
+
+        function setUpSelection(handlers) {
+            dropdown.text(null);
+
+            handlers.forEach(function (data) {
+                var div = dropdown.append('div');
+                div.classed('overlay-choice', true);
+                div.text(data.tt);
+                div.on('click', function () {
+                    clickMe(data);
+                });
+            });
+
+            showDropdown(true);
+        }
+
+        handlers = tov.overlaysAcceptingIntents(d.intentType);
+        nh = handlers.length;
+
+        if (nh === 1) {
+            setOvAndNavigate(handlers[0]);
+
+        } else if (nh > 1) {
+            // let the user choose which overlay to invoke...
+            setUpSelection(handlers);
+
+        } else {
+            $log.warn('Sorry - no overlay configured to show',
+                      'intents of type', d.intentType);
+        }
+    }
+
+
+    // === intent action functionality
+
+    // TODO: refactor to move functions below to here...
+
+
+    // === intent view controller
 
     angular.module('ovIntent', [])
         .controller('OvIntentCtrl',
         ['$log', '$scope', 'TableBuilderService', 'NavService',
             'TopoOverlayService', 'TopoTrafficService', 'DialogService',
 
-        function ($log, $scope, tbs, ns, tov, tts, ds) {
-            $scope.briefTip = 'Switch to brief view';
-            $scope.detailTip = 'Switch to detailed view';
-            $scope.brief = true;
-            $scope.intentState = 'NA';
-            $scope.fired = false;
-            $scope.showOverlays = false;
+        function (_$log_, _$scope_, tbs, _ns_, _tov_, _tts_, _ds_) {
+            $log = _$log_;
+            $scope = _$scope_;
+            ns = _ns_;
+            tov = _tov_;
+            tts = _tts_;
+            ds = _ds_;
+
+            initScope();
 
             dropdown = d3.select('div.show-intent-btn .dropdown');
 
-            function selCb($event, row) {
-                $log.debug('Got a click on:', row);
-                var m = /(\d+)\s:\s(.*)/.exec(row.appId),
-                    id = m ? m[1] : null,
-                    name = m ? m[2] : null;
+            // set up scope function references...
+            $scope.showIntent = showIntent;
 
-                $scope.intentData = ($scope.selId && m) ? {
-                    appId: id,
-                    appName: name,
-                    key: row.key
-                } : null;
+            $scope.canShowIntent = function() {
+                var d = $scope.intentData;
+                return d && tov.overlaysAcceptingIntents(d.intentType).length > 0;
+            };
 
-                $scope.intentState = row.state;
-                showDropdown(false);
-            }
-
-            function respCb() {
-                if ($scope.fired) {
-                    if ($scope.changedData) {
-                        $scope.intentState = $scope.changedData.state;
-                    }
-                    $scope.fired = false;
-                }
-            }
-
+            // build the table
             tbs.buildTable({
                 scope: $scope,
                 tag: 'intent',
@@ -75,70 +177,6 @@
                 idKey: 'key'
             });
 
-            $scope.topoTip = 'Show selected intent on topology view';
-            $scope.resubmitTip = 'Resubmit selected intent';
-            $scope.deactivateTip = 'Remove selected intent';
-            $scope.purgeTip = 'Purge selected intent';
-            $scope.purgeAllTip = 'Purge withdrawn intents';
-
-
-            function showDropdown(b) {
-                dropdown.style('display', b ? 'block' : 'none');
-            }
-
-            $scope.showIntent = function () {
-                var d = $scope.intentData,
-                    tovData,
-                    ncb;
-
-                if (!d) {
-                    // no intent selected - nothing to do
-                    return;
-                }
-
-                function setOvAndNavigate(info) {
-                    d.overlayId = info.id;
-                    ns.navTo('topo', d);
-                }
-
-                function clickMe(data) {
-                    showDropdown(false);
-                    setOvAndNavigate(data);
-                }
-
-                function setUpSelection(tovData) {
-                    dropdown.text(null);
-
-                    tovData.forEach(function (data) {
-                        var div = dropdown.append('div');
-                        div.classed('overlay-choice', true);
-                        div.text(data.tt);
-                        div.on('click', function () {
-                            clickMe(data);
-                        });
-                    });
-
-                    showDropdown(true);
-                }
-
-                tovData = tov.listOverlaysThatShowIntents();
-                ncb = tovData.length;
-                // NOTE: ncb should be at least 1, (traffic overlay)
-
-                if (ncb === 1) {
-                    setOvAndNavigate(tovData[0]);
-
-                } else if (ncb > 1) {
-                    // let the user choose which overlay to invoke...
-                    setUpSelection(tovData);
-
-                } else {
-                    $log.error('Internal Error - no overlay configured',
-                    'to show selected intent on topology view');
-                }
-            };
-
-
 
             // TODO: clean up the following code...
 
@@ -191,6 +229,7 @@
                     .addCancel(dCancel)
                     .bindKeys();
             }
+
             function executeActions(action) {
                  var content = ds.createDiv(),
                      txt='purgeIntents';
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.js b/web/gui/src/main/webapp/app/view/topo/topo.js
index 1762cf4..0623478 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.js
+++ b/web/gui/src/main/webapp/app/view/topo/topo.js
@@ -603,11 +603,13 @@
                 setMap: setMap
             });
 
+            // pull intent data from the query string...
             if (params.key && params.appId && params.appName) {
                 $scope.intentData = {
                     key: params.key,
                     appId: params.appId,
-                    appName: params.appName
+                    appName: params.appName,
+                    intentType: params.intentType
                 };
             }
 
diff --git a/web/gui/src/main/webapp/app/view/topo/topoOverlay.js b/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
index ad42470..d51d8fc 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoOverlay.js
@@ -113,18 +113,20 @@
         return d3.map(overlays).keys();
     }
 
-    // returns data on overlays that implement the showIntent callback
-    function listShowIntents() {
+    // Returns an array containing overlays that implement the showIntent and
+    // acceptIntent callbacks, and that accept the given intent type
+    function overlaysAcceptingIntents(intentType) {
         var result = [];
         angular.forEach(overlays, function (ov) {
-            var hooks = fs.isO(ov.hooks) || {},
-                sicb = fs.isF(hooks.showintent);
+            var ovid = ov.overlayId,
+                hooks = fs.isO(ov.hooks) || {},
+                aicb = fs.isF(hooks.acceptIntent),
+                sicb = fs.isF(hooks.showIntent);
 
-            if (sicb) {
+            if (sicb && aicb && aicb(intentType)) {
                 result.push({
-                    id: ov.overlayId,
-                    tt: ov.tooltip || '%' + ov.overlayId + '%',
-                    gid: ov._glyphId
+                    id: ovid,
+                    tt: ov.tooltip || '%' + ovid + '%'
                 });
             }
         });
@@ -304,7 +306,7 @@
 
     // Request from Intent View to visualize an intent on the topo view
     function showIntentHook(intentData) {
-        var cb = _hook('showintent');
+        var cb = _hook('showIntent');
         return cb && cb(intentData);
     }
 
@@ -446,7 +448,7 @@
                 register: register,
                 setApi: setApi,
                 list: list,
-                listOverlaysThatShowIntents: listShowIntents,
+                overlaysAcceptingIntents: overlaysAcceptingIntents,
                 augmentRbset: augmentRbset,
                 mkGlyphId: mkGlyphId,
                 tbSelection: tbSelection,
diff --git a/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js b/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js
index 312b652..ac8f4a2 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoTrafficNew.js
@@ -143,8 +143,12 @@
                 tts.requestTrafficForMode(true);
             },
 
-            // intent visualization hook
-            showintent: function (info) {
+            // intent visualization hooks
+            acceptIntent: function (type) {
+                // accept any intent type except "Protected" intents
+                return (!type.startsWith('Protected'));
+            },
+            showIntent: function (info) {
                 $log.debug('^^ trafficOverlay.showintent() ^^', info);
                 tts.selectIntent(info);
             }