Make leaf-list parsing explicit
[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.util.ArrayList;
17 import java.util.List;
18 import org.eclipse.jdt.annotation.NonNull;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.opendaylight.restconf.api.ApiPath;
21 import org.opendaylight.restconf.api.ApiPath.ListInstance;
22 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
23 import org.opendaylight.restconf.nb.rfc8040.Insert.PointNormalizer;
24 import org.opendaylight.restconf.server.api.DatabindContext;
25 import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Data;
26 import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.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.util.DataSchemaContext;
36 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
37 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
38 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference;
40 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.stmt.ActionEffectiveStatement;
44 import org.opendaylight.yangtools.yang.model.api.stmt.InputEffectiveStatement;
45 import org.opendaylight.yangtools.yang.model.api.stmt.OutputEffectiveStatement;
46 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
47 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
48 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
49 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
50
51 /**
52  * Utility for normalizing {@link ApiPath}s. An {@link ApiPath} can represent a number of different constructs, as
53  * denoted to in the {@link Path} interface hierarchy.
54  *
55  * <p>
56  * This process is governed by
57  * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.5.3">RFC8040, section 3.5.3</a>. The URI provides the
58  * equivalent of NETCONF XML filter encoding, with data values being escaped RFC7891 strings.
59  */
60 public final class ApiPathNormalizer implements PointNormalizer {
61     /**
62      * A normalized {@link ApiPath}. This can be either
63      * <ul>
64      *   <li>a {@link Data} pointing to a datastore resource, or</li>
65      *   <li>an {@link Rpc} pointing to a YANG {@code rpc} statement, or</li>
66      *   <li>an {@link Action} pointing to an instantiation of a YANG {@code action} statement</li>
67      * </ul>
68      */
69     @NonNullByDefault
70     public sealed interface Path {
71         /**
72          * Returns the {@link EffectiveStatementInference} made by this path.
73          *
74          * @return the {@link EffectiveStatementInference} made by this path
75          */
76         Inference inference();
77
78         /**
79          * A {@link Path} denoting an invocation of a YANG {@code action}.
80          *
81          * @param inference the {@link EffectiveStatementInference} made by this path
82          * @param instance the {@link YangInstanceIdentifier} of the instance being referenced, guaranteed to be
83          *        non-empty
84          * @param action the {@code action}
85          */
86         record Action(Inference inference, YangInstanceIdentifier instance, ActionEffectiveStatement action)
87                 implements OperationPath, InstanceReference {
88             public Action {
89                 requireNonNull(inference);
90                 requireNonNull(action);
91                 if (instance.isEmpty()) {
92                     throw new IllegalArgumentException("action must be instantiated on a data resource");
93                 }
94             }
95
96             @Override
97             public InputEffectiveStatement inputStatement() {
98                 return action.input();
99             }
100
101             @Override
102             public OutputEffectiveStatement outputStatement() {
103                 return action.output();
104             }
105         }
106
107         /**
108          * A {@link Path} denoting a datastore instance.
109          *
110          * @param inference the {@link EffectiveStatementInference} made by this path
111          * @param instance the {@link YangInstanceIdentifier} of the instance being referenced,
112          *                 {@link YangInstanceIdentifier#empty()} denotes the datastore
113          * @param schema the {@link DataSchemaContext} of the datastore instance
114          */
115         // FIXME: split into 'Datastore' and 'Data' with non-empty instance, so we can bind to correct
116         //        instance-identifier semantics, which does not allow YangInstanceIdentifier.empty()
117         record Data(Inference inference, YangInstanceIdentifier instance, DataSchemaContext schema)
118                 implements InstanceReference {
119             public Data {
120                 requireNonNull(inference);
121                 requireNonNull(instance);
122                 requireNonNull(schema);
123             }
124         }
125
126         /**
127          * A {@link Path} denoting an invocation of a YANG {@code rpc}.
128          *
129          * @param inference the {@link EffectiveStatementInference} made by this path
130          * @param rpc the {@code rpc}
131          */
132         record Rpc(Inference inference, RpcEffectiveStatement rpc) implements OperationPath {
133             public Rpc {
134                 requireNonNull(inference);
135                 requireNonNull(rpc);
136             }
137
138             @Override
139             public InputEffectiveStatement inputStatement() {
140                 return rpc.input();
141             }
142
143             @Override
144             public OutputEffectiveStatement outputStatement() {
145                 return rpc.output();
146             }
147         }
148     }
149
150     /**
151      * An intermediate trait of {@link Path}s which are referencing a YANG data resource. This can be either
152      * a {@link Data}, or an {@link Action}}.
153      */
154     @NonNullByDefault
155     public sealed interface InstanceReference extends Path {
156         /**
157          * Returns the {@link YangInstanceIdentifier} of the instance being referenced.
158          *
159          * @return the {@link YangInstanceIdentifier} of the instance being referenced,
160          *         {@link YangInstanceIdentifier#empty()} denotes the datastora
161          */
162         YangInstanceIdentifier instance();
163     }
164
165     /**
166      * An intermediate trait of {@link Path}s which are referencing a YANG operation. This can be either
167      * an {@link Action} on an {@link Rpc}.
168      */
169     @NonNullByDefault
170     public sealed interface OperationPath extends Path {
171         /**
172          * Returns the {@code input} statement of this operation.
173          *
174          * @return the {@code input} statement of this operation
175          */
176         InputEffectiveStatement inputStatement();
177
178         /**
179          * Returns the {@code output} statement of this operation.
180          *
181          * @return the {@code output} statement of this operation
182          */
183         OutputEffectiveStatement outputStatement();
184     }
185
186     private final @NonNull DatabindContext databind;
187
188     public ApiPathNormalizer(final DatabindContext databind) {
189         this.databind = requireNonNull(databind);
190     }
191
192     public @NonNull Path normalizePath(final ApiPath apiPath) {
193         final var it = apiPath.steps().iterator();
194         if (!it.hasNext()) {
195             return new Data(Inference.ofDataTreePath(databind.modelContext()), YangInstanceIdentifier.of(),
196                 databind.schemaTree().getRoot());
197         }
198
199         // First step is somewhat special:
200         // - it has to contain a module qualifier
201         // - it has to consider RPCs, for which we need SchemaContext
202         //
203         // We therefore peel that first iteration here and not worry about those details in further iterations
204         var step = it.next();
205         final var firstModule = step.module();
206         if (firstModule == null) {
207             throw new RestconfDocumentedException(
208                 "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
209                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
210         }
211
212         var namespace = resolveNamespace(firstModule);
213         var qname = step.identifier().bindTo(namespace);
214
215         // We go through more modern APIs here to get this special out of the way quickly
216         final var modelContext = databind.modelContext();
217         final var optRpc = modelContext.findModuleStatement(namespace).orElseThrow()
218             .findSchemaTreeNode(RpcEffectiveStatement.class, qname);
219         if (optRpc.isPresent()) {
220             final var rpc = optRpc.orElseThrow();
221
222             // We have found an RPC match,
223             if (it.hasNext()) {
224                 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
225                     + "therefore it must be the only step present", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
226             }
227             if (step instanceof ListInstance) {
228                 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
229                     + "therefore it must not contain key values", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
230             }
231
232             final var stack = SchemaInferenceStack.of(modelContext);
233             final var stmt = stack.enterSchemaTree(rpc.argument());
234             verify(rpc.equals(stmt), "Expecting %s, inferred %s", rpc, stmt);
235             return new OperationPath.Rpc(stack.toInference(), rpc);
236         }
237
238         final var stack = SchemaInferenceStack.of(modelContext);
239         final var path = new ArrayList<PathArgument>();
240         DataSchemaContext parentNode = databind.schemaTree().getRoot();
241         while (true) {
242             final var parentSchema = parentNode.dataSchemaNode();
243             if (parentSchema instanceof ActionNodeContainer actionParent) {
244                 final var optAction = actionParent.findAction(qname);
245                 if (optAction.isPresent()) {
246                     final var action = optAction.orElseThrow();
247
248                     if (it.hasNext()) {
249                         throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
250                             + "therefore it must not continue past it", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
251                     }
252                     if (step instanceof ListInstance) {
253                         throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
254                             + "therefore it must not contain key values",
255                             ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
256                     }
257
258                     final var stmt = stack.enterSchemaTree(qname);
259                     final var actionStmt = action.asEffectiveStatement();
260                     verify(actionStmt.equals(stmt), "Expecting %s, inferred %s", actionStmt, stmt);
261
262                     return new OperationPath.Action(stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
263                 }
264             }
265
266             // Resolve the child step with respect to data schema tree
267             final var found = parentNode instanceof DataSchemaContext.Composite composite
268                 ? composite.enterChild(stack, qname) : null;
269             if (found == null) {
270                 throw new RestconfDocumentedException("Schema for '" + qname + "' not found",
271                     ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
272             }
273
274             // Now add all mixins encountered to the path
275             var childNode = found;
276             while (childNode instanceof PathMixin currentMixin) {
277                 path.add(currentMixin.mixinPathStep());
278                 childNode = verifyNotNull(currentMixin.enterChild(stack, qname),
279                     "Mixin %s is missing child for %s while resolving %s", childNode, qname, found);
280             }
281
282             final PathArgument pathArg;
283             if (step instanceof ListInstance listStep) {
284                 final var values = listStep.keyValues();
285                 final var schema = childNode.dataSchemaNode();
286                 if (schema instanceof ListSchemaNode listSchema) {
287                     pathArg = prepareNodeWithPredicates(stack, qname, listSchema, values);
288                 } else if (schema instanceof LeafListSchemaNode leafListSchema) {
289                     if (values.size() != 1) {
290                         throw new RestconfDocumentedException("Entry '" + qname + "' requires one value predicate.",
291                             ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE);
292                     }
293                     pathArg = new NodeWithValue<>(qname, parserJsonValue(stack, leafListSchema, values.get(0)));
294                 } else {
295                     throw new RestconfDocumentedException(
296                         "Entry '" + qname + "' does not take a key or value predicate.",
297                         ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE);
298                 }
299             } else {
300                 if (childNode.dataSchemaNode() instanceof ListSchemaNode list && !list.getKeyDefinition().isEmpty()) {
301                     throw new RestconfDocumentedException(
302                         "Entry '" + qname + "' requires key or value predicate to be present.",
303                         ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE);
304                 }
305                 pathArg = childNode.getPathStep();
306             }
307
308             path.add(pathArg);
309
310             if (!it.hasNext()) {
311                 return new Data(stack.toInference(), YangInstanceIdentifier.of(path), childNode);
312             }
313
314             parentNode = childNode;
315             step = it.next();
316             final var module = step.module();
317             if (module != null) {
318                 namespace = resolveNamespace(module);
319             }
320
321             qname = step.identifier().bindTo(namespace);
322         }
323     }
324
325     public @NonNull Data normalizeDataPath(final ApiPath apiPath) {
326         final var path = normalizePath(apiPath);
327         if (path instanceof Data dataPath) {
328             return dataPath;
329         }
330         throw new RestconfDocumentedException("Point '" + apiPath + "' resolves to non-data " + path,
331             ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
332     }
333
334     @Override
335     public PathArgument normalizePoint(final ApiPath value) {
336         final var path = normalizePath(value);
337         if (path instanceof Data dataPath) {
338             final var lastArg = dataPath.instance().getLastPathArgument();
339             if (lastArg != null) {
340                 return lastArg;
341             }
342             throw new IllegalArgumentException("Point '" + value + "' resolves to an empty path");
343         }
344         throw new IllegalArgumentException("Point '" + value + "' resolves to non-data " + path);
345     }
346
347     public Path.@NonNull Rpc normalizeRpcPath(final ApiPath apiPath) {
348         final var steps = apiPath.steps();
349         return switch (steps.size()) {
350             case 0 -> throw new RestconfDocumentedException("RPC name must be present", ErrorType.PROTOCOL,
351                 ErrorTag.DATA_MISSING);
352             case 1 -> normalizeRpcPath(steps.get(0));
353             default -> throw new RestconfDocumentedException(apiPath + " does not refer to an RPC", ErrorType.PROTOCOL,
354                 ErrorTag.DATA_MISSING);
355         };
356     }
357
358     public Path.@NonNull Rpc normalizeRpcPath(final ApiPath.Step step) {
359         final var firstModule = step.module();
360         if (firstModule == null) {
361             throw new RestconfDocumentedException(
362                 "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
363                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
364         }
365
366         final var namespace = resolveNamespace(firstModule);
367         final var qname = step.identifier().bindTo(namespace);
368         final var stack = SchemaInferenceStack.of(databind.modelContext());
369         final SchemaTreeEffectiveStatement<?> stmt;
370         try {
371             stmt = stack.enterSchemaTree(qname);
372         } catch (IllegalArgumentException e) {
373             throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
374                 ErrorTag.DATA_MISSING, e);
375         }
376         if (stmt instanceof RpcEffectiveStatement rpc) {
377             return new Rpc(stack.toInference(), rpc);
378         }
379         throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
380             ErrorTag.DATA_MISSING);
381     }
382
383     public @NonNull InstanceReference normalizeDataOrActionPath(final ApiPath apiPath) {
384         // FIXME: optimize this
385         final var path = normalizePath(apiPath);
386         if (path instanceof Data dataPath) {
387             return dataPath;
388         }
389         if (path instanceof OperationPath.Action actionPath) {
390             return actionPath;
391         }
392         throw new RestconfDocumentedException("Unexpected path " + path, ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
393     }
394
395     private NodeIdentifierWithPredicates prepareNodeWithPredicates(final SchemaInferenceStack stack, final QName qname,
396             final @NonNull ListSchemaNode schema, final List<@NonNull String> keyValues) {
397         final var keyDef = schema.getKeyDefinition();
398         final var keySize = keyDef.size();
399         final var varSize = keyValues.size();
400         if (keySize != varSize) {
401             throw new RestconfDocumentedException(
402                 "Schema for " + qname + " requires " + keySize + " key values, " + varSize + " supplied",
403                 ErrorType.PROTOCOL, keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE);
404         }
405
406         final var values = ImmutableMap.<QName, Object>builderWithExpectedSize(keySize);
407         final var tmp = stack.copy();
408         for (int i = 0; i < keySize; ++i) {
409             final QName keyName = keyDef.get(i);
410             final var child = schema.getDataChildByName(keyName);
411             tmp.enterSchemaTree(keyName);
412             values.put(keyName, prepareValueByType(tmp, child, keyValues.get(i)));
413             tmp.exit();
414         }
415
416         return NodeIdentifierWithPredicates.of(qname, values.build());
417     }
418
419     private Object prepareValueByType(final SchemaInferenceStack stack, final DataSchemaNode schemaNode,
420             final @NonNull String value) {
421         if (schemaNode instanceof TypedDataSchemaNode typedSchema) {
422             return parserJsonValue(stack, typedSchema, value);
423         }
424         throw new VerifyException("Unhandled schema " + schemaNode + " decoding '" + value + "'");
425     }
426
427     private Object parserJsonValue(final SchemaInferenceStack stack, final TypedDataSchemaNode schemaNode,
428             final String value) {
429         // As per https://www.rfc-editor.org/rfc/rfc8040#page-29:
430         //            The syntax for
431         //            "api-identifier" and "key-value" MUST conform to the JSON identifier
432         //            encoding rules in Section 4 of [RFC7951]: The RESTCONF root resource
433         //            path is required.  Additional sub-resource identifiers are optional.
434         //            The characters in a key value string are constrained, and some
435         //            characters need to be percent-encoded, as described in Section 3.5.3.
436         try {
437             return databind.jsonCodecs().codecFor(schemaNode, stack).parseValue(null, value);
438         } catch (IllegalArgumentException e) {
439             throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
440                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
441         }
442     }
443
444     private @NonNull QNameModule resolveNamespace(final String moduleName) {
445         final var it = databind.modelContext().findModuleStatements(moduleName).iterator();
446         if (it.hasNext()) {
447             return it.next().localQNameModule();
448         }
449         throw new RestconfDocumentedException("Failed to lookup for module with name '" + moduleName + "'.",
450             ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);
451     }
452 }