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