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.OperationPath.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.impl.codec.TypeDefinitionAwareCodec;
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.EffectiveModelContext;
41 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
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.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.api.type.UnionTypeDefinition;
53 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
54 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
57 * Deserializer for {@link String} to {@link YangInstanceIdentifier} for restconf.
59 public final class ApiPathNormalizer implements PointNormalizer {
61 public sealed interface Path {
63 Inference inference();
67 public sealed interface InstanceReference extends Path {
69 YangInstanceIdentifier instance();
73 public record DataPath(Inference inference, YangInstanceIdentifier instance, DataSchemaContext schema)
74 implements InstanceReference {
76 requireNonNull(inference);
77 requireNonNull(instance);
78 requireNonNull(schema);
83 public sealed interface OperationPath extends Path {
85 InputEffectiveStatement inputStatement();
87 record Action(Inference inference, YangInstanceIdentifier instance, ActionEffectiveStatement action)
88 implements OperationPath, InstanceReference {
90 requireNonNull(inference);
91 requireNonNull(action);
92 requireNonNull(instance);
96 public InputEffectiveStatement inputStatement() {
97 return action.input();
101 record Rpc(Inference inference, RpcEffectiveStatement rpc) implements OperationPath {
103 requireNonNull(inference);
108 public InputEffectiveStatement inputStatement() {
114 private final @NonNull EffectiveModelContext modelContext;
115 private final @NonNull DatabindContext databind;
117 public ApiPathNormalizer(final DatabindContext databind) {
118 this.databind = requireNonNull(databind);
119 modelContext = databind.modelContext();
122 public @NonNull Path normalizePath(final ApiPath apiPath) {
123 final var it = apiPath.steps().iterator();
125 return new DataPath(Inference.ofDataTreePath(modelContext), YangInstanceIdentifier.of(),
126 databind.schemaTree().getRoot());
129 // First step is somewhat special:
130 // - it has to contain a module qualifier
131 // - it has to consider RPCs, for which we need SchemaContext
133 // We therefore peel that first iteration here and not worry about those details in further iterations
134 var step = it.next();
135 final var firstModule = step.module();
136 if (firstModule == null) {
137 throw new RestconfDocumentedException(
138 "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
139 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
142 var namespace = resolveNamespace(firstModule);
143 var qname = step.identifier().bindTo(namespace);
145 // We go through more modern APIs here to get this special out of the way quickly
146 final var optRpc = modelContext.findModuleStatement(namespace).orElseThrow()
147 .findSchemaTreeNode(RpcEffectiveStatement.class, qname);
148 if (optRpc.isPresent()) {
149 final var rpc = optRpc.orElseThrow();
151 // We have found an RPC match,
153 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
154 + "therefore it must be the only step present", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
156 if (step instanceof ListInstance) {
157 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
158 + "therefore it must not contain key values", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
161 final var stack = SchemaInferenceStack.of(modelContext);
162 final var stmt = stack.enterSchemaTree(rpc.argument());
163 verify(rpc.equals(stmt), "Expecting %s, inferred %s", rpc, stmt);
164 return new OperationPath.Rpc(stack.toInference(), rpc);
167 final var stack = SchemaInferenceStack.of(modelContext);
168 final var path = new ArrayList<PathArgument>();
169 DataSchemaContext parentNode = databind.schemaTree().getRoot();
171 final var parentSchema = parentNode.dataSchemaNode();
172 if (parentSchema instanceof ActionNodeContainer actionParent) {
173 final var optAction = actionParent.findAction(qname);
174 if (optAction.isPresent()) {
175 final var action = optAction.orElseThrow();
178 throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
179 + "therefore it must not continue past it", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
181 if (step instanceof ListInstance) {
182 throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
183 + "therefore it must not contain key values",
184 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
187 final var stmt = stack.enterSchemaTree(qname);
188 final var actionStmt = action.asEffectiveStatement();
189 verify(actionStmt.equals(stmt), "Expecting %s, inferred %s", actionStmt, stmt);
191 return new OperationPath.Action(stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
195 // Resolve the child step with respect to data schema tree
196 final var found = parentNode instanceof DataSchemaContext.Composite composite
197 ? composite.enterChild(stack, qname) : null;
199 throw new RestconfDocumentedException("Schema for '" + qname + "' not found",
200 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
203 // Now add all mixins encountered to the path
204 var childNode = found;
205 while (childNode instanceof PathMixin currentMixin) {
206 path.add(currentMixin.mixinPathStep());
207 childNode = verifyNotNull(currentMixin.enterChild(stack, qname),
208 "Mixin %s is missing child for %s while resolving %s", childNode, qname, found);
211 final PathArgument pathArg;
212 if (step instanceof ListInstance listStep) {
213 final var values = listStep.keyValues();
214 final var schema = childNode.dataSchemaNode();
215 pathArg = schema instanceof ListSchemaNode listSchema
216 ? prepareNodeWithPredicates(stack, qname, listSchema, values)
217 : prepareNodeWithValue(stack, qname, schema, values);
219 if (childNode.dataSchemaNode() instanceof ListSchemaNode list && !list.getKeyDefinition().isEmpty()) {
220 throw new RestconfDocumentedException(
221 "Entry '" + qname + "' requires key or value predicate to be present.",
222 ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE);
224 pathArg = childNode.getPathStep();
230 return new DataPath(stack.toInference(), YangInstanceIdentifier.of(path), childNode);
233 parentNode = childNode;
235 final var module = step.module();
236 if (module != null) {
237 namespace = resolveNamespace(module);
240 qname = step.identifier().bindTo(namespace);
244 public @NonNull DataPath normalizeDataPath(final ApiPath apiPath) {
245 final var path = normalizePath(apiPath);
246 if (path instanceof DataPath dataPath) {
249 throw new RestconfDocumentedException("Point '" + apiPath + "' resolves to non-data " + path,
250 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
254 public PathArgument normalizePoint(final ApiPath value) {
255 final var path = normalizePath(value);
256 if (path instanceof DataPath dataPath) {
257 final var lastArg = dataPath.instance().getLastPathArgument();
258 if (lastArg != null) {
261 throw new IllegalArgumentException("Point '" + value + "' resolves to an empty path");
263 throw new IllegalArgumentException("Point '" + value + "' resolves to non-data " + path);
266 public @NonNull Rpc normalizeRpcPath(final ApiPath apiPath) {
267 final var steps = apiPath.steps();
268 return switch (steps.size()) {
269 case 0 -> throw new RestconfDocumentedException("RPC name must be present", ErrorType.PROTOCOL,
270 ErrorTag.DATA_MISSING);
271 case 1 -> normalizeRpcPath(steps.get(0));
272 default -> throw new RestconfDocumentedException(apiPath + " does not refer to an RPC", ErrorType.PROTOCOL,
273 ErrorTag.DATA_MISSING);
277 public @NonNull Rpc normalizeRpcPath(final ApiPath.Step step) {
278 final var firstModule = step.module();
279 if (firstModule == null) {
280 throw new RestconfDocumentedException(
281 "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
282 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
285 final var namespace = resolveNamespace(firstModule);
286 final var qname = step.identifier().bindTo(namespace);
287 final var stack = SchemaInferenceStack.of(modelContext);
288 final SchemaTreeEffectiveStatement<?> stmt;
290 stmt = stack.enterSchemaTree(qname);
291 } catch (IllegalArgumentException e) {
292 throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
293 ErrorTag.DATA_MISSING, e);
295 if (stmt instanceof RpcEffectiveStatement rpc) {
296 return new Rpc(stack.toInference(), rpc);
298 throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
299 ErrorTag.DATA_MISSING);
302 public @NonNull InstanceReference normalizeDataOrActionPath(final ApiPath apiPath) {
305 // FIXME: optimize this
306 final var path = normalizePath(apiPath);
307 if (path instanceof DataPath dataPath) {
310 if (path instanceof OperationPath.Action actionPath) {
313 throw new RestconfDocumentedException("Unexpected path " + path, ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
316 private NodeIdentifierWithPredicates prepareNodeWithPredicates(final SchemaInferenceStack stack, final QName qname,
317 final @NonNull ListSchemaNode schema, final List<@NonNull String> keyValues) {
318 final var keyDef = schema.getKeyDefinition();
319 final var keySize = keyDef.size();
320 final var varSize = keyValues.size();
321 if (keySize != varSize) {
322 throw new RestconfDocumentedException(
323 "Schema for " + qname + " requires " + keySize + " key values, " + varSize + " supplied",
324 ErrorType.PROTOCOL, keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE);
327 final var values = ImmutableMap.<QName, Object>builderWithExpectedSize(keySize);
328 final var tmp = stack.copy();
329 for (int i = 0; i < keySize; ++i) {
330 final QName keyName = keyDef.get(i);
331 final var child = schema.getDataChildByName(keyName);
332 tmp.enterSchemaTree(keyName);
333 values.put(keyName, prepareValueByType(tmp, child, keyValues.get(i)));
337 return NodeIdentifierWithPredicates.of(qname, values.build());
340 private Object prepareValueByType(final SchemaInferenceStack stack, final DataSchemaNode schemaNode,
341 final @NonNull String value) {
342 if (schemaNode instanceof TypedDataSchemaNode typedSchema) {
343 return prepareValueByType(stack, typedSchema, typedSchema.getType(), value);
345 throw new VerifyException("Unhandled schema " + schemaNode + " decoding '" + value + "'");
348 private Object prepareValueByType(final SchemaInferenceStack stack, final TypedDataSchemaNode schemaNode,
349 final TypeDefinition<?> unresolved, final @NonNull String value) {
350 // Resolve 'type leafref' before dispatching on type
351 final TypeDefinition<?> typedef;
352 if (unresolved instanceof LeafrefTypeDefinition leafref) {
353 typedef = stack.resolveLeafref(leafref);
355 typedef = unresolved;
359 if (typedef instanceof IdentityrefTypeDefinition) {
360 return toIdentityrefQName(value, schemaNode);
362 if (typedef instanceof InstanceIdentifierTypeDefinition) {
363 return toInstanceIdentifier(value, schemaNode);
365 if (typedef instanceof UnionTypeDefinition union) {
366 return toUnion(stack, schemaNode, union, value);
370 final var codec = verifyNotNull(TypeDefinitionAwareCodec.from(typedef), "Unhandled type %s decoding %s",
373 return codec.deserialize(value);
374 } catch (IllegalArgumentException e) {
375 throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
376 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
380 private NodeWithValue<?> prepareNodeWithValue(final SchemaInferenceStack stack, final QName qname,
381 final DataSchemaNode schema, final List<String> keyValues) {
382 // TODO: qname should be always equal to schema.getQName(), right?
383 return new NodeWithValue<>(qname, prepareValueByType(stack, schema,
384 // FIXME: ahem: we probably want to do something differently here
388 private Object toUnion(final SchemaInferenceStack stack, final TypedDataSchemaNode schemaNode,
389 final UnionTypeDefinition union, final @NonNull String value) {
390 // As per https://www.rfc-editor.org/rfc/rfc7950#section-9.12:
391 // 'type union' must have at least one 'type'
392 // hence this variable will always end up being non-null before being used
393 RestconfDocumentedException cause = null;
394 for (var type : union.getTypes()) {
396 return prepareValueByType(stack, schemaNode, type, value);
397 } catch (RestconfDocumentedException e) {
401 cause.addSuppressed(e);
405 throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
406 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, cause);
409 private YangInstanceIdentifier toInstanceIdentifier(final String value, final TypedDataSchemaNode schemaNode) {
410 if (value.isEmpty() || !value.startsWith("/")) {
411 throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
412 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
416 return normalizeDataPath(ApiPath.parse(value.substring(1))).instance();
417 } catch (ParseException | RestconfDocumentedException e) {
418 throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
419 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
423 private QName toIdentityrefQName(final String value, final TypedDataSchemaNode schemaNode) {
424 final QNameModule namespace;
425 final String localName;
426 final int firstColon = value.indexOf(':');
427 if (firstColon != -1) {
428 namespace = resolveNamespace(value.substring(0, firstColon));
429 localName = value.substring(firstColon + 1);
431 namespace = schemaNode.getQName().getModule();
435 return modelContext.getModuleStatement(namespace)
436 .streamEffectiveSubstatements(IdentityEffectiveStatement.class)
437 .map(IdentityEffectiveStatement::argument)
438 .filter(qname -> localName.equals(qname.getLocalName()))
440 .orElseThrow(() -> new RestconfDocumentedException(
441 "No identity found for '" + localName + "' in namespace " + namespace,
442 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
445 private @NonNull QNameModule resolveNamespace(final String moduleName) {
446 final var it = modelContext.findModuleStatements(moduleName).iterator();
448 return it.next().localQNameModule();
450 throw new RestconfDocumentedException("Failed to lookup for module with name '" + moduleName + "'.",
451 ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);