Fix parsing of union values
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / server / spi / ApiPathNormalizer.java
1 /*
2  * Copyright (c) 2016 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.restconf.server.spi;
9
10 import static com.google.common.base.Verify.verify;
11 import static com.google.common.base.Verify.verifyNotNull;
12 import static java.util.Objects.requireNonNull;
13
14 import com.google.common.base.VerifyException;
15 import com.google.common.collect.ImmutableMap;
16 import java.text.ParseException;
17 import java.util.ArrayList;
18 import java.util.List;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.opendaylight.restconf.api.ApiPath;
22 import org.opendaylight.restconf.api.ApiPath.ListInstance;
23 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
24 import org.opendaylight.restconf.nb.rfc8040.Insert.PointNormalizer;
25 import org.opendaylight.restconf.server.api.DatabindContext;
26 import org.opendaylight.restconf.server.spi.ApiPathNormalizer.OperationPath.Rpc;
27 import org.opendaylight.yangtools.yang.common.ErrorTag;
28 import org.opendaylight.yangtools.yang.common.ErrorType;
29 import org.opendaylight.yangtools.yang.common.QName;
30 import org.opendaylight.yangtools.yang.common.QNameModule;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
32 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
33 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
34 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
35 import org.opendaylight.yangtools.yang.data.impl.codec.TypeDefinitionAwareCodec;
36 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
37 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
38 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
39 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
41 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
43 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.stmt.ActionEffectiveStatement;
45 import org.opendaylight.yangtools.yang.model.api.stmt.IdentityEffectiveStatement;
46 import org.opendaylight.yangtools.yang.model.api.stmt.InputEffectiveStatement;
47 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
48 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
49 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
50 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
51 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
52 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
53 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
54 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
55
56 /**
57  * Deserializer for {@link String} to {@link YangInstanceIdentifier} for restconf.
58  */
59 public final class ApiPathNormalizer implements PointNormalizer {
60     @NonNullByDefault
61     public sealed interface Path {
62
63         Inference inference();
64     }
65
66     @NonNullByDefault
67     public sealed interface InstanceReference extends Path {
68
69         YangInstanceIdentifier instance();
70     }
71
72     @NonNullByDefault
73     public record DataPath(Inference inference, YangInstanceIdentifier instance, DataSchemaContext schema)
74             implements InstanceReference {
75         public DataPath {
76             requireNonNull(inference);
77             requireNonNull(instance);
78             requireNonNull(schema);
79         }
80     }
81
82     @NonNullByDefault
83     public sealed interface OperationPath extends Path {
84
85         InputEffectiveStatement inputStatement();
86
87         record Action(Inference inference, YangInstanceIdentifier instance, ActionEffectiveStatement action)
88                 implements OperationPath, InstanceReference {
89             public Action {
90                 requireNonNull(inference);
91                 requireNonNull(action);
92                 requireNonNull(instance);
93             }
94
95             @Override
96             public InputEffectiveStatement inputStatement() {
97                 return action.input();
98             }
99         }
100
101         record Rpc(Inference inference, RpcEffectiveStatement rpc) implements OperationPath {
102             public Rpc {
103                 requireNonNull(inference);
104                 requireNonNull(rpc);
105             }
106
107             @Override
108             public InputEffectiveStatement inputStatement() {
109                 return rpc.input();
110             }
111         }
112     }
113
114     private final @NonNull EffectiveModelContext modelContext;
115     private final @NonNull DatabindContext databind;
116
117     public ApiPathNormalizer(final DatabindContext databind) {
118         this.databind = requireNonNull(databind);
119         modelContext = databind.modelContext();
120     }
121
122     public @NonNull Path normalizePath(final ApiPath apiPath) {
123         final var it = apiPath.steps().iterator();
124         if (!it.hasNext()) {
125             return new DataPath(Inference.ofDataTreePath(modelContext), YangInstanceIdentifier.of(),
126                 databind.schemaTree().getRoot());
127         }
128
129         // First step is somewhat special:
130         // - it has to contain a module qualifier
131         // - it has to consider RPCs, for which we need SchemaContext
132         //
133         // We therefore peel that first iteration here and not worry about those details in further iterations
134         var step = it.next();
135         final var firstModule = step.module();
136         if (firstModule == null) {
137             throw new RestconfDocumentedException(
138                 "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
139                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
140         }
141
142         var namespace = resolveNamespace(firstModule);
143         var qname = step.identifier().bindTo(namespace);
144
145         // We go through more modern APIs here to get this special out of the way quickly
146         final var optRpc = modelContext.findModuleStatement(namespace).orElseThrow()
147             .findSchemaTreeNode(RpcEffectiveStatement.class, qname);
148         if (optRpc.isPresent()) {
149             final var rpc = optRpc.orElseThrow();
150
151             // We have found an RPC match,
152             if (it.hasNext()) {
153                 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
154                     + "therefore it must be the only step present", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
155             }
156             if (step instanceof ListInstance) {
157                 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
158                     + "therefore it must not contain key values", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
159             }
160
161             final var stack = SchemaInferenceStack.of(modelContext);
162             final var stmt = stack.enterSchemaTree(rpc.argument());
163             verify(rpc.equals(stmt), "Expecting %s, inferred %s", rpc, stmt);
164             return new OperationPath.Rpc(stack.toInference(), rpc);
165         }
166
167         final var stack = SchemaInferenceStack.of(modelContext);
168         final var path = new ArrayList<PathArgument>();
169         DataSchemaContext parentNode = databind.schemaTree().getRoot();
170         while (true) {
171             final var parentSchema = parentNode.dataSchemaNode();
172             if (parentSchema instanceof ActionNodeContainer actionParent) {
173                 final var optAction = actionParent.findAction(qname);
174                 if (optAction.isPresent()) {
175                     final var action = optAction.orElseThrow();
176
177                     if (it.hasNext()) {
178                         throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
179                             + "therefore it must not continue past it", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
180                     }
181                     if (step instanceof ListInstance) {
182                         throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
183                             + "therefore it must not contain key values",
184                             ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
185                     }
186
187                     final var stmt = stack.enterSchemaTree(qname);
188                     final var actionStmt = action.asEffectiveStatement();
189                     verify(actionStmt.equals(stmt), "Expecting %s, inferred %s", actionStmt, stmt);
190
191                     return new OperationPath.Action(stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
192                 }
193             }
194
195             // Resolve the child step with respect to data schema tree
196             final var found = parentNode instanceof DataSchemaContext.Composite composite
197                 ? composite.enterChild(stack, qname) : null;
198             if (found == null) {
199                 throw new RestconfDocumentedException("Schema for '" + qname + "' not found",
200                     ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
201             }
202
203             // Now add all mixins encountered to the path
204             var childNode = found;
205             while (childNode instanceof PathMixin currentMixin) {
206                 path.add(currentMixin.mixinPathStep());
207                 childNode = verifyNotNull(currentMixin.enterChild(stack, qname),
208                     "Mixin %s is missing child for %s while resolving %s", childNode, qname, found);
209             }
210
211             final PathArgument pathArg;
212             if (step instanceof ListInstance listStep) {
213                 final var values = listStep.keyValues();
214                 final var schema = childNode.dataSchemaNode();
215                 pathArg = schema instanceof ListSchemaNode listSchema
216                     ? prepareNodeWithPredicates(stack, qname, listSchema, values)
217                         : prepareNodeWithValue(stack, qname, schema, values);
218             } else {
219                 if (childNode.dataSchemaNode() instanceof ListSchemaNode list && !list.getKeyDefinition().isEmpty()) {
220                     throw new RestconfDocumentedException(
221                         "Entry '" + qname + "' requires key or value predicate to be present.",
222                         ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE);
223                 }
224                 pathArg = childNode.getPathStep();
225             }
226
227             path.add(pathArg);
228
229             if (!it.hasNext()) {
230                 return new DataPath(stack.toInference(), YangInstanceIdentifier.of(path), childNode);
231             }
232
233             parentNode = childNode;
234             step = it.next();
235             final var module = step.module();
236             if (module != null) {
237                 namespace = resolveNamespace(module);
238             }
239
240             qname = step.identifier().bindTo(namespace);
241         }
242     }
243
244     public @NonNull DataPath normalizeDataPath(final ApiPath apiPath) {
245         final var path = normalizePath(apiPath);
246         if (path instanceof DataPath dataPath) {
247             return dataPath;
248         }
249         throw new RestconfDocumentedException("Point '" + apiPath + "' resolves to non-data " + path,
250             ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
251     }
252
253     @Override
254     public PathArgument normalizePoint(final ApiPath value) {
255         final var path = normalizePath(value);
256         if (path instanceof DataPath dataPath) {
257             final var lastArg = dataPath.instance().getLastPathArgument();
258             if (lastArg != null) {
259                 return lastArg;
260             }
261             throw new IllegalArgumentException("Point '" + value + "' resolves to an empty path");
262         }
263         throw new IllegalArgumentException("Point '" + value + "' resolves to non-data " + path);
264     }
265
266     public @NonNull Rpc normalizeRpcPath(final ApiPath apiPath) {
267         final var steps = apiPath.steps();
268         return switch (steps.size()) {
269             case 0 -> throw new RestconfDocumentedException("RPC name must be present", ErrorType.PROTOCOL,
270                 ErrorTag.DATA_MISSING);
271             case 1 -> normalizeRpcPath(steps.get(0));
272             default -> throw new RestconfDocumentedException(apiPath + " does not refer to an RPC", ErrorType.PROTOCOL,
273                 ErrorTag.DATA_MISSING);
274         };
275     }
276
277     public @NonNull Rpc normalizeRpcPath(final ApiPath.Step step) {
278         final var firstModule = step.module();
279         if (firstModule == null) {
280             throw new RestconfDocumentedException(
281                 "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
282                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
283         }
284
285         final var namespace = resolveNamespace(firstModule);
286         final var qname = step.identifier().bindTo(namespace);
287         final var stack = SchemaInferenceStack.of(modelContext);
288         final SchemaTreeEffectiveStatement<?> stmt;
289         try {
290             stmt = stack.enterSchemaTree(qname);
291         } catch (IllegalArgumentException e) {
292             throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
293                 ErrorTag.DATA_MISSING, e);
294         }
295         if (stmt instanceof RpcEffectiveStatement rpc) {
296             return new Rpc(stack.toInference(), rpc);
297         }
298         throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
299             ErrorTag.DATA_MISSING);
300     }
301
302     public @NonNull InstanceReference normalizeDataOrActionPath(final ApiPath apiPath) {
303
304
305         // FIXME: optimize this
306         final var path = normalizePath(apiPath);
307         if (path instanceof DataPath dataPath) {
308             return dataPath;
309         }
310         if (path instanceof OperationPath.Action actionPath) {
311             return actionPath;
312         }
313         throw new RestconfDocumentedException("Unexpected path " + path, ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
314     }
315
316     private NodeIdentifierWithPredicates prepareNodeWithPredicates(final SchemaInferenceStack stack, final QName qname,
317             final @NonNull ListSchemaNode schema, final List<@NonNull String> keyValues) {
318         final var keyDef = schema.getKeyDefinition();
319         final var keySize = keyDef.size();
320         final var varSize = keyValues.size();
321         if (keySize != varSize) {
322             throw new RestconfDocumentedException(
323                 "Schema for " + qname + " requires " + keySize + " key values, " + varSize + " supplied",
324                 ErrorType.PROTOCOL, keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE);
325         }
326
327         final var values = ImmutableMap.<QName, Object>builderWithExpectedSize(keySize);
328         final var tmp = stack.copy();
329         for (int i = 0; i < keySize; ++i) {
330             final QName keyName = keyDef.get(i);
331             final var child = schema.getDataChildByName(keyName);
332             tmp.enterSchemaTree(keyName);
333             values.put(keyName, prepareValueByType(tmp, child, keyValues.get(i)));
334             tmp.exit();
335         }
336
337         return NodeIdentifierWithPredicates.of(qname, values.build());
338     }
339
340     private Object prepareValueByType(final SchemaInferenceStack stack, final DataSchemaNode schemaNode,
341             final @NonNull String value) {
342         if (schemaNode instanceof TypedDataSchemaNode typedSchema) {
343             return prepareValueByType(stack, typedSchema, typedSchema.getType(), value);
344         }
345         throw new VerifyException("Unhandled schema " + schemaNode + " decoding '" + value + "'");
346     }
347
348     private Object prepareValueByType(final SchemaInferenceStack stack, final TypedDataSchemaNode schemaNode,
349             final TypeDefinition<?> unresolved, final @NonNull String value) {
350         // Resolve 'type leafref' before dispatching on type
351         final TypeDefinition<?> typedef;
352         if (unresolved instanceof LeafrefTypeDefinition leafref) {
353             typedef = stack.resolveLeafref(leafref);
354         } else {
355             typedef = unresolved;
356         }
357
358         // Complex types
359         if (typedef instanceof IdentityrefTypeDefinition) {
360             return toIdentityrefQName(value, schemaNode);
361         }
362         if (typedef instanceof InstanceIdentifierTypeDefinition) {
363             return toInstanceIdentifier(value, schemaNode);
364         }
365         if (typedef instanceof UnionTypeDefinition union) {
366             return toUnion(stack, schemaNode, union, value);
367         }
368
369         // Simple types
370         final var codec = verifyNotNull(TypeDefinitionAwareCodec.from(typedef), "Unhandled type %s decoding %s",
371             typedef, value);
372         try {
373             return codec.deserialize(value);
374         } catch (IllegalArgumentException e) {
375             throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
376                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
377         }
378     }
379
380     private NodeWithValue<?> prepareNodeWithValue(final SchemaInferenceStack stack, final QName qname,
381             final DataSchemaNode schema, final List<String> keyValues) {
382         // TODO: qname should be always equal to schema.getQName(), right?
383         return new NodeWithValue<>(qname, prepareValueByType(stack, schema,
384             // FIXME: ahem: we probably want to do something differently here
385             keyValues.get(0)));
386     }
387
388     private Object toUnion(final SchemaInferenceStack stack, final TypedDataSchemaNode schemaNode,
389             final UnionTypeDefinition union, final @NonNull String value) {
390         // As per https://www.rfc-editor.org/rfc/rfc7950#section-9.12:
391         //   'type union' must have at least one 'type'
392         // hence this variable will always end up being non-null before being used
393         RestconfDocumentedException cause = null;
394         for (var type : union.getTypes()) {
395             try {
396                 return prepareValueByType(stack, schemaNode, type, value);
397             } catch (RestconfDocumentedException e) {
398                 if (cause == null) {
399                     cause = e;
400                 } else {
401                     cause.addSuppressed(e);
402                 }
403             }
404         }
405         throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
406             ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, cause);
407     }
408
409     private YangInstanceIdentifier toInstanceIdentifier(final String value, final TypedDataSchemaNode schemaNode) {
410         if (value.isEmpty() || !value.startsWith("/")) {
411             throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
412                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
413         }
414
415         try {
416             return normalizeDataPath(ApiPath.parse(value.substring(1))).instance();
417         } catch (ParseException | RestconfDocumentedException e) {
418             throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
419                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
420         }
421     }
422
423     private QName toIdentityrefQName(final String value, final TypedDataSchemaNode schemaNode) {
424         final QNameModule namespace;
425         final String localName;
426         final int firstColon = value.indexOf(':');
427         if (firstColon != -1) {
428             namespace = resolveNamespace(value.substring(0, firstColon));
429             localName = value.substring(firstColon + 1);
430         } else {
431             namespace = schemaNode.getQName().getModule();
432             localName = value;
433         }
434
435         return modelContext.getModuleStatement(namespace)
436             .streamEffectiveSubstatements(IdentityEffectiveStatement.class)
437             .map(IdentityEffectiveStatement::argument)
438             .filter(qname -> localName.equals(qname.getLocalName()))
439             .findFirst()
440             .orElseThrow(() -> new RestconfDocumentedException(
441                 "No identity found for '" + localName + "' in namespace " + namespace,
442                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
443     }
444
445     private @NonNull QNameModule resolveNamespace(final String moduleName) {
446         final var it = modelContext.findModuleStatements(moduleName).iterator();
447         if (it.hasNext()) {
448             return it.next().localQNameModule();
449         }
450         throw new RestconfDocumentedException("Failed to lookup for module with name '" + moduleName + "'.",
451             ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);
452     }
453 }