Force subclasses to implement deserializeKeyValue()
[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 java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.Beta;
13 import com.google.common.escape.Escaper;
14 import com.google.common.escape.Escapers;
15 import java.util.Set;
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.data.util.DataSchemaContext.Composite;
26 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
27 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
28 import org.opendaylight.yangtools.yang.model.util.LeafrefResolver;
29
30 /**
31  * Abstract utility class for representations which encode {@link YangInstanceIdentifier} as a
32  * prefix:name tuple. Typical uses are RESTCONF/JSON (module:name) and XML (prefix:name).
33  */
34 @Beta
35 public abstract class AbstractStringInstanceIdentifierCodec extends AbstractNamespaceCodec<YangInstanceIdentifier>
36         implements InstanceIdentifierCodec<String> {
37     // Escaper as per https://www.rfc-editor.org/rfc/rfc7950#section-6.1.3
38     private static final Escaper DQUOT_ESCAPER = Escapers.builder()
39         .addEscape('\n', "\\n")
40         .addEscape('\t', "\\t")
41         .addEscape('"', "\\\"")
42         .addEscape('\\', "\\\\")
43         .build();
44
45     @Override
46     protected final String serializeImpl(final YangInstanceIdentifier data) {
47         final StringBuilder sb = new StringBuilder();
48         DataSchemaContext current = getDataContextTree().getRoot();
49         QNameModule lastModule = null;
50         for (var arg : data.getPathArguments()) {
51             current = current instanceof Composite composite ? composite.childByArg(arg) : null;
52             if (current == null) {
53                 throw new IllegalArgumentException(
54                     "Invalid input %s: schema for argument %s (after \"%s\") not found".formatted(data, arg, sb));
55             }
56
57             if (current instanceof PathMixin) {
58                 /*
59                  * XML/YANG instance identifier does not have concept of augmentation identifier, or list as whole which
60                  * identifies a mixin (same as the parent element), so we can safely ignore it if it is part of path
61                  * (since child node) is identified in same fashion.
62                  */
63                 continue;
64             }
65
66             final var qname = arg.getNodeType();
67             sb.append('/');
68             appendQName(sb, qname, lastModule);
69             lastModule = qname.getModule();
70
71             if (arg instanceof NodeIdentifierWithPredicates nip) {
72                 for (var entry : nip.entrySet()) {
73                     final var keyName = entry.getKey();
74                     appendQName(sb.append('['), keyName, lastModule).append('=');
75                     appendValue(sb, keyName.getModule(), entry.getValue()).append(']');
76                 }
77             } else if (arg instanceof NodeWithValue<?> val) {
78                 appendValue(sb.append("[.="), lastModule, val.getValue()).append(']');
79             }
80         }
81         return sb.toString();
82     }
83
84     private StringBuilder appendValue(final StringBuilder sb, final QNameModule currentModule,
85             final Object value) {
86         if (value instanceof QName qname) {
87             // QName implies identity-ref, which can never be escaped
88             return appendQName(sb.append('\''), qname, currentModule).append('\'');
89         }
90         // FIXME: YANGTOOLS-1426: update once we have a dedicated type
91         if (value instanceof Set<?> bits) {
92             // Set implies bits, which can never be escaped and need to be serialized as space-separated items
93             sb.append('\'');
94
95             final var it = bits.iterator();
96             if (it.hasNext()) {
97                 sb.append(checkBitsItem(it.next()));
98                 while (it.hasNext()) {
99                     sb.append(' ').append(checkBitsItem(it.next()));
100                 }
101             }
102
103             return sb.append('\'');
104         }
105
106         final var str = value instanceof YangInstanceIdentifier id ? serialize(id) : String.valueOf(value);
107
108         // We have two specifications here: Section 6.1.3 of both RFC6020 and RFC7950:
109         //
110         // RFC6020 Section 6.1.3:
111         //        If a string contains any space or tab characters, a semicolon (";"),
112         //        braces ("{" or "}"), or comment sequences ("//", "/*", or "*/"), then
113         //        it MUST be enclosed within double or single quotes.
114         //
115         // RFC7950 Section 6.1.3:
116         //        An unquoted string is any sequence of characters that does not
117         //        contain any space, tab, carriage return, or line feed characters, a
118         //        single or double quote character, a semicolon (";"), braces ("{" or
119         //        "}"), or comment sequences ("//", "/*", or "*/").
120         //
121         // Plus the common part:
122         //        A single-quoted string (enclosed within ' ') preserves each character
123         //        within the quotes.  A single quote character cannot occur in a
124         //        single-quoted string, even when preceded by a backslash.
125         //
126         // Unquoted strings are not interesting, as we are embedding the value in a string, not a YANG document, hence
127         // we have to use quotes. Single-quoted case is simpler, as it does not involve any escaping. The only case
128         // where we cannot use it is when the value itself has a single-quote in itself -- then we call back to
129         // double-quoting.
130
131         return str.indexOf('\'') == -1
132             // No escaping needed, use single quotes
133             ? sb.append('\'').append(str).append('\'')
134             // Escaping needed: use double quotes
135             : sb.append('"').append(DQUOT_ESCAPER.escape(str)).append('"');
136     }
137
138     /**
139      * Returns DataSchemaContextTree associated with SchemaContext for which
140      * serialization / deserialization occurs.
141      *
142      * <p>
143      * Implementations MUST provide non-null Data Tree context, in order
144      * for correct serialization / deserialization of PathArguments,
145      * since XML representation does not have Augmentation arguments
146      * and does not provide path arguments for cases.
147      *
148      * <p>
149      * This effectively means same input XPath representation of Path Argument
150      * may result in different YangInstanceIdentifiers if models are different
151      * in uses of choices and cases.
152      *
153      * @return DataSchemaContextTree associated with SchemaContext for which
154      *         serialization / deserialization occurs.
155      */
156     protected abstract @NonNull DataSchemaContextTree getDataContextTree();
157
158     protected abstract @NonNull Object deserializeKeyValue(@NonNull DataSchemaNode schemaNode,
159         @NonNull LeafrefResolver resolver, String value);
160
161     @Override
162     protected final YangInstanceIdentifier deserializeImpl(final String data) {
163         return YangInstanceIdentifier.of(
164             new XpathStringParsingPathArgumentBuilder(this, requireNonNull(data)).build());
165     }
166
167     /**
168      * Create QName from unprefixed name, potentially taking last QNameModule encountered into account.
169      *
170      * @param lastModule last QNameModule encountered, potentially null
171      * @param localName Local name string
172      * @return A newly-created QName
173      */
174     protected QName createQName(final @Nullable QNameModule lastModule, final String localName) {
175         // This implementation handles both XML encoding, where we follow XML namespace rules and old JSON encoding,
176         // which is the same thing: always encode prefixes
177         return createQName(XMLConstants.DEFAULT_NS_PREFIX, localName);
178     }
179
180     // FIXME: YANGTOOLS-1426: this will not be necessary when we have dedicated bits type
181     private static @NonNull String checkBitsItem(final Object obj) {
182         if (obj instanceof String str) {
183             return str;
184         }
185         throw new IllegalArgumentException("Unexpected bits component " + obj);
186     }
187 }