Add ApiPath.toString() 28/109028/3
authorRobert Varga <robert.varga@pantheon.tech>
Mon, 20 Nov 2023 10:21:46 +0000 (11:21 +0100)
committerRobert Varga <nite@hq.sk>
Mon, 20 Nov 2023 15:48:23 +0000 (15:48 +0000)
We are about to push ApiPath deeper into stack. This requires reporting
ApiPath values to end user -- for which we need a toString() method.

This rehosts PERCENT_ESCAPER, as we really need it to emit key values.

JIRA: NETCONF-1157
Change-Id: Ia602dacc78eb9f6b0edd9801998dcc5bd60817ea
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
protocol/restconf-api/src/main/java/org/opendaylight/restconf/api/ApiPath.java
protocol/restconf-api/src/test/java/org/opendaylight/restconf/api/ApiPathTest.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/YangInstanceIdentifierSerializer.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/utils/parser/YangInstanceIdentifierSerializerTest.java

index a88d37574197bb688689e54cddc361bf339a6f98..8157a3296fb262134e1e864f35f41d22744cdaa8 100644 (file)
@@ -13,7 +13,10 @@ import static java.util.Objects.requireNonNull;
 import com.google.common.base.MoreObjects;
 import com.google.common.base.MoreObjects.ToStringHelper;
 import com.google.common.collect.ImmutableList;
+import com.google.common.escape.Escaper;
+import com.google.common.escape.Escapers;
 import java.text.ParseException;
+import java.util.HexFormat;
 import java.util.Objects;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
@@ -67,6 +70,13 @@ public final class ApiPath implements Immutable {
         ToStringHelper addToStringAttributes(final ToStringHelper helper) {
             return helper.add("module", module).add("identifier", identifier);
         }
+
+        void appendTo(final StringBuilder sb) {
+            if (module != null) {
+                sb.append(module).append(':');
+            }
+            sb.append(identifier.getLocalName());
+        }
     }
 
     /**
@@ -118,6 +128,40 @@ public final class ApiPath implements Immutable {
         ToStringHelper addToStringAttributes(final ToStringHelper helper) {
             return super.addToStringAttributes(helper).add("keyValues", keyValues);
         }
+
+        @Override
+        void appendTo(final StringBuilder sb) {
+            super.appendTo(sb);
+            sb.append('=');
+            final var it = keyValues.iterator();
+            while (true) {
+                sb.append(PERCENT_ESCAPER.escape(it.next()));
+                if (it.hasNext()) {
+                    sb.append(',');
+                } else {
+                    break;
+                }
+            }
+        }
+    }
+
+    // Escaper based on RFC8040-requirement to percent-encode reserved characters, as defined in
+    // https://tools.ietf.org/html/rfc3986#section-2.2
+    public static final Escaper PERCENT_ESCAPER;
+
+    static {
+        final var hexFormat = HexFormat.of().withUpperCase();
+        final var builder = Escapers.builder();
+        for (char ch : new char[] {
+            // Reserved characters as per https://tools.ietf.org/html/rfc3986#section-2.2
+            ':', '/', '?', '#', '[', ']', '@',
+            '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=',
+            // FIXME: this space should not be here, but that was a day-0 bug and we have asserts on this
+            ' '
+        }) {
+            builder.addEscape(ch, "%" + hexFormat.toHighHexDigit(ch) + hexFormat.toLowHexDigit(ch));
+        }
+        PERCENT_ESCAPER = builder.build();
     }
 
     private static final ApiPath EMPTY = new ApiPath(ImmutableList.of());
@@ -206,6 +250,24 @@ public final class ApiPath implements Immutable {
         return obj == this || obj instanceof ApiPath other && steps.equals(other.steps());
     }
 
+    @Override
+    public String toString() {
+        if (steps.isEmpty()) {
+            return "";
+        }
+        final var sb = new StringBuilder();
+        final var it = steps.iterator();
+        while (true) {
+            it.next().appendTo(sb);
+            if (it.hasNext()) {
+                sb.append('/');
+            } else {
+                break;
+            }
+        }
+        return sb.toString();
+    }
+
     private static ApiPath parseString(final ApiPathParser parser, final String str) throws ParseException {
         final var steps = parser.parseSteps(str);
         return steps.isEmpty() ? EMPTY : new ApiPath(steps);
index d61028fc8100499bc4ea9eb80dded2ecb7e7bab3..eb34ab2abca680ddef1ee572ad72536afbb05377 100644 (file)
@@ -27,7 +27,7 @@ class ApiPathTest {
 
     @Test
     void testEmpty() {
-        assertEquals(List.of(), parse("/"));
+        assertEquals(List.of(), parse("").steps());
     }
 
     @Test
@@ -46,35 +46,50 @@ class ApiPathTest {
 
     @Test
     void testExample1() {
-        final var path = parse("/example-top:top/list1=key1,key2,key3/list2=key4,key5/X");
-        assertEquals(4, path.size());
-        assertApiIdentifier(path.get(0), "example-top", "top");
-        assertListInstance(path.get(1), null, "list1", "key1", "key2", "key3");
-        assertListInstance(path.get(2), null, "list2", "key4", "key5");
-        assertApiIdentifier(path.get(3), null, "X");
+        final var str = "example-top:top/list1=key1,key2,key3/list2=key4,key5/X";
+        final var path = parse(str);
+        assertEquals(str, path.toString());
+
+        final var steps = path.steps();
+        assertEquals(4, steps.size());
+        assertApiIdentifier(steps.get(0), "example-top", "top");
+        assertListInstance(steps.get(1), null, "list1", "key1", "key2", "key3");
+        assertListInstance(steps.get(2), null, "list2", "key4", "key5");
+        assertApiIdentifier(steps.get(3), null, "X");
     }
 
     @Test
     void testExample2() {
-        final var path = parse("/example-top:top/Y=instance-value");
-        assertEquals(2, path.size());
-        assertApiIdentifier(path.get(0), "example-top", "top");
-        assertListInstance(path.get(1), null, "Y", "instance-value");
+        final var str = "example-top:top/Y=instance-value";
+        final var path = parse(str);
+        assertEquals(str, path.toString());
+
+        final var steps = path.steps();
+        assertEquals(2, steps.size());
+        assertApiIdentifier(steps.get(0), "example-top", "top");
+        assertListInstance(steps.get(1), null, "Y", "instance-value");
     }
 
     @Test
     void testExample3() {
-        final var path = parse("/example-top:top/list1=%2C%27\"%3A\"%20%2F,,foo");
-        assertEquals(2, path.size());
-        assertApiIdentifier(path.get(0), "example-top", "top");
-        assertListInstance(path.get(1), null, "list1", ",'\":\" /", "", "foo");
+        final var str = "example-top:top/list1=%2C%27\"%3A\"%20%2F,,foo";
+        final var path = parse(str);
+        assertEquals(str, path.toString());
+
+        final var steps = path.steps();
+        assertEquals(2, steps.size());
+        assertApiIdentifier(steps.get(0), "example-top", "top");
+        assertListInstance(steps.get(1), null, "list1", ",'\":\" /", "", "foo");
     }
 
     @Test
     void testEscapedColon() {
-        final var path = parse("/foo%3Afoo");
-        assertEquals(1, path.size());
-        assertApiIdentifier(path.get(0), "foo", "foo");
+        final var path = parse("foo%3Afoo");
+        assertEquals("foo:foo", path.toString());
+
+        final var steps = path.steps();
+        assertEquals(1, steps.size());
+        assertApiIdentifier(steps.get(0), "foo", "foo");
     }
 
     @Test
@@ -98,6 +113,35 @@ class ApiPathTest {
         assertEquals(8, ex.getErrorOffset());
     }
 
+    /**
+     * Test to verify if all reserved characters according to rfc3986 are considered by serializer implementation to
+     * be percent encoded.
+     */
+    @Test
+    void verifyReservedCharactersTest() {
+        final char[] genDelims = { ':', '/', '?', '#', '[', ']', '@' };
+        final char[] subDelims = { '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' };
+
+        for (final char ch : genDelims) {
+            assertPercentEncoded(ch);
+        }
+
+        for (final char ch : subDelims) {
+            assertPercentEncoded(ch);
+        }
+    }
+
+    @Test
+    void testEmptyToString() {
+        assertEquals("", ApiPath.empty().toString());
+    }
+
+    private static void assertPercentEncoded(final char ch) {
+        final var str = ApiPath.PERCENT_ESCAPER.escape(String.valueOf(ch));
+        assertEquals(3, str.length());
+        assertEquals('%', str.charAt(0));
+    }
+
     private static void assertApiIdentifier(final Step step, final String module, final String identifier) {
         assertInstanceOf(ApiIdentifier.class, step);
         assertEquals(module, step.module());
@@ -112,12 +156,11 @@ class ApiPathTest {
         assertEquals(List.of(keyValues), listInstance.keyValues());
     }
 
-    private static List<Step> parse(final String str) {
-        final String toParse = str.substring(1);
+    private static ApiPath parse(final String str) {
         try {
-            return ApiPath.parse(toParse).steps();
+            return ApiPath.parse(str);
         } catch (ParseException e) {
-            throw new AssertionError("Failed to parse \"" + toParse + "\"", e);
+            throw new AssertionError("Failed to parse \"" + str + "\"", e);
         }
     }
 }
index fb9d2a52c8c92a6f48df0293cfe82c3a6e194fd2..a6cd8a58f58f556cf2b2f09eea8abc18c75dd0e5 100644 (file)
@@ -7,12 +7,9 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.utils.parser;
 
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.escape.Escaper;
-import com.google.common.escape.Escapers;
-import java.util.HexFormat;
 import java.util.Map.Entry;
 import java.util.Set;
+import org.opendaylight.restconf.api.ApiPath;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
@@ -33,25 +30,6 @@ import org.opendaylight.yangtools.yang.model.api.SchemaContext;
  * Serializer for {@link YangInstanceIdentifier} to {@link String} for restconf.
  */
 public final class YangInstanceIdentifierSerializer {
-    // Escaper based on RFC8040-requirement to percent-encode reserved characters, as defined in
-    // https://tools.ietf.org/html/rfc3986#section-2.2
-    @VisibleForTesting
-    static final Escaper PERCENT_ESCAPER;
-
-    static {
-        final var hexFormat = HexFormat.of().withUpperCase();
-        final var builder = Escapers.builder();
-        for (char ch : new char[] {
-            // Reserved characters as per https://tools.ietf.org/html/rfc3986#section-2.2
-            ':', '/', '?', '#', '[', ']', '@',
-            '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=',
-            // FIXME: this space should not be here, but that was a day-0 bug and we have asserts on this
-            ' '
-        }) {
-            builder.addEscape(ch, "%" + hexFormat.toHighHexDigit(ch) + hexFormat.toLowHexDigit(ch));
-        }
-        PERCENT_ESCAPER = builder.build();
-    }
 
     private YangInstanceIdentifierSerializer() {
         // Hidden on purpose
@@ -123,7 +101,7 @@ public final class YangInstanceIdentifierSerializer {
 
         // FIXME: this is quite fishy
         final var str = String.valueOf(value);
-        path.append(PERCENT_ESCAPER.escape(str));
+        path.append(ApiPath.PERCENT_ESCAPER.escape(str));
     }
 
     private static void prepareNodeWithPredicates(final StringBuilder path, final Set<Entry<QName, Object>> entries) {
@@ -135,7 +113,7 @@ public final class YangInstanceIdentifierSerializer {
         while (iterator.hasNext()) {
             // FIXME: this is quite fishy
             final var str = String.valueOf(iterator.next().getValue());
-            path.append(PERCENT_ESCAPER.escape(str));
+            path.append(ApiPath.PERCENT_ESCAPER.escape(str));
             if (iterator.hasNext()) {
                 path.append(',');
             }
index 13ab3d9b97c29e5573673da8f1d2797cafef98c4..2d7e15716098cc265bfef5da921975ffd68e5977 100644 (file)
@@ -242,30 +242,6 @@ public class YangInstanceIdentifierSerializerTest {
             () -> YangInstanceIdentifierSerializer.create(SCHEMA_CONTEXT, data));
     }
 
-    /**
-     * Test to verify if all reserved characters according to rfc3986 are considered by serializer implementation to
-     * be percent encoded.
-     */
-    @Test
-    public void verifyReservedCharactersTest() {
-        final char[] genDelims = { ':', '/', '?', '#', '[', ']', '@' };
-        final char[] subDelims = { '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=' };
-
-        for (final char ch : genDelims) {
-            assertPercentEncoded(ch);
-        }
-
-        for (final char ch : subDelims) {
-            assertPercentEncoded(ch);
-        }
-    }
-
-    private static void assertPercentEncoded(final char ch) {
-        final var str = YangInstanceIdentifierSerializer.PERCENT_ESCAPER.escape(String.valueOf(ch));
-        assertEquals(3, str.length());
-        assertEquals('%', str.charAt(0));
-    }
-
     /**
      * Test if URIs with percent encoded characters are all correctly serialized.
      */