Moving from a searching a string for capabilities to a Set.

ONOS-5947 ONOS-5948

Change-Id: Icac65263691e624dc74cfbc03c27e3974b935da2
diff --git a/protocols/netconf/api/src/main/java/org/onosproject/netconf/NetconfSession.java b/protocols/netconf/api/src/main/java/org/onosproject/netconf/NetconfSession.java
index 31481d2..4fffde6 100644
--- a/protocols/netconf/api/src/main/java/org/onosproject/netconf/NetconfSession.java
+++ b/protocols/netconf/api/src/main/java/org/onosproject/netconf/NetconfSession.java
@@ -20,8 +20,12 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.LinkedHashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * NETCONF session object that allows NETCONF operations on top with the physical
@@ -315,17 +319,45 @@
     String getSessionId();
 
     /**
-     * Gets the capabilities of the Netconf server associated to this session.
+     * Gets the capabilities of the remote Netconf device associated to this
+     * session.
+     *
+     * @return Network capabilities as strings in a Set.
+     *
+     * @since 1.10.0
+     * Note: default implementation provided with the interface
+     * will be removed when {@code getServerCapabilities()} reaches
+     * deprecation grace period.
+     */
+    default Set<String> getDeviceCapabilitiesSet() {
+        // default implementation should be removed in the future
+        Set<String> capabilities = new LinkedHashSet<>();
+        Matcher capabilityMatcher =
+                Pattern.compile("<capability>\\s*(.*?)\\s*</capability>")
+                       .matcher(getServerCapabilities());
+        while (capabilityMatcher.find()) {
+            capabilities.add(capabilityMatcher.group(1));
+        }
+        return capabilities;
+    }
+
+    /**
+     * Gets the capabilities of the Netconf server (remote device) associated
+     * to this session.
      *
      * @return Network capabilities as a string.
+     * @deprecated 1.10.0 use {@link #getDeviceCapabilitiesSet()} instead
      */
+    @Deprecated
     String getServerCapabilities();
 
     /**
      * Sets the ONOS side capabilities.
      *
      * @param capabilities list of capabilities the device has.
+     * @deprecated 1.10.0 use {@link #setOnosCapabilities(Iterable)} instead
      */
+    @Deprecated
     void setDeviceCapabilities(List<String> capabilities);
 
     /**
@@ -340,6 +372,17 @@
         Logger log = LoggerFactory.getLogger(NetconfSession.class);
         log.error("Not implemented/exposed by the underlying session implementation");
     }
+    /**
+     * Sets the ONOS side capabilities.
+     *
+     * @param capabilities list of capabilities ONOS has.
+     *
+     * @since 1.10.0
+     */
+    default void setOnosCapabilities(Iterable<String> capabilities) {
+        // default implementation should be removed in the future
+        // no-op
+    }
 
     /**
      * Remove a listener from the underlying stream handler implementation.
diff --git a/protocols/netconf/ctl/src/main/java/org/onosproject/netconf/ctl/NetconfSessionImpl.java b/protocols/netconf/ctl/src/main/java/org/onosproject/netconf/ctl/NetconfSessionImpl.java
index 7da836c..457bcf2 100644
--- a/protocols/netconf/ctl/src/main/java/org/onosproject/netconf/ctl/NetconfSessionImpl.java
+++ b/protocols/netconf/ctl/src/main/java/org/onosproject/netconf/ctl/NetconfSessionImpl.java
@@ -33,17 +33,20 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.regex.Pattern;
 
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
 
 /**
  * Implementation of a NETCONF session to talk to a device.
@@ -85,16 +88,27 @@
     private static final String SUBSCRIPTION_SUBTREE_FILTER_OPEN =
             "<filter xmlns:base10=\"urn:ietf:params:xml:ns:netconf:base:1.0\" base10:type=\"subtree\">";
 
-    private static Pattern msgIdPattern = Pattern.compile("(message-id=\"[0-9]+\")");
+    private static final String INTERLEAVE_CAPABILITY_STRING = "urn:ietf:params:netconf:capability:interleave:1.0";
 
+    private static final String CAPABILITY_REGEX = "<capability>\\s*(.*?)\\s*</capability>";
+    private static final Pattern CAPABILITY_REGEX_PATTERN = Pattern.compile(CAPABILITY_REGEX);
+
+    private static final String SESSION_ID_REGEX = "<session-id>\\s*(.*?)\\s*</session-id>";
+    private static final Pattern SESSION_ID_REGEX_PATTERN = Pattern.compile(SESSION_ID_REGEX);
+
+    private String sessionID;
     private final AtomicInteger messageIdInteger = new AtomicInteger(0);
     private Connection netconfConnection;
     private NetconfDeviceInfo deviceInfo;
     private Session sshSession;
     private boolean connectionActive;
-    private List<String> deviceCapabilities =
+    private Iterable<String> onosCapabilities =
             Collections.singletonList("urn:ietf:params:netconf:base:1.0");
-    private String serverCapabilities;
+
+    /* NOTE: the "serverHelloResponseOld" is deprecated in 1.10.0 and should eventually be removed */
+    @Deprecated
+    private String serverHelloResponseOld;
+    private final Set<String> deviceCapabilities = new LinkedHashSet<>();
     private NetconfStreamHandler streamHandler;
     private Map<Integer, CompletableFuture<String>> replies;
     private List<String> errorReplies;
@@ -178,7 +192,7 @@
 
     @Beta
     private void startSubscriptionConnection(String filterSchema) throws NetconfException {
-        if (!serverCapabilities.contains("interleave")) {
+        if (!deviceCapabilities.contains(INTERLEAVE_CAPABILITY_STRING)) {
             throw new NetconfException("Device" + deviceInfo + "does not support interleave");
         }
         String reply = sendRequest(createSubscriptionString(filterSchema));
@@ -237,7 +251,20 @@
     }
 
     private void sendHello() throws NetconfException {
-        serverCapabilities = sendRequest(createHelloString());
+        serverHelloResponseOld = sendRequest(createHelloString());
+        Matcher capabilityMatcher = CAPABILITY_REGEX_PATTERN.matcher(serverHelloResponseOld);
+        while (capabilityMatcher.find()) {
+            deviceCapabilities.add(capabilityMatcher.group(1));
+        }
+        sessionID = String.valueOf(-1);
+        Matcher sessionIDMatcher = SESSION_ID_REGEX_PATTERN.matcher(serverHelloResponseOld);
+        if (sessionIDMatcher.find()) {
+            sessionID = sessionIDMatcher.group(1);
+        } else {
+            throw new NetconfException("Missing SessionID in server hello " +
+                                               "reponse.");
+        }
+
     }
 
     private String createHelloString() {
@@ -246,7 +273,7 @@
         hellobuffer.append("\n");
         hellobuffer.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">\n");
         hellobuffer.append("  <capabilities>\n");
-        deviceCapabilities.forEach(
+        onosCapabilities.forEach(
                 cap -> hellobuffer.append("    <capability>")
                         .append(cap)
                         .append("</capability>\n"));
@@ -594,29 +621,32 @@
 
     @Override
     public String getSessionId() {
-        if (serverCapabilities.contains("<session-id>")) {
-            String[] outer = serverCapabilities.split("<session-id>");
-            Preconditions.checkArgument(outer.length != 1,
-                                        "Error in retrieving the session id");
-            String[] value = outer[1].split("</session-id>");
-            Preconditions.checkArgument(value.length != 1,
-                                        "Error in retrieving the session id");
-            return value[0];
-        } else {
-            return String.valueOf(-1);
-        }
+        return sessionID;
     }
 
     @Override
+    public Set<String> getDeviceCapabilitiesSet() {
+        return Collections.unmodifiableSet(deviceCapabilities);
+    }
+
+    @Deprecated
+    @Override
     public String getServerCapabilities() {
-        return serverCapabilities;
+        return serverHelloResponseOld;
+    }
+
+    @Deprecated
+    @Override
+    public void setDeviceCapabilities(List<String> capabilities) {
+        onosCapabilities = capabilities;
     }
 
     @Override
-    public void setDeviceCapabilities(List<String> capabilities) {
-        deviceCapabilities = capabilities;
+    public void setOnosCapabilities(Iterable<String> capabilities) {
+        onosCapabilities = capabilities;
     }
 
+
     @Override
     public void addDeviceOutputListener(NetconfDeviceOutputEventListener listener) {
         streamHandler.addDeviceEventListener(listener);
diff --git a/protocols/netconf/ctl/src/test/java/org/onosproject/netconf/ctl/NetconfSessionImplTest.java b/protocols/netconf/ctl/src/test/java/org/onosproject/netconf/ctl/NetconfSessionImplTest.java
index 2dcb14f..86822a6 100644
--- a/protocols/netconf/ctl/src/test/java/org/onosproject/netconf/ctl/NetconfSessionImplTest.java
+++ b/protocols/netconf/ctl/src/test/java/org/onosproject/netconf/ctl/NetconfSessionImplTest.java
@@ -15,7 +15,9 @@
  */
 package org.onosproject.netconf.ctl;
 
+import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.junit.Assert.assertFalse;
@@ -24,6 +26,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.Callable;
@@ -52,6 +55,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.collect.ImmutableList;
+
 /**
  * Unit tests for NetconfSession.
  *
@@ -86,6 +91,20 @@
                     + "</edit-config>\n"
                     + "</rpc>]]>]]>";
 
+    static final List<String> DEFAULT_CAPABILITIES = ImmutableList.<String>builder()
+            .add("urn:ietf:params:netconf:base:1.0")
+            .add("urn:ietf:params:netconf:base:1.1")
+            .add("urn:ietf:params:netconf:capability:writable-running:1.0")
+            .add("urn:ietf:params:netconf:capability:candidate:1.0")
+            .add("urn:ietf:params:netconf:capability:startup:1.0")
+            .add("urn:ietf:params:netconf:capability:rollback-on-error:1.0")
+            .add("urn:ietf:params:netconf:capability:interleave:1.0")
+            .add("urn:ietf:params:netconf:capability:notification:1.0")
+            .add("urn:ietf:params:netconf:capability:validate:1.0")
+            .add("urn:ietf:params:netconf:capability:validate:1.1")
+            .build();
+
+
     private static NetconfSession session1;
     private static NetconfSession session2;
     private static SshServer sshServerNetconf;
@@ -93,7 +112,7 @@
     @BeforeClass
     public static void setUp() throws Exception {
         sshServerNetconf = SshServer.setUpDefaultServer();
-        List<NamedFactory<UserAuth>> userAuthFactories = new ArrayList<NamedFactory<UserAuth>>();
+        List<NamedFactory<UserAuth>> userAuthFactories = new ArrayList<>();
         userAuthFactories.add(new UserAuthPassword.Factory());
         sshServerNetconf.setUserAuthFactories(userAuthFactories);
         sshServerNetconf.setPasswordAuthenticator(
@@ -120,11 +139,13 @@
         log.info("Started NETCONF Session {} with test SSHD server in Unit Test", session1.getSessionId());
         assertTrue("Incorrect sessionId", !session1.getSessionId().equalsIgnoreCase("-1"));
         assertTrue("Incorrect sessionId", !session1.getSessionId().equalsIgnoreCase("0"));
+        assertThat(session1.getDeviceCapabilitiesSet(), containsInAnyOrder(DEFAULT_CAPABILITIES.toArray()));
 
         session2 = new NetconfSessionImpl(deviceInfo);
         log.info("Started NETCONF Session {} with test SSHD server in Unit Test", session2.getSessionId());
         assertTrue("Incorrect sessionId", !session2.getSessionId().equalsIgnoreCase("-1"));
         assertTrue("Incorrect sessionId", !session2.getSessionId().equalsIgnoreCase("0"));
+        assertThat(session2.getDeviceCapabilitiesSet(), containsInAnyOrder(DEFAULT_CAPABILITIES.toArray()));
     }
 
     @AfterClass
@@ -262,8 +283,8 @@
         NCCopyConfigCallable testCopyConfig1 = new NCCopyConfigCallable(session1, RUNNING, "candidate");
         NCCopyConfigCallable testCopyConfig2 = new NCCopyConfigCallable(session1, RUNNING, "startup");
 
-        FutureTask<Boolean> futureCopyConfig1 = new FutureTask<Boolean>(testCopyConfig1);
-        FutureTask<Boolean> futureCopyConfig2 = new FutureTask<Boolean>(testCopyConfig2);
+        FutureTask<Boolean> futureCopyConfig1 = new FutureTask<>(testCopyConfig1);
+        FutureTask<Boolean> futureCopyConfig2 = new FutureTask<>(testCopyConfig2);
 
         ExecutorService executor = Executors.newFixedThreadPool(2);
         log.info("Starting concurrent execution of copy-config through same session");
@@ -288,8 +309,8 @@
         NCCopyConfigCallable testCopySession1 = new NCCopyConfigCallable(session1, RUNNING, "candidate");
         NCCopyConfigCallable testCopySession2 = new NCCopyConfigCallable(session2, RUNNING, "candidate");
 
-        FutureTask<Boolean> futureCopySession1 = new FutureTask<Boolean>(testCopySession1);
-        FutureTask<Boolean> futureCopySession2 = new FutureTask<Boolean>(testCopySession2);
+        FutureTask<Boolean> futureCopySession1 = new FutureTask<>(testCopySession1);
+        FutureTask<Boolean> futureCopySession2 = new FutureTask<>(testCopySession2);
 
         ExecutorService executor = Executors.newFixedThreadPool(2);
         log.info("Starting concurrent execution of copy-config through 2 different sessions");
@@ -311,20 +332,17 @@
 
 
     public static String getTestHelloReply(Optional<Long> sessionId) {
+        return getTestHelloReply(DEFAULT_CAPABILITIES, sessionId);
+    }
+
+    public static String getTestHelloReply(Collection<String> capabilities, Optional<Long> sessionId) {
         StringBuffer sb = new StringBuffer();
 
         sb.append("<hello xmlns=\"urn:ietf:params:xml:ns:netconf:base:1.0\">");
         sb.append("<capabilities>");
-        sb.append("<capability>urn:ietf:params:netconf:base:1.0</capability>");
-        sb.append("<capability>urn:ietf:params:netconf:base:1.1</capability>");
-        sb.append("<capability>urn:ietf:params:netconf:capability:writable-running:1.0</capability>");
-        sb.append("<capability>urn:ietf:params:netconf:capability:candidate:1.0</capability>");
-        sb.append("<capability>urn:ietf:params:netconf:capability:startup:1.0</capability>");
-        sb.append("<capability>urn:ietf:params:netconf:capability:rollback-on-error:1.0</capability>");
-        sb.append("<capability>urn:ietf:params:netconf:capability:interleave:1.0</capability>");
-        sb.append("<capability>urn:ietf:params:netconf:capability:notification:1.0</capability>");
-        sb.append("<capability>urn:ietf:params:netconf:capability:validate:1.0</capability>");
-        sb.append("<capability>urn:ietf:params:netconf:capability:validate:1.1</capability>");
+        capabilities.forEach(capability -> {
+            sb.append("<capability>").append(capability).append("</capability>");
+        });
         sb.append("</capabilities>");
         if (sessionId.isPresent()) {
             sb.append("<session-id>");