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.Iterator;
18 import java.util.List;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.opendaylight.restconf.api.ApiPath;
21 import org.opendaylight.restconf.api.ApiPath.ListInstance;
22 import org.opendaylight.restconf.api.ApiPath.Step;
23 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
24 import org.opendaylight.restconf.nb.rfc8040.Insert.PointNormalizer;
25 import org.opendaylight.restconf.server.api.DatabindAware;
26 import org.opendaylight.restconf.server.api.DatabindContext;
27 import org.opendaylight.restconf.server.api.DatabindPath;
28 import org.opendaylight.restconf.server.api.DatabindPath.Action;
29 import org.opendaylight.restconf.server.api.DatabindPath.Data;
30 import org.opendaylight.restconf.server.api.DatabindPath.InstanceReference;
31 import org.opendaylight.restconf.server.api.DatabindPath.Rpc;
32 import org.opendaylight.yangtools.yang.common.ErrorTag;
33 import org.opendaylight.yangtools.yang.common.ErrorType;
34 import org.opendaylight.yangtools.yang.common.QName;
35 import org.opendaylight.yangtools.yang.common.QNameModule;
36 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
37 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
40 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
41 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
42 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
43 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
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;
52 * Utility for normalizing {@link ApiPath}s. An {@link ApiPath} can represent a number of different constructs, as
53 * denoted to in the {@link DatabindPath} interface hierarchy.
56 * This process is governed by
57 * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.5.3">RFC8040, section 3.5.3</a>. The URI provides the
58 * equivalent of NETCONF XML filter encoding, with data values being escaped RFC7891 strings.
60 public final class ApiPathNormalizer implements DatabindAware, PointNormalizer {
61 private final @NonNull DatabindContext databind;
63 public ApiPathNormalizer(final DatabindContext databind) {
64 this.databind = requireNonNull(databind);
68 public DatabindContext databind() {
72 public @NonNull DatabindPath normalizePath(final ApiPath apiPath) {
73 final var it = apiPath.steps().iterator();
75 return new Data(databind);
78 // First step is somewhat special:
79 // - it has to contain a module qualifier
80 // - it has to consider RPCs, for which we need SchemaContext
82 // We therefore peel that first iteration here and not worry about those details in further iterations
83 final var firstStep = it.next();
84 final var firstModule = firstStep.module();
85 if (firstModule == null) {
86 throw new RestconfDocumentedException(
87 "First member must use namespace-qualified form, '" + firstStep.identifier() + "' does not",
88 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
91 var namespace = resolveNamespace(firstModule);
93 var qname = step.identifier().bindTo(namespace);
95 // We go through more modern APIs here to get this special out of the way quickly
96 final var modelContext = databind.modelContext();
97 final var optRpc = modelContext.findModuleStatement(namespace).orElseThrow()
98 .findSchemaTreeNode(RpcEffectiveStatement.class, qname);
99 if (optRpc.isPresent()) {
100 final var rpc = optRpc.orElseThrow();
102 // We have found an RPC match,
104 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
105 + "therefore it must be the only step present", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
107 if (step instanceof ListInstance) {
108 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
109 + "therefore it must not contain key values", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
112 final var stack = SchemaInferenceStack.of(modelContext);
113 final var stmt = stack.enterSchemaTree(rpc.argument());
114 verify(rpc.equals(stmt), "Expecting %s, inferred %s", rpc, stmt);
115 return new Rpc(databind, stack.toInference(), rpc);
118 return normalizeSteps(SchemaInferenceStack.of(modelContext), databind.schemaTree().getRoot(), List.of(),
119 namespace, firstStep, it);
122 @NonNull DatabindPath normalizeSteps(final SchemaInferenceStack stack, final @NonNull DataSchemaContext rootNode,
123 final @NonNull List<PathArgument> pathPrefix, final @NonNull QNameModule firstNamespace,
124 final @NonNull Step firstStep, final Iterator<@NonNull Step> it) {
125 var parentNode = rootNode;
126 var namespace = firstNamespace;
127 var step = firstStep;
128 var qname = step.identifier().bindTo(namespace);
130 final var path = new ArrayList<>(pathPrefix);
132 final var parentSchema = parentNode.dataSchemaNode();
133 if (parentSchema instanceof ActionNodeContainer actionParent) {
134 final var optAction = actionParent.findAction(qname);
135 if (optAction.isPresent()) {
136 final var action = optAction.orElseThrow();
139 throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
140 + "therefore it must not continue past it", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
142 if (step instanceof ListInstance) {
143 throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
144 + "therefore it must not contain key values",
145 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
148 final var stmt = stack.enterSchemaTree(qname);
149 final var actionStmt = action.asEffectiveStatement();
150 verify(actionStmt.equals(stmt), "Expecting %s, inferred %s", actionStmt, stmt);
152 return new Action(databind, stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
156 // Resolve the child step with respect to data schema tree
157 final var found = parentNode instanceof DataSchemaContext.Composite composite
158 ? composite.enterChild(stack, qname) : null;
160 throw new RestconfDocumentedException("Schema for '" + qname + "' not found",
161 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
164 // Now add all mixins encountered to the path
165 var childNode = found;
166 while (childNode instanceof PathMixin currentMixin) {
167 path.add(currentMixin.mixinPathStep());
168 childNode = verifyNotNull(currentMixin.enterChild(stack, qname),
169 "Mixin %s is missing child for %s while resolving %s", childNode, qname, found);
172 final PathArgument pathArg;
173 if (step instanceof ListInstance listStep) {
174 final var values = listStep.keyValues();
175 final var schema = childNode.dataSchemaNode();
176 if (schema instanceof ListSchemaNode listSchema) {
177 pathArg = prepareNodeWithPredicates(stack, qname, listSchema, values);
178 } else if (schema instanceof LeafListSchemaNode leafListSchema) {
179 if (values.size() != 1) {
180 throw new RestconfDocumentedException("Entry '" + qname + "' requires one value predicate.",
181 ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE);
183 pathArg = new NodeWithValue<>(qname, parserJsonValue(stack, leafListSchema, values.get(0)));
185 throw new RestconfDocumentedException(
186 "Entry '" + qname + "' does not take a key or value predicate.",
187 ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE);
190 if (childNode.dataSchemaNode() instanceof ListSchemaNode list && !list.getKeyDefinition().isEmpty()) {
191 throw new RestconfDocumentedException(
192 "Entry '" + qname + "' requires key or value predicate to be present.",
193 ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE);
195 pathArg = childNode.getPathStep();
201 return new Data(databind, stack.toInference(), YangInstanceIdentifier.of(path), childNode);
204 parentNode = childNode;
206 final var module = step.module();
207 if (module != null) {
208 namespace = resolveNamespace(module);
211 qname = step.identifier().bindTo(namespace);
215 public @NonNull Data normalizeDataPath(final ApiPath apiPath) {
216 final var path = normalizePath(apiPath);
217 if (path instanceof Data dataPath) {
220 throw new RestconfDocumentedException("Point '" + apiPath + "' resolves to non-data " + path,
221 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
225 public PathArgument normalizePoint(final ApiPath value) {
226 final var path = normalizePath(value);
227 if (path instanceof Data dataPath) {
228 final var lastArg = dataPath.instance().getLastPathArgument();
229 if (lastArg != null) {
232 throw new IllegalArgumentException("Point '" + value + "' resolves to an empty path");
234 throw new IllegalArgumentException("Point '" + value + "' resolves to non-data " + path);
237 public @NonNull Rpc normalizeRpcPath(final ApiPath apiPath) {
238 final var steps = apiPath.steps();
239 return switch (steps.size()) {
240 case 0 -> throw new RestconfDocumentedException("RPC name must be present", ErrorType.PROTOCOL,
241 ErrorTag.DATA_MISSING);
242 case 1 -> normalizeRpcPath(steps.get(0));
243 default -> throw new RestconfDocumentedException(apiPath + " does not refer to an RPC", ErrorType.PROTOCOL,
244 ErrorTag.DATA_MISSING);
248 public @NonNull Rpc normalizeRpcPath(final ApiPath.Step step) {
249 final var firstModule = step.module();
250 if (firstModule == null) {
251 throw new RestconfDocumentedException(
252 "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
253 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
256 final var namespace = resolveNamespace(firstModule);
257 final var qname = step.identifier().bindTo(namespace);
258 final var stack = SchemaInferenceStack.of(databind.modelContext());
259 final SchemaTreeEffectiveStatement<?> stmt;
261 stmt = stack.enterSchemaTree(qname);
262 } catch (IllegalArgumentException e) {
263 throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
264 ErrorTag.DATA_MISSING, e);
266 if (stmt instanceof RpcEffectiveStatement rpc) {
267 return new Rpc(databind, stack.toInference(), rpc);
269 throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL,
270 ErrorTag.DATA_MISSING);
273 public @NonNull InstanceReference normalizeDataOrActionPath(final ApiPath apiPath) {
274 // FIXME: optimize this
275 final var path = normalizePath(apiPath);
276 if (path instanceof Data dataPath) {
279 if (path instanceof Action actionPath) {
282 throw new RestconfDocumentedException("Unexpected path " + path, ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
285 private NodeIdentifierWithPredicates prepareNodeWithPredicates(final SchemaInferenceStack stack, final QName qname,
286 final @NonNull ListSchemaNode schema, final List<@NonNull String> keyValues) {
287 final var keyDef = schema.getKeyDefinition();
288 final var keySize = keyDef.size();
289 final var varSize = keyValues.size();
290 if (keySize != varSize) {
291 throw new RestconfDocumentedException(
292 "Schema for " + qname + " requires " + keySize + " key values, " + varSize + " supplied",
293 ErrorType.PROTOCOL, keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE);
296 final var values = ImmutableMap.<QName, Object>builderWithExpectedSize(keySize);
297 final var tmp = stack.copy();
298 for (int i = 0; i < keySize; ++i) {
299 final QName keyName = keyDef.get(i);
300 final var child = schema.getDataChildByName(keyName);
301 tmp.enterSchemaTree(keyName);
302 values.put(keyName, prepareValueByType(tmp, child, keyValues.get(i)));
306 return NodeIdentifierWithPredicates.of(qname, values.build());
309 private Object prepareValueByType(final SchemaInferenceStack stack, final DataSchemaNode schemaNode,
310 final @NonNull String value) {
311 if (schemaNode instanceof TypedDataSchemaNode typedSchema) {
312 return parserJsonValue(stack, typedSchema, value);
314 throw new VerifyException("Unhandled schema " + schemaNode + " decoding '" + value + "'");
317 private Object parserJsonValue(final SchemaInferenceStack stack, final TypedDataSchemaNode schemaNode,
318 final String value) {
319 // As per https://www.rfc-editor.org/rfc/rfc8040#page-29:
321 // "api-identifier" and "key-value" MUST conform to the JSON identifier
322 // encoding rules in Section 4 of [RFC7951]: The RESTCONF root resource
323 // path is required. Additional sub-resource identifiers are optional.
324 // The characters in a key value string are constrained, and some
325 // characters need to be percent-encoded, as described in Section 3.5.3.
327 return databind.jsonCodecs().codecFor(schemaNode, stack).parseValue(null, value);
328 } catch (IllegalArgumentException e) {
329 throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
330 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
334 private @NonNull QNameModule resolveNamespace(final String moduleName) {
335 final var it = databind.modelContext().findModuleStatements(moduleName).iterator();
337 return it.next().localQNameModule();
339 throw new RestconfDocumentedException("Failed to lookup for module with name '" + moduleName + "'.",
340 ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT);