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.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;
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.
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.
61 public final class ApiPathNormalizer implements PointNormalizer {
63 * A normalized {@link ApiPath}. This can be either
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>
71 public sealed interface Path {
73 * Returns the {@link EffectiveStatementInference} made by this path.
75 * @return the {@link EffectiveStatementInference} made by this path
77 Inference inference();
80 * A {@link Path} denoting an invocation of a YANG {@code action}.
82 * @param inference the {@link EffectiveStatementInference} made by this path
83 * @param instance the {@link YangInstanceIdentifier} of the instance being referenced, guaranteed to be
85 * @param action the {@code action}
87 record Action(Inference inference, YangInstanceIdentifier instance, ActionEffectiveStatement action)
88 implements OperationPath, InstanceReference {
90 requireNonNull(inference);
91 requireNonNull(action);
92 if (instance.isEmpty()) {
93 throw new IllegalArgumentException("action must be instantiated on a data resource");
98 public InputEffectiveStatement inputStatement() {
99 return action.input();
103 public OutputEffectiveStatement outputStatement() {
104 return action.output();
109 * A {@link Path} denoting a datastore instance.
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
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 {
121 requireNonNull(inference);
122 requireNonNull(instance);
123 requireNonNull(schema);
128 * A {@link Path} denoting an invocation of a YANG {@code rpc}.
130 * @param inference the {@link EffectiveStatementInference} made by this path
131 * @param rpc the {@code rpc}
133 record Rpc(Inference inference, RpcEffectiveStatement rpc) implements OperationPath {
135 requireNonNull(inference);
140 public InputEffectiveStatement inputStatement() {
145 public OutputEffectiveStatement outputStatement() {
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}}.
156 public sealed interface InstanceReference extends Path {
158 * Returns the {@link YangInstanceIdentifier} of the instance being referenced.
160 * @return the {@link YangInstanceIdentifier} of the instance being referenced,
161 * {@link YangInstanceIdentifier#empty()} denotes the datastora
163 YangInstanceIdentifier instance();
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}.
171 public sealed interface OperationPath extends Path {
173 * Returns the {@code input} statement of this operation.
175 * @return the {@code input} statement of this operation
177 InputEffectiveStatement inputStatement();
180 * Returns the {@code output} statement of this operation.
182 * @return the {@code output} statement of this operation
184 OutputEffectiveStatement outputStatement();
187 private final @NonNull DatabindContext databind;
189 public ApiPathNormalizer(final DatabindContext databind) {
190 this.databind = requireNonNull(databind);
193 public @NonNull Path normalizePath(final ApiPath apiPath) {
194 final var it = apiPath.steps().iterator();
196 return new Data(Inference.ofDataTreePath(databind.modelContext()), YangInstanceIdentifier.of(),
197 databind.schemaTree().getRoot());
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
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);
213 var namespace = resolveNamespace(firstModule);
214 var qname = step.identifier().bindTo(namespace);
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();
223 // We have found an RPC match,
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);
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);
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);
239 final var stack = SchemaInferenceStack.of(modelContext);
240 final var path = new ArrayList<PathArgument>();
241 DataSchemaContext parentNode = databind.schemaTree().getRoot();
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();
250 throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
251 + "therefore it must not continue past it", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
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);
259 final var stmt = stack.enterSchemaTree(qname);
260 final var actionStmt = action.asEffectiveStatement();
261 verify(actionStmt.equals(stmt), "Expecting %s, inferred %s", actionStmt, stmt);
263 return new Action(stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
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;
271 throw new RestconfDocumentedException("Schema for '" + qname + "' not found",
272 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
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);
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);
294 pathArg = new NodeWithValue<>(qname, parserJsonValue(stack, leafListSchema, values.get(0)));
296 throw new RestconfDocumentedException(
297 "Entry '" + qname + "' does not take a key or value predicate.",
298 ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE);
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);
306 pathArg = childNode.getPathStep();
312 return new Data(stack.toInference(), YangInstanceIdentifier.of(path), childNode);
315 parentNode = childNode;
317 final var module = step.module();
318 if (module != null) {
319 namespace = resolveNamespace(module);
322 qname = step.identifier().bindTo(namespace);
326 public @NonNull Data normalizeDataPath(final ApiPath apiPath) {
327 final var path = normalizePath(apiPath);
328 if (path instanceof Data dataPath) {
331 throw new RestconfDocumentedException("Point '" + apiPath + "' resolves to non-data " + path,
332 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
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) {
343 throw new IllegalArgumentException("Point '" + value + "' resolves to an empty path");
345 throw new IllegalArgumentException("Point '" + value + "' resolves to non-data " + path);
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);
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);
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;
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);
377 if (stmt instanceof RpcEffectiveStatement rpc) {
378 return new Rpc(stack.toInference(), rpc);
380 throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
381 ErrorTag.DATA_MISSING);
384 public @NonNull InstanceReference normalizeDataOrActionPath(final ApiPath apiPath) {
385 // FIXME: optimize this
386 final var path = normalizePath(apiPath);
387 if (path instanceof Data dataPath) {
390 if (path instanceof Action actionPath) {
393 throw new RestconfDocumentedException("Unexpected path " + path, ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
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);
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)));
417 return NodeIdentifierWithPredicates.of(qname, values.build());
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);
425 throw new VerifyException("Unhandled schema " + schemaNode + " decoding '" + value + "'");
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:
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.
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);
445 private @NonNull QNameModule resolveNamespace(final String moduleName) {
446 final var it = databind.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);