Fix String value parsing/serialization
[yangtools.git] / data / yang-data-util / src / main / java / org / opendaylight / yangtools / yang / data / util / AbstractStringInstanceIdentifierCodec.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.data.util;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static java.util.Objects.requireNonNull;
12
13 import com.google.common.annotations.Beta;
14 import com.google.common.escape.Escaper;
15 import com.google.common.escape.Escapers;
16 import javax.xml.XMLConstants;
17 import org.eclipse.jdt.annotation.NonNull;
18 import org.eclipse.jdt.annotation.Nullable;
19 import org.opendaylight.yangtools.yang.common.QName;
20 import org.opendaylight.yangtools.yang.common.QNameModule;
21 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
22 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
24 import org.opendaylight.yangtools.yang.data.api.codec.InstanceIdentifierCodec;
25 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
26 import org.opendaylight.yangtools.yang.model.util.LeafrefResolver;
27
28 /**
29  * Abstract utility class for representations which encode {@link YangInstanceIdentifier} as a
30  * prefix:name tuple. Typical uses are RESTCONF/JSON (module:name) and XML (prefix:name).
31  */
32 @Beta
33 public abstract class AbstractStringInstanceIdentifierCodec extends AbstractNamespaceCodec<YangInstanceIdentifier>
34         implements InstanceIdentifierCodec<String> {
35     // Escaper as per https://www.rfc-editor.org/rfc/rfc7950#section-6.1.3
36     private static final Escaper DQUOT_ESCAPER = Escapers.builder()
37         .addEscape('\n', "\\n")
38         .addEscape('\t', "\\t")
39         .addEscape('"', "\\\"")
40         .addEscape('\\', "\\\\")
41         .build();
42
43     @Override
44     protected final String serializeImpl(final YangInstanceIdentifier data) {
45         final StringBuilder sb = new StringBuilder();
46         DataSchemaContextNode<?> current = getDataContextTree().getRoot();
47         QNameModule lastModule = null;
48         for (var arg : data.getPathArguments()) {
49             current = current.getChild(arg);
50             checkArgument(current != null, "Invalid input %s: schema for argument %s (after %s) not found", data, arg,
51                     sb);
52
53             if (current.isMixin()) {
54                 /*
55                  * XML/YANG instance identifier does not have concept
56                  * of augmentation identifier, or list as whole which
57                  * identifies a mixin (same as the parent element),
58                  * so we can safely ignore it if it is part of path
59                  * (since child node) is identified in same fashion.
60                  */
61                 continue;
62             }
63
64             final var qname = arg.getNodeType();
65             sb.append('/');
66             appendQName(sb, qname, lastModule);
67             lastModule = qname.getModule();
68
69             if (arg instanceof NodeIdentifierWithPredicates nip) {
70                 for (var entry : nip.entrySet()) {
71                     final var keyName = entry.getKey();
72                     appendQName(sb.append('['), keyName, lastModule).append('=');
73                     appendValue(sb, keyName.getModule(), entry.getValue()).append(']');
74                 }
75             } else if (arg instanceof NodeWithValue<?> val) {
76                 appendValue(sb.append("[.="), lastModule, val.getValue()).append(']');
77             }
78         }
79         return sb.toString();
80     }
81
82     private static StringBuilder appendValue(final StringBuilder sb, final QNameModule currentModule,
83             final Object value) {
84         final var str = String.valueOf(value);
85
86         // We have two specifications here: Section 6.1.3 of both RFC6020 and RFC7950:
87         //
88         // RFC6020 Section 6.1.3:
89         //        If a string contains any space or tab characters, a semicolon (";"),
90         //        braces ("{" or "}"), or comment sequences ("//", "/*", or "*/"), then
91         //        it MUST be enclosed within double or single quotes.
92         //
93         // RFC7950 Section 6.1.3:
94         //        An unquoted string is any sequence of characters that does not
95         //        contain any space, tab, carriage return, or line feed characters, a
96         //        single or double quote character, a semicolon (";"), braces ("{" or
97         //        "}"), or comment sequences ("//", "/*", or "*/").
98         //
99         // Plus the common part:
100         //        A single-quoted string (enclosed within ' ') preserves each character
101         //        within the quotes.  A single quote character cannot occur in a
102         //        single-quoted string, even when preceded by a backslash.
103         //
104         // Unquoted strings are not interesting, as we are embedding the value in a string, not a YANG document, hence
105         // we have to use quotes. Single-quoted case is simpler, as it does not involve any escaping. The only case
106         // where we cannot use it is when the value itself has a single-quote in itself -- then we call back to
107         // double-quoting.
108
109         return str.indexOf('\'') == -1
110             // No escaping needed, use single quotes
111             ? sb.append('\'').append(str).append('\'')
112             // Escaping needed: use double quotes
113             : sb.append('"').append(DQUOT_ESCAPER.escape(str)).append('"');
114     }
115
116     /**
117      * Returns DataSchemaContextTree associated with SchemaContext for which
118      * serialization / deserialization occurs.
119      *
120      * <p>
121      * Implementations MUST provide non-null Data Tree context, in order
122      * for correct serialization / deserialization of PathArguments,
123      * since XML representation does not have Augmentation arguments
124      * and does not provide path arguments for cases.
125      *
126      * <p>
127      * This effectively means same input XPath representation of Path Argument
128      * may result in different YangInstanceIdentifiers if models are different
129      * in uses of choices and cases.
130      *
131      * @return DataSchemaContextTree associated with SchemaContext for which
132      *         serialization / deserialization occurs.
133      */
134     protected abstract @NonNull DataSchemaContextTree getDataContextTree();
135
136     protected Object deserializeKeyValue(final DataSchemaNode schemaNode, final LeafrefResolver resolver,
137             final String value) {
138         return value;
139     }
140
141     @Override
142     protected final YangInstanceIdentifier deserializeImpl(final String data) {
143         return YangInstanceIdentifier.create(
144             new XpathStringParsingPathArgumentBuilder(this, requireNonNull(data)).build());
145     }
146
147     /**
148      * Create QName from unprefixed name, potentially taking last QNameModule encountered into account.
149      *
150      * @param lastModule last QNameModule encountered, potentially null
151      * @param localName Local name string
152      * @return A newly-created QName
153      */
154     protected QName createQName(final @Nullable QNameModule lastModule, final String localName) {
155         // This implementation handles both XML encoding, where we follow XML namespace rules and old JSON encoding,
156         // which is the same thing: always encode prefixes
157         return createQName(XMLConstants.DEFAULT_NS_PREFIX, localName);
158     }
159 }