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;
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());
+ }
}
/**
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());
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);
@Test
void testEmpty() {
- assertEquals(List.of(), parse("/"));
+ assertEquals(List.of(), parse("").steps());
}
@Test
@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
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());
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);
}
}
}
*/
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;
* 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
// 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) {
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(',');
}