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