Address comments in gerrit 20328

Create a new class XconnecEndpoint to cover both physical port and load balancer
Also change the CLI load balancer identifier to "LB:"

Change-Id: I0b4cd6e474d8b21468d87fcadd9280fdf7d6aafa
diff --git a/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectAddCommand.java b/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectAddCommand.java
index a986c30..22daeb5 100644
--- a/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectAddCommand.java
+++ b/app/src/main/java/org/onosproject/segmentrouting/cli/XconnectAddCommand.java
@@ -26,18 +26,22 @@
 import org.onosproject.cli.net.DeviceIdCompleter;
 import org.onosproject.cli.net.PortNumberCompleter;
 import org.onosproject.net.DeviceId;
+import org.onosproject.segmentrouting.xconnect.api.XconnectEndpoint;
+import org.onosproject.segmentrouting.xconnect.api.XconnectPortEndpoint;
 import org.onosproject.segmentrouting.xconnect.api.XconnectService;
 
 import java.util.Set;
 
-import static com.google.common.base.Preconditions.checkArgument;
-
 /**
  * Creates Xconnect.
  */
 @Service
 @Command(scope = "onos", name = "sr-xconnect-add", description = "Create Xconnect")
 public class XconnectAddCommand extends AbstractShellCommand {
+    private static final String EP_DESC = "Can be a physical port number or a load balancer key. " +
+            "Use integer to specify physical port number. " +
+            "Use " + XconnectPortEndpoint.LB_KEYWORD + "key to specify load balancer key";
+
     @Argument(index = 0, name = "deviceId",
             description = "Device ID",
             required = true, multiValued = false)
@@ -50,17 +54,17 @@
     @Completion(PlaceholderCompleter.class)
     private String vlanIdStr;
 
-    @Argument(index = 2, name = "port1",
-            description = "Port 1. Can also specify L2 load balancer by L2LB(<key>)",
+    @Argument(index = 2, name = "ep1",
+            description = "First endpoint. " + EP_DESC,
             required = true, multiValued = false)
     @Completion(PortNumberCompleter.class)
-    private String port1Str;
+    private String ep1Str;
 
-    @Argument(index = 3, name = "port2",
-            description = "Port 2. Can also specify L2 load balancer by L2LB(<key>)",
+    @Argument(index = 3, name = "ep2",
+            description = "Second endpoint. " + EP_DESC,
             required = true, multiValued = false)
     @Completion(PortNumberCompleter.class)
-    private String port2Str;
+    private String ep2Str;
 
     private static final String L2LB_PATTERN = "^(\\d*|L2LB\\(\\d*\\))$";
 
@@ -68,12 +72,15 @@
     protected void doExecute() {
         DeviceId deviceId = DeviceId.deviceId(deviceIdStr);
         VlanId vlanId = VlanId.vlanId(vlanIdStr);
-        Set<String> ports = Sets.newHashSet(port1Str, port2Str);
 
-        checkArgument(port1Str.matches(L2LB_PATTERN), "Wrong L2 load balancer format " + port1Str);
-        checkArgument(port2Str.matches(L2LB_PATTERN), "Wrong L2 load balancer format " + port2Str);
+        XconnectEndpoint ep1 = XconnectEndpoint.fromString(ep1Str);
+        XconnectEndpoint ep2 = XconnectEndpoint.fromString(ep2Str);
+
+        Set<XconnectEndpoint> endpoints = Sets.newHashSet(ep1, ep2);
 
         XconnectService xconnectService = get(XconnectService.class);
-        xconnectService.addOrUpdateXconnect(deviceId, vlanId, ports);
+        xconnectService.addOrUpdateXconnect(deviceId, vlanId, endpoints);
     }
+
+
 }
diff --git a/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectCodec.java b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectCodec.java
index 08558a8..1154e40 100644
--- a/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectCodec.java
+++ b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectCodec.java
@@ -28,10 +28,13 @@
 
 import java.util.Set;
 
+/**
+ * Codec for Xconnect.
+ */
 public class XconnectCodec extends JsonCodec<XconnectDesc> {
     private static final String DEVICE_ID = "deviceId";
     private static final String VLAN_ID = "vlanId";
-    private static final String PORTS = "ports";
+    private static final String ENDPOINTS = "endpoints";
 
     private static Logger log = LoggerFactory.getLogger(XconnectCodec.class);
 
@@ -40,8 +43,8 @@
         final ObjectNode result = context.mapper().createObjectNode();
         result.put(DEVICE_ID, desc.key().deviceId().toString());
         result.put(VLAN_ID, desc.key().vlanId().toString());
-        final ArrayNode portNode = result.putArray(PORTS);
-        desc.ports().forEach(port -> portNode.add(port.toString()));
+        final ArrayNode portNode = result.putArray(ENDPOINTS);
+        desc.endpoints().forEach(endpoint -> portNode.add(endpoint.toString()));
 
         return result;
     }
@@ -51,13 +54,14 @@
         DeviceId deviceId = DeviceId.deviceId(json.path(DEVICE_ID).asText());
         VlanId vlanId = VlanId.vlanId(json.path(VLAN_ID).asText());
 
-        Set<String> ports = Sets.newHashSet();
-        JsonNode portNodes = json.get(PORTS);
-        if (portNodes != null) {
-            portNodes.forEach(portNode -> ports.add(portNode.asText()));
+        Set<XconnectEndpoint> endpoints = Sets.newHashSet();
+        JsonNode endpointNodes = json.get(ENDPOINTS);
+        if (endpointNodes != null) {
+            XconnectEndpoint endpoint = XconnectEndpoint.fromString(endpointNodes.asText());
+            endpointNodes.forEach(endpointNode -> endpoints.add(endpoint));
         }
 
         XconnectKey key = new XconnectKey(deviceId, vlanId);
-        return new XconnectDesc(key, ports);
+        return new XconnectDesc(key, endpoints);
     }
 }
diff --git a/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectDesc.java b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectDesc.java
index e94c1db..1e7a3b2 100644
--- a/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectDesc.java
+++ b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectDesc.java
@@ -25,17 +25,17 @@
  */
 public class XconnectDesc {
     private XconnectKey key;
-    private Set<String> ports;
+    private Set<XconnectEndpoint> endpoints;
 
     /**
      * Constructs new Xconnect description with given device ID and VLAN ID.
      *
      * @param key Xconnect key
-     * @param ports set of ports
+     * @param endpoints set of endpoints
      */
-    public XconnectDesc(XconnectKey key, Set<String> ports) {
+    public XconnectDesc(XconnectKey key, Set<XconnectEndpoint> endpoints) {
         this.key = key;
-        this.ports = ports;
+        this.endpoints = endpoints;
     }
 
     /**
@@ -48,12 +48,12 @@
     }
 
     /**
-     * Gets ports.
+     * Gets endpoints.
      *
-     * @return set of ports
+     * @return set of endpoints
      */
-    public Set<String> ports() {
-        return ports;
+    public Set<XconnectEndpoint> endpoints() {
+        return endpoints;
     }
 
     @Override
@@ -69,19 +69,19 @@
         }
         final XconnectDesc other = (XconnectDesc) obj;
         return Objects.equals(this.key, other.key) &&
-                Objects.equals(this.ports, other.ports);
+                Objects.equals(this.endpoints, other.endpoints);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(key, ports);
+        return Objects.hash(key, endpoints);
     }
 
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(getClass())
                 .add("key", key)
-                .add("ports", ports)
+                .add("endpoints", endpoints)
                 .toString();
     }
 }
diff --git a/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectEndpoint.java b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectEndpoint.java
new file mode 100644
index 0000000..c7a2242
--- /dev/null
+++ b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectEndpoint.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.segmentrouting.xconnect.api;
+
+/**
+ * Represents cross connect endpoint.
+ */
+public abstract class XconnectEndpoint {
+    public static final String LB_KEYWORD = "LB:";
+    static final String PORT_PATTERN = "^\\d+$";
+    static final String LOAD_BALANCER_PATTERN = "^" + LB_KEYWORD + "\\d+$";
+
+    /**
+     * Types of endpoint.
+     */
+    public enum Type {
+        /**
+         * The endpoint is specified by an port number.
+         */
+        PORT,
+
+        /**
+         * The endpoint is specified by a load balancer.
+         */
+        LOAD_BALANCER
+    }
+
+    /**
+     * Type of this endpoint.
+     *
+     * @return type
+     */
+    public abstract XconnectEndpoint.Type type();
+
+    /**
+     * Constructs XconnectEndpoint from string.
+     *
+     * @param s string
+     * @return XconnectEndpoint
+     * @throws IllegalArgumentException if given string is in a wrong format
+     */
+    public static XconnectEndpoint fromString(String s) {
+        if (s.matches(XconnectEndpoint.PORT_PATTERN)) {
+            return XconnectPortEndpoint.fromString(s);
+        } else if (s.matches(XconnectEndpoint.LOAD_BALANCER_PATTERN)) {
+            return XconnectLoadBalancerEndpoint.fromString(s);
+        } else {
+            throw new IllegalArgumentException("Illegal endpoint format: " + s);
+        }
+    }
+}
diff --git a/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectLoadBalancerEndpoint.java b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectLoadBalancerEndpoint.java
new file mode 100644
index 0000000..4172292
--- /dev/null
+++ b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectLoadBalancerEndpoint.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.segmentrouting.xconnect.api;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Represents a cross connect endpoint specified by load balancer.
+ */
+public final class XconnectLoadBalancerEndpoint extends XconnectEndpoint {
+    private final int key;
+
+    private XconnectLoadBalancerEndpoint(int key) {
+        this.key = key;
+    }
+
+    /**
+     * Returns load balancer key.
+     *
+     * @return load balancer key.
+     */
+    public int key() {
+        return key;
+    }
+
+    /**
+     * Returns an instance of XconnectLoadBalancerEndpoint with given load balancer key.
+     *
+     * @param key load balancer key
+     * @return an instance of XconnectLoadBalancerEndpoint
+     */
+    public static XconnectLoadBalancerEndpoint of(int key) {
+        return new XconnectLoadBalancerEndpoint(key);
+    }
+
+    /**
+     * Gets XconnectLoadBalancerEndpoint from string.
+     *
+     * @param s string
+     * @return XconnectLoadBalancerEndpoint
+     */
+    public static XconnectLoadBalancerEndpoint fromString(String s) {
+        checkArgument(s.matches(LOAD_BALANCER_PATTERN), "String {} does not match {} format", s, LOAD_BALANCER_PATTERN);
+        return new XconnectLoadBalancerEndpoint(Integer.valueOf(s.replaceFirst(LB_KEYWORD, "")));
+    }
+
+    @Override
+    public Type type() {
+        return Type.LOAD_BALANCER;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(key);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof XconnectLoadBalancerEndpoint) {
+            final XconnectLoadBalancerEndpoint other = (XconnectLoadBalancerEndpoint) obj;
+            return Objects.equals(this.key, other.key);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return LB_KEYWORD + String.valueOf(key);
+    }
+}
diff --git a/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectPortEndpoint.java b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectPortEndpoint.java
new file mode 100644
index 0000000..f26eaf0
--- /dev/null
+++ b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectPortEndpoint.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2019-present Open Networking Foundation
+ *
+ * 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.segmentrouting.xconnect.api;
+
+import org.onosproject.net.PortNumber;
+
+import java.util.Objects;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Represents a cross connect endpoint specified by port number.
+ */
+public final class XconnectPortEndpoint extends XconnectEndpoint {
+    private final PortNumber port;
+
+    private XconnectPortEndpoint(PortNumber port) {
+        this.port = port;
+    }
+
+    /**
+     * Returns port number.
+     *
+     * @return port number
+     */
+    public PortNumber port() {
+        return port;
+    }
+
+    /**
+     * Returns an instance of XconnectPortEndpoint with given port number.
+     *
+     * @param port port number
+     * @return an instance of XconnectPortEndpoint
+     */
+    public static XconnectPortEndpoint of(PortNumber port) {
+        return new XconnectPortEndpoint(port);
+    }
+
+    /**
+     * Gets XconnectPortEndpoint from string.
+     *
+     * @param s string
+     * @return XconnectPortEndpoint
+     */
+    public static XconnectPortEndpoint fromString(String s) {
+        checkArgument(s.matches(PORT_PATTERN), "String {} does not match {} format", s, PORT_PATTERN);
+        return new XconnectPortEndpoint(PortNumber.fromString(s));
+    }
+
+    @Override
+    public XconnectEndpoint.Type type() {
+        return Type.PORT;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(port);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof XconnectPortEndpoint) {
+            final XconnectPortEndpoint other = (XconnectPortEndpoint) obj;
+            return Objects.equals(this.port, other.port);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return String.valueOf(port);
+    }
+}
diff --git a/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java
index fe51b06..2fe6488 100644
--- a/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java
+++ b/app/src/main/java/org/onosproject/segmentrouting/xconnect/api/XconnectService.java
@@ -44,9 +44,9 @@
      *
      * @param deviceId device ID
      * @param vlanId VLAN ID
-     * @param ports set of ports
+     * @param endpoints set of endpoints
      */
-    void addOrUpdateXconnect(DeviceId deviceId, VlanId vlanId, Set<String> ports);
+    void addOrUpdateXconnect(DeviceId deviceId, VlanId vlanId, Set<XconnectEndpoint> endpoints);
 
     /**
      * Deletes Xconnect.
diff --git a/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java b/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java
index d8f03aa..80c1c80 100644
--- a/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java
+++ b/app/src/main/java/org/onosproject/segmentrouting/xconnect/impl/XconnectManager.java
@@ -74,7 +74,10 @@
 import org.onosproject.segmentrouting.storekey.VlanNextObjectiveStoreKey;
 import org.onosproject.segmentrouting.xconnect.api.XconnectCodec;
 import org.onosproject.segmentrouting.xconnect.api.XconnectDesc;
+import org.onosproject.segmentrouting.xconnect.api.XconnectEndpoint;
 import org.onosproject.segmentrouting.xconnect.api.XconnectKey;
+import org.onosproject.segmentrouting.xconnect.api.XconnectLoadBalancerEndpoint;
+import org.onosproject.segmentrouting.xconnect.api.XconnectPortEndpoint;
 import org.onosproject.segmentrouting.xconnect.api.XconnectService;
 import org.onosproject.store.serializers.KryoNamespaces;
 import org.onosproject.store.service.ConsistentMap;
@@ -147,7 +150,7 @@
     HostService hostService;
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY)
-    L2LbService l2LbService;
+    private L2LbService l2LbService;
 
     private static final String APP_NAME = "org.onosproject.xconnect";
     private static final String ERROR_NOT_LEADER = "Not leader controller";
@@ -157,13 +160,13 @@
     private static Logger log = LoggerFactory.getLogger(XconnectManager.class);
 
     private ApplicationId appId;
-    private ConsistentMap<XconnectKey, Set<String>> xconnectStore;
+    private ConsistentMap<XconnectKey, Set<XconnectEndpoint>> xconnectStore;
     private ConsistentMap<XconnectKey, Integer> xconnectNextObjStore;
 
     private ConsistentMap<VlanNextObjectiveStoreKey, Integer> xconnectMulticastNextStore;
     private ConsistentMap<VlanNextObjectiveStoreKey, List<PortNumber>> xconnectMulticastPortsStore;
 
-    private final MapEventListener<XconnectKey, Set<String>> xconnectListener = new XconnectMapListener();
+    private final MapEventListener<XconnectKey, Set<XconnectEndpoint>> xconnectListener = new XconnectMapListener();
     private ExecutorService xConnectExecutor;
 
     private final DeviceListener deviceListener = new InternalDeviceListener();
@@ -178,8 +181,6 @@
     private Cache<L2LbId, XconnectKey> l2LbCache;
     // Executor for the cache
     private ScheduledExecutorService l2lbExecutor;
-    // Pattern for L2Lb key
-    private static final String L2LB_PATTERN = "^(L2LB\\(\\d*\\))$";
     // We need to listen for some events to properly installed the xconnect with l2lb
     private final L2LbListener l2LbListener = new InternalL2LbListener();
 
@@ -192,9 +193,12 @@
                 .register(KryoNamespaces.API)
                 .register(XconnectManager.class)
                 .register(XconnectKey.class)
+                .register(XconnectEndpoint.class)
+                .register(XconnectPortEndpoint.class)
+                .register(XconnectLoadBalancerEndpoint.class)
                 .register(VlanNextObjectiveStoreKey.class);
 
-        xconnectStore = storageService.<XconnectKey, Set<String>>consistentMapBuilder()
+        xconnectStore = storageService.<XconnectKey, Set<XconnectEndpoint>>consistentMapBuilder()
                 .withName("onos-sr-xconnect")
                 .withRelaxedReadConsistency()
                 .withSerializer(Serializer.using(serializer.build()))
@@ -257,11 +261,11 @@
     }
 
     @Override
-    public void addOrUpdateXconnect(DeviceId deviceId, VlanId vlanId, Set<String> ports) {
-        log.info("Adding or updating xconnect. deviceId={}, vlanId={}, ports={}",
-                 deviceId, vlanId, ports);
+    public void addOrUpdateXconnect(DeviceId deviceId, VlanId vlanId, Set<XconnectEndpoint> endpoints) {
+        log.info("Adding or updating xconnect. deviceId={}, vlanId={}, endpoints={}",
+                 deviceId, vlanId, endpoints);
         final XconnectKey key = new XconnectKey(deviceId, vlanId);
-        xconnectStore.put(key, ports);
+        xconnectStore.put(key, endpoints);
     }
 
     @Override
@@ -272,9 +276,9 @@
         xconnectStore.remove(key);
 
         // Cleanup multicasting support, if any.
-        srService.getPairDeviceId(deviceId).ifPresent(pairDeviceId -> {
-            cleanupL2MulticastRule(pairDeviceId, srService.getPairLocalPort(pairDeviceId).get(), vlanId, true);
-        });
+        srService.getPairDeviceId(deviceId).ifPresent(pairDeviceId ->
+            cleanupL2MulticastRule(pairDeviceId, srService.getPairLocalPort(pairDeviceId).get(), vlanId, true)
+        );
 
     }
 
@@ -287,16 +291,20 @@
 
     @Override
     public boolean hasXconnect(ConnectPoint cp) {
-        return getXconnects().stream().anyMatch(desc -> desc.key().deviceId().equals(cp.deviceId())
-                && desc.ports().contains(cp.port().toString())
+        return getXconnects().stream().anyMatch(desc ->
+                desc.key().deviceId().equals(cp.deviceId()) && desc.endpoints().stream().anyMatch(ep ->
+                        ep.type() == XconnectEndpoint.Type.PORT && ((XconnectPortEndpoint) ep).port().equals(cp.port())
+                )
         );
     }
 
     @Override
     public List<VlanId> getXconnectVlans(DeviceId deviceId, PortNumber port) {
         return getXconnects().stream()
-                .filter(desc -> desc.key().deviceId().equals(deviceId) && desc.ports().contains(port.toString()))
-                .map(desc -> desc.key().vlanId())
+                .filter(desc -> desc.key().deviceId().equals(deviceId) && desc.endpoints().stream().anyMatch(ep ->
+                        ep.type() == XconnectEndpoint.Type.PORT && ((XconnectPortEndpoint) ep).port().equals(port)))
+                .map(XconnectDesc::key)
+                .map(XconnectKey::vlanId)
                 .collect(Collectors.toList());
     }
 
@@ -329,12 +337,12 @@
         });
     }
 
-    private class XconnectMapListener implements MapEventListener<XconnectKey, Set<String>> {
+    private class XconnectMapListener implements MapEventListener<XconnectKey, Set<XconnectEndpoint>> {
         @Override
-        public void event(MapEvent<XconnectKey, Set<String>> event) {
+        public void event(MapEvent<XconnectKey, Set<XconnectEndpoint>> event) {
             XconnectKey key = event.key();
-            Set<String> ports = Versioned.valueOrNull(event.newValue());
-            Set<String> oldPorts = Versioned.valueOrNull(event.oldValue());
+            Set<XconnectEndpoint> ports = Versioned.valueOrNull(event.newValue());
+            Set<XconnectEndpoint> oldPorts = Versioned.valueOrNull(event.oldValue());
 
             switch (event.type()) {
                 case INSERT:
@@ -476,7 +484,7 @@
     private void init(DeviceId deviceId) {
         getXconnects().stream()
                 .filter(desc -> desc.key().deviceId().equals(deviceId))
-                .forEach(desc -> populateXConnect(desc.key(), desc.ports()));
+                .forEach(desc -> populateXConnect(desc.key(), desc.endpoints()));
     }
 
     private void cleanup(DeviceId deviceId) {
@@ -489,21 +497,21 @@
     /**
      * Populates XConnect groups and flows for given key.
      *
-     * @param key   XConnect key
-     * @param ports a set of ports to be cross-connected
+     * @param key       XConnect key
+     * @param endpoints a set of endpoints to be cross-connected
      */
-    private void populateXConnect(XconnectKey key, Set<String> ports) {
+    private void populateXConnect(XconnectKey key, Set<XconnectEndpoint> endpoints) {
         if (!isLocalLeader(key.deviceId())) {
             log.debug("Abort populating XConnect {}: {}", key, ERROR_NOT_LEADER);
             return;
         }
 
-        int nextId = populateNext(key, ports);
+        int nextId = populateNext(key, endpoints);
         if (nextId == -1) {
             log.warn("Fail to populateXConnect {}: {}", key, ERROR_NEXT_ID);
             return;
         }
-        populateFilter(key, ports);
+        populateFilter(key, endpoints);
         populateFwd(key, nextId);
         populateAcl(key);
     }
@@ -511,45 +519,45 @@
     /**
      * Populates filtering objectives for given XConnect.
      *
-     * @param key   XConnect store key
-     * @param ports XConnect ports
+     * @param key       XConnect store key
+     * @param endpoints XConnect endpoints
      */
-    private void populateFilter(XconnectKey key, Set<String> ports) {
+    private void populateFilter(XconnectKey key, Set<XconnectEndpoint> endpoints) {
         // FIXME Improve the logic
         //       If L2 load balancer is not involved, use filtered port. Otherwise, use unfiltered port.
         //       The purpose is to make sure existing XConnect logic can still work on a configured port.
-        boolean filtered = ports.stream()
-                .map(p -> getNextTreatment(key.deviceId(), p, false))
+        boolean filtered = endpoints.stream()
+                .map(ep -> getNextTreatment(key.deviceId(), ep, false))
                 .allMatch(t -> t.type().equals(NextTreatment.Type.TREATMENT));
 
-        ports.stream()
-                .map(p -> getPhysicalPorts(key.deviceId(), p))
+        endpoints.stream()
+                .map(ep -> getPhysicalPorts(key.deviceId(), ep))
                 .flatMap(Set::stream).forEach(port -> {
-            FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port, filtered);
-            ObjectiveContext context = new DefaultObjectiveContext(
-                    (objective) -> log.debug("XConnect FilterObj for {} on port {} populated",
-                            key, port),
-                    (objective, error) ->
-                            log.warn("Failed to populate XConnect FilterObj for {} on port {}: {}",
-                                    key, port, error));
-            flowObjectiveService.filter(key.deviceId(), filtObjBuilder.add(context));
-        });
+                    FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port, filtered);
+                    ObjectiveContext context = new DefaultObjectiveContext(
+                            (objective) -> log.debug("XConnect FilterObj for {} on port {} populated",
+                                    key, port),
+                            (objective, error) ->
+                                    log.warn("Failed to populate XConnect FilterObj for {} on port {}: {}",
+                                            key, port, error));
+                    flowObjectiveService.filter(key.deviceId(), filtObjBuilder.add(context));
+                });
     }
 
     /**
      * Populates next objectives for given XConnect.
      *
-     * @param key   XConnect store key
-     * @param ports XConnect ports
+     * @param key       XConnect store key
+     * @param endpoints XConnect endpoints
      * @return next id
      */
-    private int populateNext(XconnectKey key, Set<String> ports) {
+    private int populateNext(XconnectKey key, Set<XconnectEndpoint> endpoints) {
         int nextId = Versioned.valueOrElse(xconnectNextObjStore.get(key), -1);
         if (nextId != -1) {
             log.debug("NextObj for {} found, id={}", key, nextId);
             return nextId;
         } else {
-            NextObjective.Builder nextObjBuilder = nextObjBuilder(key, ports);
+            NextObjective.Builder nextObjBuilder = nextObjBuilder(key, endpoints);
             if (nextObjBuilder == null) {
                 log.warn("Fail to populate {}: {}", key, ERROR_NEXT_OBJ_BUILDER);
                 return -1;
@@ -602,61 +610,64 @@
     /**
      * Revokes XConnect groups and flows for given key.
      *
-     * @param key   XConnect key
-     * @param ports XConnect ports
+     * @param key       XConnect key
+     * @param endpoints XConnect endpoints
      */
-    private void revokeXConnect(XconnectKey key, Set<String> ports) {
+    private void revokeXConnect(XconnectKey key, Set<XconnectEndpoint> endpoints) {
         if (!isLocalLeader(key.deviceId())) {
             log.debug("Abort revoking XConnect {}: {}", key, ERROR_NOT_LEADER);
             return;
         }
 
-        revokeFilter(key, ports);
+        revokeFilter(key, endpoints);
         int nextId = Versioned.valueOrElse(xconnectNextObjStore.get(key), -1);
         if (nextId != -1) {
             revokeFwd(key, nextId, null);
-            revokeNext(key, ports, nextId, null);
+            revokeNext(key, endpoints, nextId, null);
         } else {
             log.warn("NextObj for {} does not exist in the store.", key);
         }
-        revokeFilter(key, ports);
+        revokeFilter(key, endpoints);
         revokeAcl(key);
     }
 
     /**
      * Revokes filtering objectives for given XConnect.
      *
-     * @param key   XConnect store key
-     * @param ports XConnect ports
+     * @param key       XConnect store key
+     * @param endpoints XConnect endpoints
      */
-    private void revokeFilter(XconnectKey key, Set<String> ports) {
+    private void revokeFilter(XconnectKey key, Set<XconnectEndpoint> endpoints) {
         // FIXME Improve the logic
         //       If L2 load balancer is not involved, use filtered port. Otherwise, use unfiltered port.
         //       The purpose is to make sure existing XConnect logic can still work on a configured port.
-        Set<Set<PortNumber>> portsSet = ports.stream()
-                .map(p -> getPhysicalPorts(key.deviceId(), p)).collect(Collectors.toSet());
-        boolean filtered = portsSet.stream().allMatch(s -> s.size() == 1);
-        portsSet.stream().flatMap(Set::stream).forEach(port -> {
-            FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port, filtered);
-            ObjectiveContext context = new DefaultObjectiveContext(
-                    (objective) -> log.debug("XConnect FilterObj for {} on port {} revoked",
-                                             key, port),
-                    (objective, error) ->
-                            log.warn("Failed to revoke XConnect FilterObj for {} on port {}: {}",
-                                     key, port, error));
-            flowObjectiveService.filter(key.deviceId(), filtObjBuilder.remove(context));
-        });
+        boolean filtered = endpoints.stream()
+                .map(ep -> getNextTreatment(key.deviceId(), ep, false))
+                .allMatch(t -> t.type().equals(NextTreatment.Type.TREATMENT));
+
+        endpoints.stream()
+                .map(ep -> getPhysicalPorts(key.deviceId(), ep)).
+                flatMap(Set::stream).forEach(port -> {
+                    FilteringObjective.Builder filtObjBuilder = filterObjBuilder(key, port, filtered);
+                    ObjectiveContext context = new DefaultObjectiveContext(
+                            (objective) -> log.debug("XConnect FilterObj for {} on port {} revoked",
+                                                     key, port),
+                            (objective, error) ->
+                                    log.warn("Failed to revoke XConnect FilterObj for {} on port {}: {}",
+                                             key, port, error));
+                    flowObjectiveService.filter(key.deviceId(), filtObjBuilder.remove(context));
+                });
     }
 
     /**
      * Revokes next objectives for given XConnect.
      *
      * @param key        XConnect store key
-     * @param ports      ports in the XConnect
+     * @param endpoints  XConnect endpoints
      * @param nextId     next objective id
      * @param nextFuture completable future for this next objective operation
      */
-    private void revokeNext(XconnectKey key, Set<String> ports, int nextId,
+    private void revokeNext(XconnectKey key, Set<XconnectEndpoint> endpoints, int nextId,
                             CompletableFuture<ObjectiveError> nextFuture) {
         ObjectiveContext context = new ObjectiveContext() {
             @Override
@@ -677,18 +688,18 @@
             }
         };
 
-        NextObjective.Builder nextObjBuilder = nextObjBuilder(key, ports, nextId);
+        NextObjective.Builder nextObjBuilder = nextObjBuilder(key, endpoints, nextId);
         if (nextObjBuilder == null) {
             log.warn("Fail to revokeNext {}: {}", key, ERROR_NEXT_OBJ_BUILDER);
             return;
         }
         // Release the L2Lbs if present
-        ports.forEach(port -> {
-            if (isL2LbKey(port)) {
-                String l2LbKey = port.substring("L2LB(".length(), port.length() - 1);
-                l2LbService.release(new L2LbId(key.deviceId(), Integer.parseInt(l2LbKey)), appId);
-            }
-        });
+        endpoints.stream()
+                .filter(endpoint -> endpoint.type() == XconnectEndpoint.Type.LOAD_BALANCER)
+                .forEach(endpoint -> {
+                    String l2LbKey = String.valueOf(((XconnectLoadBalancerEndpoint) endpoint).key());
+                    l2LbService.release(new L2LbId(key.deviceId(), Integer.parseInt(l2LbKey)), appId);
+                });
         flowObjectiveService.next(key.deviceId(), nextObjBuilder.remove(context));
         xconnectNextObjStore.remove(key);
     }
@@ -739,12 +750,12 @@
     /**
      * Updates XConnect groups and flows for given key.
      *
-     * @param key       XConnect key
-     * @param prevPorts previous XConnect ports
-     * @param ports     new XConnect ports
+     * @param key           XConnect key
+     * @param prevEndpoints previous XConnect endpoints
+     * @param endpoints     new XConnect endpoints
      */
-    private void updateXConnect(XconnectKey key, Set<String> prevPorts,
-                                Set<String> ports) {
+    private void updateXConnect(XconnectKey key, Set<XconnectEndpoint> prevEndpoints,
+                                Set<XconnectEndpoint> endpoints) {
         if (!isLocalLeader(key.deviceId())) {
             log.debug("Abort updating XConnect {}: {}", key, ERROR_NOT_LEADER);
             return;
@@ -753,11 +764,11 @@
         //       Pair port is built-in and thus not going to change. No need to update it.
 
         // remove old filter
-        prevPorts.stream().filter(port -> !ports.contains(port)).forEach(port ->
-                revokeFilter(key, ImmutableSet.of(port)));
+        prevEndpoints.stream().filter(prevEndpoint -> !endpoints.contains(prevEndpoint)).forEach(prevEndpoint ->
+                revokeFilter(key, ImmutableSet.of(prevEndpoint)));
         // install new filter
-        ports.stream().filter(port -> !prevPorts.contains(port)).forEach(port ->
-                populateFilter(key, ImmutableSet.of(port)));
+        endpoints.stream().filter(endpoint -> !prevEndpoints.contains(endpoint)).forEach(endpoint ->
+                populateFilter(key, ImmutableSet.of(endpoint)));
 
         CompletableFuture<ObjectiveError> fwdFuture = new CompletableFuture<>();
         CompletableFuture<ObjectiveError> nextFuture = new CompletableFuture<>();
@@ -769,14 +780,14 @@
             fwdFuture.thenAcceptAsync(fwdStatus -> {
                 if (fwdStatus == null) {
                     log.debug("Fwd removed. Now remove group {}", key);
-                    revokeNext(key, prevPorts, nextId, nextFuture);
+                    revokeNext(key, prevEndpoints, nextId, nextFuture);
                 }
             });
 
             nextFuture.thenAcceptAsync(nextStatus -> {
                 if (nextStatus == null) {
                     log.debug("Installing new group and flow for {}", key);
-                    int newNextId = populateNext(key, ports);
+                    int newNextId = populateNext(key, endpoints);
                     if (newNextId == -1) {
                         log.warn("Fail to updateXConnect {}: {}", key, ERROR_NEXT_ID);
                         return;
@@ -792,12 +803,12 @@
     /**
      * Creates a next objective builder for XConnect with given nextId.
      *
-     * @param key     XConnect key
-     * @param ports   ports or L2 load balancer key
+     * @param key       XConnect key
+     * @param endpoints XConnect endpoints
      * @param nextId  next objective id
      * @return next objective builder
      */
-    private NextObjective.Builder nextObjBuilder(XconnectKey key, Set<String> ports, int nextId) {
+    private NextObjective.Builder nextObjBuilder(XconnectKey key, Set<XconnectEndpoint> endpoints, int nextId) {
         TrafficSelector metadata =
                 DefaultTrafficSelector.builder().matchVlanId(key.vlanId()).build();
         NextObjective.Builder nextObjBuilder = DefaultNextObjective
@@ -805,13 +816,13 @@
                 .withType(NextObjective.Type.BROADCAST).fromApp(appId)
                 .withMeta(metadata);
 
-        for (String port : ports) {
-            NextTreatment nextTreatment = getNextTreatment(key.deviceId(), port, true);
+        for (XconnectEndpoint endpoint : endpoints) {
+            NextTreatment nextTreatment = getNextTreatment(key.deviceId(), endpoint, true);
             if (nextTreatment == null) {
                 // If a L2Lb is used in the XConnect - putting on hold
-                if (isL2LbKey(port)) {
+                if (endpoint.type() == XconnectEndpoint.Type.LOAD_BALANCER) {
                     log.warn("Unable to create nextObj. L2Lb not ready");
-                    String l2LbKey = port.substring("L2LB(".length(), port.length() - 1);
+                    String l2LbKey = String.valueOf(((XconnectLoadBalancerEndpoint) endpoint).key());
                     l2LbCache.asMap().putIfAbsent(new L2LbId(key.deviceId(), Integer.parseInt(l2LbKey)),
                                                   key);
                 } else {
@@ -828,13 +839,13 @@
     /**
      * Creates a next objective builder for XConnect.
      *
-     * @param key   XConnect key
-     * @param ports set of XConnect ports
+     * @param key       XConnect key
+     * @param endpoints Xconnect endpoints
      * @return next objective builder
      */
-    private NextObjective.Builder nextObjBuilder(XconnectKey key, Set<String> ports) {
+    private NextObjective.Builder nextObjBuilder(XconnectKey key, Set<XconnectEndpoint> endpoints) {
         int nextId = flowObjectiveService.allocateNextId();
-        return nextObjBuilder(key, ports, nextId);
+        return nextObjBuilder(key, endpoints, nextId);
     }
 
 
@@ -1229,64 +1240,39 @@
         return true;
     }
 
-    private Set<PortNumber> getPhysicalPorts(DeviceId deviceId, String port) {
-        // If port is numeric, treat it as regular port.
-        // Otherwise try to parse it as load balancer key and get the physical port the LB maps to.
-        try {
-            return Sets.newHashSet(PortNumber.portNumber(Integer.parseInt(port)));
-        } catch (NumberFormatException e) {
-            log.debug("Port {} is not numeric. Try to parse it as load balancer key", port);
+    private Set<PortNumber> getPhysicalPorts(DeviceId deviceId, XconnectEndpoint endpoint) {
+        if (endpoint.type() == XconnectEndpoint.Type.PORT) {
+            PortNumber port = ((XconnectPortEndpoint) endpoint).port();
+            return Sets.newHashSet(port);
         }
-
-        String l2LbKey = port.substring("L2LB(".length(), port.length() - 1);
-        L2LbId l2LbId = new L2LbId(deviceId, Integer.parseInt(l2LbKey));
-        try {
-            return Sets.newHashSet(l2LbService.getL2Lb(l2LbId).ports());
-        } catch (NumberFormatException e) {
-            log.debug("Port {} is not load balancer key either. Ignore", port);
-        } catch (NullPointerException e) {
-            log.debug("L2 load balancer {} not found. Ignore", l2LbKey);
+        if (endpoint.type() == XconnectEndpoint.Type.LOAD_BALANCER) {
+            L2LbId l2LbId = new L2LbId(deviceId, ((XconnectLoadBalancerEndpoint) endpoint).key());
+            Set<PortNumber> ports = l2LbService.getL2Lb(l2LbId).ports();
+            return Sets.newHashSet(ports);
         }
-
         return Sets.newHashSet();
     }
 
-    private NextTreatment getNextTreatment(DeviceId deviceId, String port, boolean reserve) {
-        // If port is numeric, treat it as regular port.
-        // Otherwise try to parse it as load balancer key and get the physical port the LB maps to.
-        try {
-            PortNumber portNumber = PortNumber.portNumber(Integer.parseInt(port));
-            return DefaultNextTreatment.of(DefaultTrafficTreatment.builder().setOutput(portNumber).build());
-        } catch (NumberFormatException e) {
-            log.debug("Port {} is not numeric. Try to parse it as load balancer key", port);
+    private NextTreatment getNextTreatment(DeviceId deviceId, XconnectEndpoint endpoint, boolean reserve) {
+        if (endpoint.type() == XconnectEndpoint.Type.PORT) {
+            PortNumber port = ((XconnectPortEndpoint) endpoint).port();
+            return DefaultNextTreatment.of(DefaultTrafficTreatment.builder().setOutput(port).build());
         }
-
-        String l2LbKey = port.substring("L2LB(".length(), port.length() - 1);
-        try {
-            L2LbId l2LbId = new L2LbId(deviceId, Integer.parseInt(l2LbKey));
-            NextTreatment idNextTreatment =  IdNextTreatment.of(
-                    l2LbService.getL2LbNext(l2LbId));
+        if (endpoint.type() == XconnectEndpoint.Type.LOAD_BALANCER) {
+            L2LbId l2LbId = new L2LbId(deviceId, ((XconnectLoadBalancerEndpoint) endpoint).key());
+            NextTreatment idNextTreatment =  IdNextTreatment.of(l2LbService.getL2LbNext(l2LbId));
             // Reserve only one time during next objective creation
             if (reserve) {
-                if (!l2LbService.reserve(new L2LbId(deviceId, Integer.parseInt(l2LbKey)), appId)) {
+                if (!l2LbService.reserve(l2LbId, appId)) {
                     log.warn("Reservation failed for {}", l2LbId);
                     idNextTreatment = null;
                 }
             }
             return idNextTreatment;
-        } catch (NumberFormatException e) {
-            log.debug("Port {} is not load balancer key either. Ignore", port);
-        } catch (NullPointerException e) {
-            log.debug("L2 load balancer {} not found. Ignore", l2LbKey);
         }
-
         return null;
     }
 
-    private boolean isL2LbKey(String l2LbKey) {
-        return l2LbKey.matches(L2LB_PATTERN);
-    }
-
     private class InternalL2LbListener implements L2LbListener {
         // Populate xconnect once l2lb is available
         @Override
@@ -1309,12 +1295,12 @@
         }
         log.debug("Dequeue {}", l2LbId);
         l2LbCache.invalidate(l2LbId);
-        Set<String> ports = Versioned.valueOrNull(xconnectStore.get(xconnectKey));
-        if (ports == null || ports.isEmpty()) {
-            log.warn("Ports not found for XConnect {}", xconnectKey);
+        Set<XconnectEndpoint> endpoints = Versioned.valueOrNull(xconnectStore.get(xconnectKey));
+        if (endpoints == null || endpoints.isEmpty()) {
+            log.warn("Endpoints not found for XConnect {}", xconnectKey);
             return;
         }
-        populateXConnect(xconnectKey, ports);
+        populateXConnect(xconnectKey, endpoints);
         log.trace("L2Lb cache size {}", l2LbCache.size());
     }
 
diff --git a/web/src/main/java/org/onosproject/segmentrouting/web/XconnectWebResource.java b/web/src/main/java/org/onosproject/segmentrouting/web/XconnectWebResource.java
index edaea9e..51c9cc9 100644
--- a/web/src/main/java/org/onosproject/segmentrouting/web/XconnectWebResource.java
+++ b/web/src/main/java/org/onosproject/segmentrouting/web/XconnectWebResource.java
@@ -75,12 +75,12 @@
         ObjectNode json = readTreeFromStream(mapper, input);
         XconnectDesc desc = codec(XconnectDesc.class).decode(json, this);
 
-        if (desc.ports().size() != 2) {
+        if (desc.endpoints().size() != 2) {
             throw new IllegalArgumentException("Ports should have only two items.");
         }
 
         XconnectService xconnectService = get(XconnectService.class);
-        xconnectService.addOrUpdateXconnect(desc.key().deviceId(), desc.key().vlanId(), desc.ports());
+        xconnectService.addOrUpdateXconnect(desc.key().deviceId(), desc.key().vlanId(), desc.endpoints());
 
         return Response.ok().build();
     }