[ONOS-7981] GUI2 Topo View Link handling fails WIP
on device name with multiple ':'

Change-Id: I698ff2e9a38d3ee45ce1ffa163137c84c12439f3
(cherry picked from commit b77768e06e72dda3c84f917ca39b7d114f6d84bf)
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.spec.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.spec.ts
index fb8f42c..d10df1c9 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.spec.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.spec.ts
@@ -26,7 +26,6 @@
 import {DraggableDirective} from './draggable/draggable.directive';
 import {ActivatedRoute, Params} from '@angular/router';
 import {of} from 'rxjs';
-import {MapSvgComponent} from '../mapsvg/mapsvg.component';
 import {DeviceNodeSvgComponent} from './visuals/devicenodesvg/devicenodesvg.component';
 import {SubRegionNodeSvgComponent} from './visuals/subregionnodesvg/subregionnodesvg.component';
 import {HostNodeSvgComponent} from './visuals/hostnodesvg/hostnodesvg.component';
@@ -96,8 +95,12 @@
     let logServiceSpy: jasmine.SpyObj<LogService>;
     let component: ForceSvgComponent;
     let fixture: ComponentFixture<ForceSvgComponent>;
-    const sampledata = require('./tests/test-module-topo2CurrentRegion.json');
-    const regionData: Region = <Region><unknown>(sampledata.payload);
+    const openflowSampleData = require('./tests/test-module-topo2CurrentRegion.json');
+    const openflowRegionData: Region = <Region><unknown>(openflowSampleData.payload);
+
+    const odtnSampleData = require('./tests/test-OdtnConfig-topo2CurrentRegion.json');
+    const odtnRegionData: Region = <Region><unknown>(odtnSampleData.payload);
+
     const emptyRegion: Region = <Region>{devices: [ [], [], [] ], hosts: [ [], [], [] ], links: []};
 
     beforeEach(() => {
@@ -169,26 +172,30 @@
         expect(component).toBeTruthy();
     });
 
-    it('load sample file', () => {
-        expect(sampledata).toBeTruthy();
-        expect(sampledata.payload).toBeTruthy();
-        expect(sampledata.payload.id).toBe('(root)');
+    it('load sample files', () => {
+        expect(openflowSampleData).toBeTruthy();
+        expect(openflowSampleData.payload).toBeTruthy();
+        expect(openflowSampleData.payload.id).toBe('(root)');
+
+        expect(odtnSampleData).toBeTruthy();
+        expect(odtnSampleData.payload).toBeTruthy();
+        expect(odtnSampleData.payload.id).toBe('(root)');
     });
 
     it('should read sample data payload as Region', () => {
-        expect(regionData).toBeTruthy();
+        expect(openflowRegionData).toBeTruthy();
         // console.log(regionData);
-        expect(regionData.id).toBe('(root)');
-        expect(regionData.devices).toBeTruthy();
-        expect(regionData.devices.length).toBe(3);
-        expect(regionData.devices[2].length).toBe(10);
-        expect(regionData.hosts.length).toBe(3);
-        expect(regionData.hosts[2].length).toBe(20);
-        expect(regionData.links.length).toBe(44);
+        expect(openflowRegionData.id).toBe('(root)');
+        expect(openflowRegionData.devices).toBeTruthy();
+        expect(openflowRegionData.devices.length).toBe(3);
+        expect(openflowRegionData.devices[2].length).toBe(10);
+        expect(openflowRegionData.hosts.length).toBe(3);
+        expect(openflowRegionData.hosts[2].length).toBe(20);
+        expect(openflowRegionData.links.length).toBe(44);
     });
 
     it('should read device246 correctly', () => {
-        const device246: Device = regionData.devices[2][0];
+        const device246: Device = openflowRegionData.devices[2][0];
         expect(device246.id).toBe('of:0000000000000246');
         expect(device246.nodeType).toBe('device');
         expect(device246.type).toBe('switch');
@@ -211,7 +218,7 @@
     });
 
     it('should read host 3 correctly', () => {
-        const host3: Host = regionData.hosts[2][0];
+        const host3: Host = openflowRegionData.hosts[2][0];
         expect(host3.id).toBe('00:88:00:00:00:03/110');
         expect(host3.nodeType).toBe('host');
         expect(host3.layer).toBe('def');
@@ -223,7 +230,7 @@
     });
 
     it('should read link 3-205 correctly', () => {
-        const link3_205: Link = regionData.links[0];
+        const link3_205: Link = openflowRegionData.links[0];
         expect(link3_205.id).toBe('00:AA:00:00:00:03/None~of:0000000000000205/6');
         expect(link3_205.epA).toBe('00:AA:00:00:00:03/None');
         expect(link3_205.epB).toBe('of:0000000000000205');
@@ -249,25 +256,41 @@
         expect(component.graph.nodes.length).toBe(0);
     });
 
-    it('should know hwo to format names', () => {
-        expect(ForceSvgComponent.extractNodeName('00:AA:00:00:00:03/None'))
+    it('should know how to format names', () => {
+        expect(ForceSvgComponent.extractNodeName('00:AA:00:00:00:03/None', undefined))
             .toEqual('00:AA:00:00:00:03/None');
 
-        expect(ForceSvgComponent.extractNodeName('00:AA:00:00:00:03/161'))
-            .toEqual('00:AA:00:00:00:03/161');
+        expect(ForceSvgComponent.extractNodeName('00:AA:00:00:00:03/161', '161'))
+            .toEqual('00:AA:00:00:00:03');
 
-        expect(ForceSvgComponent.extractNodeName('of:0000000000000206/6'))
+        // Like epB of first example in sampleData file - endPtStr contains port number
+        expect(ForceSvgComponent.extractNodeName('of:0000000000000206/6', '6'))
+            .toEqual('of:0000000000000206');
+
+        // Like epB of second example in sampleData file - endPtStr does not contain port number
+        expect(ForceSvgComponent.extractNodeName('of:0000000000000206', '6'))
             .toEqual('of:0000000000000206');
     });
 
-    it('should handle regionData change - sample Region', () => {
-        component.regionData = regionData;
+    it('should handle openflow regionData change - sample Region', () => {
+        component.regionData = openflowRegionData;
         component.ngOnChanges(
-            {'regionData' : new SimpleChange(<Region>{}, regionData, true)});
+            {'regionData' : new SimpleChange(<Region>{}, openflowRegionData, true)});
 
         expect(component.graph.nodes.length).toBe(30);
 
         expect(component.graph.links.length).toBe(44);
 
     });
+
+    it('should handle odtn regionData change - sample odtn Region', () => {
+        component.regionData = odtnRegionData;
+        component.ngOnChanges(
+            {'regionData' : new SimpleChange(<Region>{}, odtnRegionData, true)});
+
+        expect(component.graph.nodes.length).toBe(2);
+
+        expect(component.graph.links.length).toBe(6);
+
+    });
 });
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
index d6a9148..adc814e 100644
--- a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/forcesvg.component.ts
@@ -119,21 +119,13 @@
      * name
      * @param endPtStr The end point name
      */
-    static extractNodeName(endPtStr: string): string {
-        const slash: number = endPtStr.indexOf('/');
-        if (slash === -1) {
+    static extractNodeName(endPtStr: string, portStr: string): string {
+        if (portStr === undefined || endPtStr === undefined) {
             return endPtStr;
-        } else {
-            const afterSlash = endPtStr.substr(slash + 1);
-            const beforeSlash = endPtStr.substr(0, slash);
-            if (afterSlash === 'None') {
-                return endPtStr;
-            } else if (beforeSlash.split(':').length > 2) {
-                return endPtStr; // Host name with mac address
-            } else {
-                return endPtStr.substr(0, slash);
-            }
+        } else if (endPtStr.includes('/')) {
+            return endPtStr.substr(0, endPtStr.length - portStr.length - 1);
         }
+        return endPtStr;
     }
 
     /**
@@ -236,13 +228,21 @@
             // Associate the endpoints of each link with a real node
             this.graph.links = [];
             for (const linkIdx of Object.keys(this.regionData.links)) {
-                const epA = ForceSvgComponent.extractNodeName(
-                                        this.regionData.links[linkIdx].epA);
+                const link = this.regionData.links[linkIdx];
+                const epA = ForceSvgComponent.extractNodeName(link.epA, link.portA);
+                if (!this.graph.nodes.find((node) => node.id === epA)) {
+                    this.log.error('ngOnChange Could not find endpoint A', epA, 'for', link);
+                    continue;
+                }
+                const epB = ForceSvgComponent.extractNodeName(
+                    link.epB, link.portB);
+                if (!this.graph.nodes.find((node) => node.id === epB)) {
+                    this.log.error('ngOnChange Could not find endpoint B', epB, 'for', link);
+                    continue;
+                }
                 this.regionData.links[linkIdx].source =
                     this.graph.nodes.find((node) =>
                         node.id === epA);
-                const epB = ForceSvgComponent.extractNodeName(
-                    this.regionData.links[linkIdx].epB);
                 this.regionData.links[linkIdx].target =
                     this.graph.nodes.find((node) =>
                         node.id === epB);
@@ -472,17 +472,27 @@
             case ModelEventType.LINK_ADDED_OR_UPDATED:
                 if (memo === ModelEventMemo.ADDED &&
                     this.regionData.links.findIndex((l) => l.id === subject) === -1) {
-                    const listLen = this.regionData.links.push(<RegionLink>data);
+                    const newLink = <RegionLink>data;
+
+
                     const epA = ForceSvgComponent.extractNodeName(
-                        this.regionData.links[listLen - 1].epA);
-                    this.regionData.links[listLen - 1].source =
-                        this.graph.nodes.find((node) =>
-                            node.id === epA);
+                        newLink.epA, newLink.portA);
+                    if (!this.graph.nodes.find((node) => node.id === epA)) {
+                        this.log.error('Could not find endpoint A', epA, 'of', newLink);
+                        break;
+                    }
                     const epB = ForceSvgComponent.extractNodeName(
-                        this.regionData.links[listLen - 1].epB);
+                        newLink.epB, newLink.portB);
+                    if (!this.graph.nodes.find((node) => node.id === epB)) {
+                        this.log.error('Could not find endpoint B', epB, 'of link', newLink);
+                        break;
+                    }
+
+                    const listLen = this.regionData.links.push(<RegionLink>data);
+                    this.regionData.links[listLen - 1].source =
+                        this.graph.nodes.find((node) => node.id === epA);
                     this.regionData.links[listLen - 1].target =
-                        this.graph.nodes.find((node) =>
-                            node.id === epB);
+                        this.graph.nodes.find((node) => node.id === epB);
                     this.log.debug('Link added', subject);
                 } else if (memo === ModelEventMemo.UPDATED) {
                     const oldLink = this.regionData.links.find((l) => l.id === subject);
@@ -510,8 +520,8 @@
         const len = this.regionData.links.length;
         for (let i = 0; i < len; i++) {
             const linkIdx = this.regionData.links.findIndex((l) =>
-                (ForceSvgComponent.extractNodeName(l.epA) === subject ||
-                    ForceSvgComponent.extractNodeName(l.epB) === subject));
+                (ForceSvgComponent.extractNodeName(l.epA, l.portA) === subject ||
+                    ForceSvgComponent.extractNodeName(l.epB, l.portB) === subject));
             if (linkIdx >= 0) {
                 this.regionData.links.splice(linkIdx, 1);
                 this.log.debug('Link ', linkIdx, 'removed on attempt', i);
diff --git a/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/tests/test-OdtnConfig-topo2CurrentRegion.json b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/tests/test-OdtnConfig-topo2CurrentRegion.json
new file mode 100644
index 0000000..2087940
--- /dev/null
+++ b/web/gui2-topo-lib/projects/gui2-topo-lib/src/lib/layer/forcesvg/tests/test-OdtnConfig-topo2CurrentRegion.json
@@ -0,0 +1,165 @@
+{
+    "event": "topo2CurrentRegion",
+    "payload": {
+        "id": "(root)",
+        "subregions": [],
+        "links": [
+            {
+                "id": "netconf:127.0.0.1:11002/201~netconf:127.0.0.1:11003/201",
+                "epA": "netconf:127.0.0.1:11002/201",
+                "epB": "netconf:127.0.0.1:11003/201",
+                "type": "UiDeviceLink",
+                "portA": "201",
+                "portB": "201",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/201~netconf:127.0.0.1:11003/201",
+                        "epA": "netconf:127.0.0.1:11002/201",
+                        "epB": "netconf:127.0.0.1:11003/201",
+                        "type": "UiDeviceLink",
+                        "portA": "201",
+                        "portB": "201"
+                    }
+                ]
+            },
+            {
+                "id": "netconf:127.0.0.1:11002/202~netconf:127.0.0.1:11003/202",
+                "epA": "netconf:127.0.0.1:11002/202",
+                "epB": "netconf:127.0.0.1:11003/202",
+                "type": "UiDeviceLink",
+                "portA": "202",
+                "portB": "202",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/202~netconf:127.0.0.1:11003/202",
+                        "epA": "netconf:127.0.0.1:11002/202",
+                        "epB": "netconf:127.0.0.1:11003/202",
+                        "type": "UiDeviceLink",
+                        "portA": "202",
+                        "portB": "202"
+                    }
+                ]
+            },
+            {
+                "id": "netconf:127.0.0.1:11002/203~netconf:127.0.0.1:11003/203",
+                "epA": "netconf:127.0.0.1:11002/203",
+                "epB": "netconf:127.0.0.1:11003/203",
+                "type": "UiDeviceLink",
+                "portA": "203",
+                "portB": "203",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/203~netconf:127.0.0.1:11003/203",
+                        "epA": "netconf:127.0.0.1:11002/203",
+                        "epB": "netconf:127.0.0.1:11003/203",
+                        "type": "UiDeviceLink",
+                        "portA": "203",
+                        "portB": "203"
+                    }
+                ]
+            },
+            {
+                "id": "netconf:127.0.0.1:11002/204~netconf:127.0.0.1:11003/204",
+                "epA": "netconf:127.0.0.1:11002/204",
+                "epB": "netconf:127.0.0.1:11003/204",
+                "type": "UiDeviceLink",
+                "portA": "204",
+                "portB": "204",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/204~netconf:127.0.0.1:11003/204",
+                        "epA": "netconf:127.0.0.1:11002/204",
+                        "epB": "netconf:127.0.0.1:11003/204",
+                        "type": "UiDeviceLink",
+                        "portA": "204",
+                        "portB": "204"
+                    }
+                ]
+            },
+            {
+                "id": "netconf:127.0.0.1:11002/205~netconf:127.0.0.1:11003/205",
+                "epA": "netconf:127.0.0.1:11002/205",
+                "epB": "netconf:127.0.0.1:11003/205",
+                "type": "UiDeviceLink",
+                "portA": "205",
+                "portB": "205",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/205~netconf:127.0.0.1:11003/205",
+                        "epA": "netconf:127.0.0.1:11002/205",
+                        "epB": "netconf:127.0.0.1:11003/205",
+                        "type": "UiDeviceLink",
+                        "portA": "205",
+                        "portB": "205"
+                    }
+                ]
+            },
+            {
+                "id": "netconf:127.0.0.1:11002/206~netconf:127.0.0.1:11003/206",
+                "epA": "netconf:127.0.0.1:11002/206",
+                "epB": "netconf:127.0.0.1:11003/206",
+                "type": "UiDeviceLink",
+                "portA": "206",
+                "portB": "206",
+                "rollup": [
+                    {
+                        "id": "netconf:127.0.0.1:11002/206~netconf:127.0.0.1:11003/206",
+                        "epA": "netconf:127.0.0.1:11002/206",
+                        "epB": "netconf:127.0.0.1:11003/206",
+                        "type": "UiDeviceLink",
+                        "portA": "206",
+                        "portB": "206"
+                    }
+                ]
+            }
+        ],
+        "devices": [
+            [],
+            [],
+            [
+                {
+                    "id": "netconf:127.0.0.1:11002",
+                    "nodeType": "device",
+                    "type": "terminal_device",
+                    "online": true,
+                    "master": "127.0.0.1",
+                    "layer": "def",
+                    "props": {
+                        "ipaddress": "127.0.0.1",
+                        "protocol": "NETCONF",
+                        "driver": "cassini-ocnos",
+                        "port": "11002",
+                        "name": "cassini2",
+                        "locType": "none"
+                    }
+                },
+                {
+                    "id": "netconf:127.0.0.1:11003",
+                    "nodeType": "device",
+                    "type": "terminal_device",
+                    "online": true,
+                    "master": "127.0.0.1",
+                    "layer": "def",
+                    "props": {
+                        "ipaddress": "127.0.0.1",
+                        "protocol": "NETCONF",
+                        "driver": "cassini-ocnos",
+                        "port": "11003",
+                        "name": "cassini1",
+                        "locType": "none"
+                    }
+                }
+            ]
+        ],
+        "hosts": [
+            [],
+            [],
+            []
+        ],
+        "layerOrder": [
+            "opt",
+            "pkt",
+            "def"
+        ]
+    }
+}