ResourceId to instance-identifier string converter methods

part of ONOS-7503

Change-Id: I5c0b0c0c38f51ea1a94208c0b7cb9d4be1db060f
diff --git a/apps/config/BUCK b/apps/config/BUCK
index 7b2a79b..dac73d6 100644
--- a/apps/config/BUCK
+++ b/apps/config/BUCK
@@ -9,6 +9,7 @@
     '//lib:onos-yang-model',
     '//core/store/serializers:onos-core-serializers',
     '//cli:onos-cli',
+    '//lib:commons-text',
 ]
 
 osgi_jar_with_tests (
diff --git a/apps/config/pom.xml b/apps/config/pom.xml
index bf6844a..ac06548 100644
--- a/apps/config/pom.xml
+++ b/apps/config/pom.xml
@@ -25,7 +25,7 @@
     <modelVersion>4.0.0</modelVersion>
     <artifactId>onos-apps-config</artifactId>
     <packaging>bundle</packaging>
-    <description>Dynamic Config App6</description>
+    <description>Dynamic Config App</description>
     <properties>
         <onos.app.name>org.onosproject.configapp</onos.app.name>
         <onos.app.category>Utility</onos.app.category>
@@ -71,8 +71,12 @@
             <artifactId>onlab-junit</artifactId>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-text</artifactId>
+        </dependency>
+
     </dependencies>
 
-
-
 </project>
diff --git a/apps/config/src/main/java/org/onosproject/d/config/DeviceResourceIds.java b/apps/config/src/main/java/org/onosproject/d/config/DeviceResourceIds.java
index 358b400..6e517a0 100644
--- a/apps/config/src/main/java/org/onosproject/d/config/DeviceResourceIds.java
+++ b/apps/config/src/main/java/org/onosproject/d/config/DeviceResourceIds.java
@@ -44,6 +44,7 @@
      */
     public static final String DCS_NAMESPACE = "org.onosproject.dcs";
 
+    // FIXME '/' is problematic name from RFC 7950/7951 point of view
     /**
      * SchemaId name for root node.
      */
diff --git a/apps/config/src/main/java/org/onosproject/d/config/ResourceIds.java b/apps/config/src/main/java/org/onosproject/d/config/ResourceIds.java
index 95c8276..bff42b5 100644
--- a/apps/config/src/main/java/org/onosproject/d/config/ResourceIds.java
+++ b/apps/config/src/main/java/org/onosproject/d/config/ResourceIds.java
@@ -16,18 +16,28 @@
 package org.onosproject.d.config;
 
 import com.google.common.annotations.Beta;
+import org.apache.commons.text.StringEscapeUtils;
 import org.onosproject.yang.model.DataNode;
 import org.onosproject.yang.model.KeyLeaf;
 import org.onosproject.yang.model.LeafListKey;
+import org.onosproject.yang.model.LeafListKey.LeafListKeyBuilder;
+import org.onosproject.yang.model.ListKey.ListKeyBuilder;
 import org.onosproject.yang.model.ListKey;
 import org.onosproject.yang.model.NodeKey;
 import org.onosproject.yang.model.ResourceId;
+import org.onosproject.yang.model.ResourceId.Builder;
 import org.onosproject.yang.model.SchemaId;
 import org.slf4j.Logger;
 
+import java.util.Arrays;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
+import static com.google.common.base.MoreObjects.firstNonNull;
 import static com.google.common.base.Preconditions.checkArgument;
 import static org.slf4j.LoggerFactory.getLogger;
 
@@ -40,7 +50,15 @@
     private static final Logger log = getLogger(ResourceIds.class);
 
     /**
+     * Root resource Id used by Yang Runtime.
+     * (name: {@code "/"}, nameSpace: {@code null})
+     */
+    public static final ResourceId YRS_ROOT =
+            ResourceId.builder().addBranchPointSchema("/", null).build();
+    /**
      * Absolute ResourceId pointing at root node.
+     * (name: {@link DeviceResourceIds#ROOT_NAME},
+     *  nameSpace: {@link DeviceResourceIds#DCS_NAMESPACE})
      */
     public static final ResourceId ROOT_ID = ResourceId.builder()
             .addBranchPointSchema(DeviceResourceIds.ROOT_NAME,
@@ -186,9 +204,239 @@
                 prefix.nodeKeys().equals(child.nodeKeys().subList(0, prefix.nodeKeys().size()));
     }
 
+    /**
+     * Tests if {@code path} starts with {@link DeviceResourceIds#ROOT_NAME}.
+     *
+     * @param path to test
+     * @return true if {@code path} starts with {@link DeviceResourceIds#ROOT_NAME}
+     */
     public static boolean startsWithRootNode(ResourceId path) {
         return !path.nodeKeys().isEmpty() &&
                 DeviceResourceIds.ROOT_NAME.equals(path.nodeKeys().get(0).schemaId().name());
     }
 
+
+    /**
+     * Converts node-identifier element to a NodeKey.
+     *
+     * @param id to parse (node-identifier fragment between '/')
+     * @return NodeKey (warning: returned namespace can be null, which should be interpreted as
+     *         same as parent)
+     */
+    private static NodeKey toNodeKey(String id) {
+        Pattern nodeId = Pattern.compile("^((?<prefix>[a-zA-Z_](?:[a-zA-Z0-9_.\\-]*)):)?"
+                + "(?<identifier>[a-zA-Z_](?:[a-zA-Z0-9_.-]*))");
+
+        Matcher nidMatcher = nodeId.matcher(id);
+        if (!nidMatcher.find()) {
+            throw new IllegalArgumentException("node identifier not found in " + id);
+        }
+
+        String prefix = nidMatcher.group("prefix");
+        String identifier = nidMatcher.group("identifier");
+
+        // key and val pattern is a bit loosened from RFC for simplicity
+        Pattern preds = Pattern.compile("\\[\\s*(?<key>[^=\\s]+)\\s*=\\s*\\\"(?<val>[^\\]]+)\\\"\\s*\\]");
+        Matcher predMatcher = preds.matcher(id);
+        predMatcher.region(nidMatcher.end(), id.length());
+        LeafListKeyBuilder llkb = null;
+        ListKeyBuilder llb = null;
+        while (predMatcher.find()) {
+            String key = predMatcher.group("key");
+            String val = predMatcher.group("val");
+            if (key.equals(".")) {
+                // LeafList
+                if (llkb == null) {
+                    llkb = new LeafListKeyBuilder();
+                }
+                llkb.schemaId(identifier, prefix)
+                .value(val);
+            } else {
+                // ListKey
+                if (llb == null) {
+                    llb = new ListKeyBuilder();
+                }
+                llb.schemaId(identifier, prefix);
+                Matcher m = nodeId.matcher(key);
+                m.matches();
+                llb.addKeyLeaf(m.group("identifier"), m.group("prefix"), val);
+            }
+        }
+        if (llkb != null) {
+            return llkb.build();
+        } else if (llb != null) {
+            return llb.build();
+        } else {
+            return NodeKey.builder().schemaId(identifier, prefix).build();
+        }
+    }
+
+
+    /**
+     * Add {@link #YRS_ROOT} prefix if not already.
+     *
+     * @param rid resource id
+     * @return ResourceId starting from {@link #YRS_ROOT}
+     */
+    public static ResourceId prefixYrsRoot(ResourceId rid) {
+        if (rid == null) {
+            return YRS_ROOT;
+        }
+
+        if (isPrefix(YRS_ROOT, rid)) {
+            return rid;
+        }
+
+        if (isPrefix(ROOT_ID, rid)) {
+            return concat(YRS_ROOT, relativize(ROOT_ID, rid));
+        }
+
+        return concat(YRS_ROOT, rid);
+    }
+
+    /**
+     * Add {@link #ROOT_ID} prefix if not already.
+     *
+     * @param rid resource id
+     * @return ResourceId starting from {@link #ROOT_ID}
+     */
+    public static ResourceId prefixDcsRoot(ResourceId rid) {
+        if (rid == null) {
+            return ROOT_ID;
+        }
+
+        if (isPrefix(ROOT_ID, rid)) {
+            return rid;
+        }
+
+        // test and replace YangRuntime root?
+        if (isPrefix(YRS_ROOT, rid)) {
+            return concat(ROOT_ID, relativize(YRS_ROOT, rid));
+        }
+
+        return concat(ROOT_ID, rid);
+    }
+
+
+    /**
+     * Converts instance-identifier String into a ResourceId.
+     *
+     * @param input instance-identifier
+     * @return Corresponding ResourceId relative to root or null if {@code iid} is '/'
+     * Returned ResourceId will not have root NodeKey in it's path.
+     * consider using {@link #prefixDcsRoot(ResourceId)},
+     * {@link #prefixYrsRoot(ResourceId)} to add them.
+     */
+    public static ResourceId fromInstanceIdentifier(String input) {
+
+        String[] nodes = input.split("/");
+        List<NodeKey> nodeKeys = Arrays.stream(nodes)
+            .filter(s -> !s.isEmpty())
+            .map(ResourceIds::toNodeKey)
+            .collect(Collectors.toList());
+
+        if (nodeKeys.isEmpty()) {
+            return null;
+        }
+
+        Builder builder = ResourceId.builder();
+
+        // fill-in null (=inherit from parent) nameSpace
+        String lastNamespace = null;
+        for (NodeKey nodeKey : nodeKeys) {
+            if (nodeKey.schemaId().namespace() != null) {
+                lastNamespace = nodeKey.schemaId().namespace();
+            }
+            if (nodeKey instanceof LeafListKey) {
+                builder.addLeafListBranchPoint(nodeKey.schemaId().name(),
+                                               firstNonNull(nodeKey.schemaId().namespace(), lastNamespace),
+                                               ((LeafListKey) nodeKey).value());
+
+            } else if (nodeKey instanceof ListKey) {
+                builder.addBranchPointSchema(nodeKey.schemaId().name(), lastNamespace);
+                for (KeyLeaf kl : ((ListKey) nodeKey).keyLeafs()) {
+                    builder.addKeyLeaf(kl.leafSchema().name(),
+                                       firstNonNull(kl.leafSchema().namespace(), lastNamespace),
+                                       kl.leafValue());
+                }
+            } else {
+                builder.addBranchPointSchema(nodeKey.schemaId().name(), lastNamespace);
+            }
+        }
+        return builder.build();
+    }
+
+
+    /**
+     * Converts ResourceId to instance-identifier.
+     *
+     * @param rid to convert
+     * @return instance-identifier
+     *
+     * @see <a href="https://tools.ietf.org/html/rfc7951#section-6.11">RFC 7951</a>
+     * @see <a href="https://tools.ietf.org/html/rfc7950#section-14">RFC 7950 for ABNF</a>
+     */
+    public static String toInstanceIdentifier(ResourceId rid) {
+        StringBuilder s = new StringBuilder();
+
+        String lastNamespace = null;
+        for (NodeKey nk : rid.nodeKeys()) {
+            if (nk.schemaId().name().equals("/")) {
+                // special handling for root nodeKey: skip it
+                // YANG runtime root: null:/
+                // DCS root: org.onosproject.dcs:/
+                continue;
+            }
+
+            s.append('/');
+
+            if (!Objects.equals(lastNamespace, nk.schemaId().namespace())) {
+                s.append(nk.schemaId().namespace());
+                s.append(':');
+                lastNamespace = nk.schemaId().namespace();
+            }
+            s.append(nk.schemaId().name());
+
+            if (nk instanceof LeafListKey) {
+                LeafListKey llk = (LeafListKey) nk;
+                s.append('[');
+                s.append('.');
+
+                s.append('=');
+
+                s.append('"')
+                 .append(StringEscapeUtils.escapeJson(llk.asString()))
+                 .append('"');
+                s.append(']');
+
+            } else if (nk instanceof ListKey) {
+                ListKey lk = (ListKey) nk;
+
+                for (KeyLeaf kl : lk.keyLeafs()) {
+                    s.append('[');
+
+                    if (!Objects.equals(kl.leafSchema().namespace(), lastNamespace)) {
+                        s.append(kl.leafSchema().namespace());
+                        s.append(':');
+                    }
+                    s.append(kl.leafSchema().name());
+
+                    s.append('=');
+
+                    s.append('"')
+                     .append(StringEscapeUtils.escapeJson(kl.leafValAsString()))
+                     .append('"');
+                    s.append(']');
+                }
+            } else {
+                // normal NodeKey
+                // nothing to do
+            }
+        }
+        if (s.length() == 0) {
+            return "/";
+        }
+        return s.toString();
+    }
+
 }
diff --git a/apps/config/src/test/java/org/onosproject/d/config/ResourceIdsTest.java b/apps/config/src/test/java/org/onosproject/d/config/ResourceIdsTest.java
index b1c237b..0c9623e 100644
--- a/apps/config/src/test/java/org/onosproject/d/config/ResourceIdsTest.java
+++ b/apps/config/src/test/java/org/onosproject/d/config/ResourceIdsTest.java
@@ -15,10 +15,15 @@
  */
 package org.onosproject.d.config;
 
+import static org.hamcrest.Matchers.*;
 import static org.junit.Assert.*;
 import static org.onosproject.d.config.DeviceResourceIds.DCS_NAMESPACE;
+import static org.onosproject.d.config.DeviceResourceIds.DEVICES_ID;
+import static org.onosproject.d.config.DeviceResourceIds.toResourceId;
+import static org.onosproject.d.config.ResourceIds.fromInstanceIdentifier;
 
 import org.junit.Test;
+import org.onosproject.net.DeviceId;
 import org.onosproject.yang.model.ResourceId;
 
 public class ResourceIdsTest {
@@ -29,6 +34,64 @@
             .build();
 
     @Test
+    public void testFromInstanceIdentifier() {
+
+        ResourceId eth0 = ResourceId.builder()
+                .addBranchPointSchema("interfaces", "ietf-interfaces")
+                .addBranchPointSchema("interface", "ietf-interfaces")
+                .addKeyLeaf("name", "ietf-interfaces", "eth0")
+                .build();
+        assertThat(ResourceIds.fromInstanceIdentifier("/ietf-interfaces:interfaces/interface[name=\"eth0\"]"),
+                   is(eth0));
+
+        assertThat("fromInstanceIdentifier return path relative to virtual root",
+                   ResourceIds.fromInstanceIdentifier("/org.onosproject.dcs:devices"),
+                   is(ResourceIds.relativize(ResourceIds.ROOT_ID, DEVICES_ID)));
+
+        assertThat(ResourceIds.prefixDcsRoot(
+                     ResourceIds.fromInstanceIdentifier("/org.onosproject.dcs:devices")),
+                   is(DEVICES_ID));
+
+        assertThat(ResourceIds.fromInstanceIdentifier("/"),
+                    is(nullValue()));
+
+        DeviceId deviceId = DeviceId.deviceId("test:device-identifier");
+        assertThat(ResourceIds.prefixDcsRoot(
+                 fromInstanceIdentifier("/org.onosproject.dcs:devices/device[device-id=\"test:device-identifier\"]")),
+                   is(toResourceId(deviceId)));
+
+    }
+
+    @Test
+    public void testToInstanceIdentifier() {
+
+        assertThat(ResourceIds.toInstanceIdentifier(ResourceIds.ROOT_ID),
+                   is("/"));
+        assertThat(ResourceIds.toInstanceIdentifier(DEVICES_ID),
+                   is("/org.onosproject.dcs:devices"));
+
+        DeviceId deviceId = DeviceId.deviceId("test:device-identifier");
+        assertThat(ResourceIds.toInstanceIdentifier(toResourceId(deviceId)),
+                   is("/org.onosproject.dcs:devices/device[device-id=\"test:device-identifier\"]"));
+
+        assertThat(ResourceIds.toInstanceIdentifier(ResourceIds.relativize(DEVICES_ID, toResourceId(deviceId))),
+                   is("/org.onosproject.dcs:device[device-id=\"test:device-identifier\"]"));
+
+        ResourceId eth0 = ResourceId.builder()
+                .addBranchPointSchema("interfaces", "ietf-interfaces")
+                .addBranchPointSchema("interface", "ietf-interfaces")
+                .addKeyLeaf("name", "ietf-interfaces", "eth0")
+                .build();
+        assertThat(ResourceIds.toInstanceIdentifier(eth0),
+                   is("/ietf-interfaces:interfaces/interface[name=\"eth0\"]"));
+
+
+        assertThat(ResourceIds.toInstanceIdentifier(ResourceIds.concat(toResourceId(deviceId), eth0)),
+                   is("/org.onosproject.dcs:devices/device[device-id=\"test:device-identifier\"]"
+                           + "/ietf-interfaces:interfaces/interface[name=\"eth0\"]"));
+    }
+
+    @Test
     public void testConcat() {
         ResourceId devices = ResourceId.builder()
             .addBranchPointSchema(DeviceResourceIds.DEVICES_NAME,
diff --git a/features/BUCK b/features/BUCK
index 1434165..be2dd94 100644
--- a/features/BUCK
+++ b/features/BUCK
@@ -6,6 +6,7 @@
     '//lib:atomix',
     '//lib:commons-lang',
     '//lib:commons-lang3',
+    '//lib:commons-text',
     '//lib:commons-configuration',
     '//lib:guava',
     '//lib:netty',
diff --git a/lib/BUCK b/lib/BUCK
index 4f588ae..ddc98b9 100644
--- a/lib/BUCK
+++ b/lib/BUCK
@@ -1,4 +1,4 @@
-# ***** This file was auto-generated at Fri, 23 Feb 2018 16:41:27 GMT. Do not edit this file manually. *****
+# ***** This file was auto-generated at Tue, 27 Feb 2018 19:20:22 GMT. Do not edit this file manually. *****
 # ***** Use onos-lib-gen *****
 
 pass_thru_pom(
@@ -296,6 +296,15 @@
 )
 
 remote_jar (
+  name = 'commons-text',
+  out = 'commons-text-1.2.jar',
+  url = 'mvn:org.apache.commons:commons-text:jar:1.2',
+  sha1 = '74acdec7237f576c4803fff0c1008ab8a3808b2b',
+  maven_coords = 'org.apache.commons:commons-text:1.2',
+  visibility = [ 'PUBLIC' ],
+)
+
+remote_jar (
   name = 'commons-beanutils',
   out = 'commons-beanutils-1.9.2.jar',
   url = 'mvn:commons-beanutils:commons-beanutils:jar:1.9.2',
diff --git a/lib/deps.json b/lib/deps.json
index 4f27a3c..c6d7948 100644
--- a/lib/deps.json
+++ b/lib/deps.json
@@ -126,6 +126,7 @@
     "commons-logging": "mvn:commons-logging:commons-logging:1.2",
     "commons-math3": "mvn:org.apache.commons:commons-math3:3.6.1",
     "commons-pool": "mvn:commons-pool:commons-pool:1.6",
+    "commons-text": "mvn:org.apache.commons:commons-text:1.2",
     "commons-beanutils": "mvn:commons-beanutils:commons-beanutils:1.9.2",
     "concurrent-trees": "mvn:com.googlecode.concurrent-trees:concurrent-trees:2.6.1",
     "easymock": "mvn:org.easymock:easymock:3.4",
diff --git a/lib/pom.xml b/lib/pom.xml
index 01ac45b..de64efb 100644
--- a/lib/pom.xml
+++ b/lib/pom.xml
@@ -174,6 +174,12 @@
             </dependency>
 
             <dependency>
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-text</artifactId>
+                <version>1.2</version>
+            </dependency>
+
+            <dependency>
                 <groupId>io.dropwizard.metrics</groupId>
                 <artifactId>metrics-core</artifactId>
                 <version>3.2.2</version>