Fix identity-ref value parsing/serialization
[yangtools.git] / data / yang-data-util / src / main / java / org / opendaylight / yangtools / yang / data / util / AbstractStringInstanceIdentifierCodec.java
index f59236ba90c4f70c396c6b1f343850f3e7d48255..a89ae431a80f5e54267618e893039a8df3fc4cd4 100644 (file)
@@ -11,7 +11,8 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.annotations.Beta;
-import java.util.Map.Entry;
+import com.google.common.escape.Escaper;
+import com.google.common.escape.Escapers;
 import javax.xml.XMLConstants;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
@@ -20,7 +21,6 @@ import org.opendaylight.yangtools.yang.common.QNameModule;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
 import org.opendaylight.yangtools.yang.data.api.codec.InstanceIdentifierCodec;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.util.LeafrefResolver;
@@ -32,12 +32,20 @@ import org.opendaylight.yangtools.yang.model.util.LeafrefResolver;
 @Beta
 public abstract class AbstractStringInstanceIdentifierCodec extends AbstractNamespaceCodec<YangInstanceIdentifier>
         implements InstanceIdentifierCodec<String> {
+    // Escaper as per https://www.rfc-editor.org/rfc/rfc7950#section-6.1.3
+    private static final Escaper DQUOT_ESCAPER = Escapers.builder()
+        .addEscape('\n', "\\n")
+        .addEscape('\t', "\\t")
+        .addEscape('"', "\\\"")
+        .addEscape('\\', "\\\\")
+        .build();
+
     @Override
     protected final String serializeImpl(final YangInstanceIdentifier data) {
         final StringBuilder sb = new StringBuilder();
         DataSchemaContextNode<?> current = getDataContextTree().getRoot();
         QNameModule lastModule = null;
-        for (PathArgument arg : data.getPathArguments()) {
+        for (var arg : data.getPathArguments()) {
             current = current.getChild(arg);
             checkArgument(current != null, "Invalid input %s: schema for argument %s (after %s) not found", data, arg,
                     sb);
@@ -53,23 +61,63 @@ public abstract class AbstractStringInstanceIdentifierCodec extends AbstractName
                 continue;
             }
 
-            final QName qname = arg.getNodeType();
+            final var qname = arg.getNodeType();
             sb.append('/');
             appendQName(sb, qname, lastModule);
             lastModule = qname.getModule();
 
-            if (arg instanceof NodeIdentifierWithPredicates) {
-                for (Entry<QName, Object> entry : ((NodeIdentifierWithPredicates) arg).entrySet()) {
-                    appendQName(sb.append('['), entry.getKey(), lastModule);
-                    sb.append("='").append(String.valueOf(entry.getValue())).append("']");
+            if (arg instanceof NodeIdentifierWithPredicates nip) {
+                for (var entry : nip.entrySet()) {
+                    final var keyName = entry.getKey();
+                    appendQName(sb.append('['), keyName, lastModule).append('=');
+                    appendValue(sb, keyName.getModule(), entry.getValue()).append(']');
                 }
-            } else if (arg instanceof NodeWithValue) {
-                sb.append("[.='").append(((NodeWithValue<?>) arg).getValue()).append("']");
+            } else if (arg instanceof NodeWithValue<?> val) {
+                appendValue(sb.append("[.="), lastModule, val.getValue()).append(']');
             }
         }
         return sb.toString();
     }
 
+    private StringBuilder appendValue(final StringBuilder sb, final QNameModule currentModule,
+            final Object value) {
+        if (value instanceof QName qname) {
+            // QName implies identity-ref, which can never be escaped
+            return appendQName(sb.append('\''), qname, currentModule).append('\'');
+        }
+
+        final var str = String.valueOf(value);
+
+        // We have two specifications here: Section 6.1.3 of both RFC6020 and RFC7950:
+        //
+        // RFC6020 Section 6.1.3:
+        //        If a string contains any space or tab characters, a semicolon (";"),
+        //        braces ("{" or "}"), or comment sequences ("//", "/*", or "*/"), then
+        //        it MUST be enclosed within double or single quotes.
+        //
+        // RFC7950 Section 6.1.3:
+        //        An unquoted string is any sequence of characters that does not
+        //        contain any space, tab, carriage return, or line feed characters, a
+        //        single or double quote character, a semicolon (";"), braces ("{" or
+        //        "}"), or comment sequences ("//", "/*", or "*/").
+        //
+        // Plus the common part:
+        //        A single-quoted string (enclosed within ' ') preserves each character
+        //        within the quotes.  A single quote character cannot occur in a
+        //        single-quoted string, even when preceded by a backslash.
+        //
+        // Unquoted strings are not interesting, as we are embedding the value in a string, not a YANG document, hence
+        // we have to use quotes. Single-quoted case is simpler, as it does not involve any escaping. The only case
+        // where we cannot use it is when the value itself has a single-quote in itself -- then we call back to
+        // double-quoting.
+
+        return str.indexOf('\'') == -1
+            // No escaping needed, use single quotes
+            ? sb.append('\'').append(str).append('\'')
+            // Escaping needed: use double quotes
+            : sb.append('"').append(DQUOT_ESCAPER.escape(str)).append('"');
+    }
+
     /**
      * Returns DataSchemaContextTree associated with SchemaContext for which
      * serialization / deserialization occurs.
@@ -97,9 +145,8 @@ public abstract class AbstractStringInstanceIdentifierCodec extends AbstractName
 
     @Override
     protected final YangInstanceIdentifier deserializeImpl(final String data) {
-        XpathStringParsingPathArgumentBuilder builder = new XpathStringParsingPathArgumentBuilder(this,
-            requireNonNull(data));
-        return YangInstanceIdentifier.create(builder.build());
+        return YangInstanceIdentifier.create(
+            new XpathStringParsingPathArgumentBuilder(this, requireNonNull(data)).build());
     }
 
     /**