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