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