2 * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.restconf.server.spi;
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;
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;
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.
63 public final class ApiPathNormalizer implements PointNormalizer {
65 * A normalized {@link ApiPath}. This can be either
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>
73 public sealed interface Path {
75 * Returns the {@link EffectiveStatementInference} made by this path.
77 * @return the {@link EffectiveStatementInference} made by this path
79 Inference inference();
82 * A {@link Path} denoting an invocation of a YANG {@code action}.
84 * @param inference the {@link EffectiveStatementInference} made by this path
85 * @param instance the {@link YangInstanceIdentifier} of the instance being referenced, guaranteed to be
87 * @param action the {@code action}
89 record Action(Inference inference, YangInstanceIdentifier instance, ActionEffectiveStatement action)
90 implements OperationPath, InstanceReference {
92 requireNonNull(inference);
93 requireNonNull(action);
94 if (instance.isEmpty()) {
95 throw new IllegalArgumentException("action must be instantiated on a data resource");
100 public InputEffectiveStatement inputStatement() {
101 return action.input();
105 public OutputEffectiveStatement outputStatement() {
106 return action.output();
111 * A {@link Path} denoting a datastore instance.
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
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 {
123 requireNonNull(inference);
124 requireNonNull(instance);
125 requireNonNull(schema);
130 * A {@link Path} denoting an invocation of a YANG {@code rpc}.
132 * @param inference the {@link EffectiveStatementInference} made by this path
133 * @param rpc the {@code rpc}
135 record Rpc(Inference inference, RpcEffectiveStatement rpc) implements OperationPath {
137 requireNonNull(inference);
142 public InputEffectiveStatement inputStatement() {
147 public OutputEffectiveStatement outputStatement() {
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}}.
158 public sealed interface InstanceReference extends Path {
160 * Returns the {@link YangInstanceIdentifier} of the instance being referenced.
162 * @return the {@link YangInstanceIdentifier} of the instance being referenced,
163 * {@link YangInstanceIdentifier#empty()} denotes the datastora
165 YangInstanceIdentifier instance();
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}.
173 public sealed interface OperationPath extends Path {
175 * Returns the {@code input} statement of this operation.
177 * @return the {@code input} statement of this operation
179 InputEffectiveStatement inputStatement();
182 * Returns the {@code output} statement of this operation.
184 * @return the {@code output} statement of this operation
186 OutputEffectiveStatement outputStatement();
189 private final @NonNull EffectiveModelContext modelContext;
190 private final @NonNull DatabindContext databind;
192 public ApiPathNormalizer(final DatabindContext databind) {
193 this.databind = requireNonNull(databind);
194 modelContext = databind.modelContext();
197 public @NonNull Path normalizePath(final ApiPath apiPath) {
198 final var it = apiPath.steps().iterator();
200 return new Data(Inference.ofDataTreePath(modelContext), YangInstanceIdentifier.of(),
201 databind.schemaTree().getRoot());
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
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);
217 var namespace = resolveNamespace(firstModule);
218 var qname = step.identifier().bindTo(namespace);
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();
226 // We have found an RPC match,
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);
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);
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);
242 final var stack = SchemaInferenceStack.of(modelContext);
243 final var path = new ArrayList<PathArgument>();
244 DataSchemaContext parentNode = databind.schemaTree().getRoot();
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();
253 throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
254 + "therefore it must not continue past it", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
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);
262 final var stmt = stack.enterSchemaTree(qname);
263 final var actionStmt = action.asEffectiveStatement();
264 verify(actionStmt.equals(stmt), "Expecting %s, inferred %s", actionStmt, stmt);
266 return new OperationPath.Action(stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
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;
274 throw new RestconfDocumentedException("Schema for '" + qname + "' not found",
275 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
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);
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);
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);
299 pathArg = childNode.getPathStep();
305 return new Data(stack.toInference(), YangInstanceIdentifier.of(path), childNode);
308 parentNode = childNode;
310 final var module = step.module();
311 if (module != null) {
312 namespace = resolveNamespace(module);
315 qname = step.identifier().bindTo(namespace);
319 public @NonNull Data normalizeDataPath(final ApiPath apiPath) {
320 final var path = normalizePath(apiPath);
321 if (path instanceof Data dataPath) {
324 throw new RestconfDocumentedException("Point '" + apiPath + "' resolves to non-data " + path,
325 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
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) {
336 throw new IllegalArgumentException("Point '" + value + "' resolves to an empty path");
338 throw new IllegalArgumentException("Point '" + value + "' resolves to non-data " + path);
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);
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);
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;
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);
370 if (stmt instanceof RpcEffectiveStatement rpc) {
371 return new Rpc(stack.toInference(), rpc);
373 throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
374 ErrorTag.DATA_MISSING);
377 public @NonNull InstanceReference normalizeDataOrActionPath(final ApiPath apiPath) {
378 // FIXME: optimize this
379 final var path = normalizePath(apiPath);
380 if (path instanceof Data dataPath) {
383 if (path instanceof OperationPath.Action actionPath) {
386 throw new RestconfDocumentedException("Unexpected path " + path, ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
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);
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)));
410 return NodeIdentifierWithPredicates.of(qname, values.build());
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);
418 throw new VerifyException("Unhandled schema " + schemaNode + " decoding '" + value + "'");
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);
428 typedef = unresolved;
432 if (typedef instanceof IdentityrefTypeDefinition) {
433 return toIdentityrefQName(value, schemaNode);
435 if (typedef instanceof InstanceIdentifierTypeDefinition) {
436 return toInstanceIdentifier(value, schemaNode);
438 if (typedef instanceof UnionTypeDefinition union) {
439 return toUnion(stack, schemaNode, union, value);
443 final var codec = verifyNotNull(TypeDefinitionAwareCodec.from(typedef), "Unhandled type %s decoding %s",
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);
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
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()) {
469 return prepareValueByType(stack, schemaNode, type, value);
470 } catch (RestconfDocumentedException e) {
474 cause.addSuppressed(e);
478 throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
479 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, cause);
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);
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);
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);
504 namespace = schemaNode.getQName().getModule();
508 return modelContext.getModuleStatement(namespace)
509 .streamEffectiveSubstatements(IdentityEffectiveStatement.class)
510 .map(IdentityEffectiveStatement::argument)
511 .filter(qname -> localName.equals(qname.getLocalName()))
513 .orElseThrow(() -> new RestconfDocumentedException(
514 "No identity found for '" + localName + "' in namespace " + namespace,
515 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
518 private @NonNull QNameModule resolveNamespace(final String moduleName) {
519 final var it = modelContext.findModuleStatements(moduleName).iterator();
521 return it.next().localQNameModule();
523 throw new RestconfDocumentedException("Failed to lookup for module with name '" + moduleName + "'.",
524 ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);