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.nb.rfc8040.Insert.PointNormalizer;
24 import org.opendaylight.restconf.server.api.DatabindAware;
25 import org.opendaylight.restconf.server.api.DatabindContext;
26 import org.opendaylight.restconf.server.api.DatabindPath;
27 import org.opendaylight.restconf.server.api.DatabindPath.Action;
28 import org.opendaylight.restconf.server.api.DatabindPath.Data;
29 import org.opendaylight.restconf.server.api.DatabindPath.InstanceReference;
30 import org.opendaylight.restconf.server.api.DatabindPath.Rpc;
31 import org.opendaylight.restconf.server.api.ServerException;
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) throws ServerException {
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 ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
87 "First member must use namespace-qualified form, '%s' does not", firstStep.identifier());
90 var namespace = resolveNamespace(firstModule);
92 var qname = step.identifier().bindTo(namespace);
94 // We go through more modern APIs here to get this special out of the way quickly
95 final var modelContext = databind.modelContext();
96 final var optRpc = modelContext.findModuleStatement(namespace).orElseThrow()
97 .findSchemaTreeNode(RpcEffectiveStatement.class, qname);
98 if (optRpc.isPresent()) {
99 final var rpc = optRpc.orElseThrow();
101 // We have found an RPC match,
103 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
104 "First step in the path resolves to RPC '%s' and therefore it must be the only step present",
107 if (step instanceof ListInstance) {
108 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
109 "First step in the path resolves to RPC '%s' and therefore it must not contain key values", qname);
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) throws ServerException {
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 ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
140 "Request path resolves to action '%s' and therefore it must not continue past it", qname);
142 if (step instanceof ListInstance) {
143 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
144 "Request path resolves to action '%s' and therefore it must not contain key values", qname);
147 final var stmt = stack.enterSchemaTree(qname);
148 final var actionStmt = action.asEffectiveStatement();
149 verify(actionStmt.equals(stmt), "Expecting %s, inferred %s", actionStmt, stmt);
151 return new Action(databind, stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
155 // Resolve the child step with respect to data schema tree
156 final var found = parentNode instanceof DataSchemaContext.Composite composite
157 ? composite.enterChild(stack, qname) : null;
159 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "Schema for '%s' not found",
163 // Now add all mixins encountered to the path
164 var childNode = found;
165 while (childNode instanceof PathMixin currentMixin) {
166 path.add(currentMixin.mixinPathStep());
167 childNode = verifyNotNull(currentMixin.enterChild(stack, qname),
168 "Mixin %s is missing child for %s while resolving %s", childNode, qname, found);
171 final PathArgument pathArg;
172 if (step instanceof ListInstance listStep) {
173 final var values = listStep.keyValues();
174 final var schema = childNode.dataSchemaNode();
175 if (schema instanceof ListSchemaNode listSchema) {
176 pathArg = prepareNodeWithPredicates(stack, qname, listSchema, values);
177 } else if (schema instanceof LeafListSchemaNode leafListSchema) {
178 if (values.size() != 1) {
179 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE,
180 "Entry '%s' requires one value predicate.", qname);
182 pathArg = new NodeWithValue<>(qname, parserJsonValue(stack, leafListSchema, values.get(0)));
184 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
185 "Entry '%s' does not take a key or value predicate.", qname);
188 if (childNode.dataSchemaNode() instanceof ListSchemaNode list && !list.getKeyDefinition().isEmpty()) {
189 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
190 "Entry '%s' requires key or value predicate to be present.", qname);
192 pathArg = childNode.getPathStep();
198 return new Data(databind, stack.toInference(), YangInstanceIdentifier.of(path), childNode);
201 parentNode = childNode;
203 final var module = step.module();
204 if (module != null) {
205 namespace = resolveNamespace(module);
208 qname = step.identifier().bindTo(namespace);
212 public @NonNull Data normalizeDataPath(final ApiPath apiPath) throws ServerException {
213 final var path = normalizePath(apiPath);
214 if (path instanceof Data dataPath) {
217 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "Point '%s' resolves to non-data %s",
222 public PathArgument normalizePoint(final ApiPath value) throws ServerException {
223 final var path = normalizePath(value);
224 if (path instanceof Data dataPath) {
225 final var lastArg = dataPath.instance().getLastPathArgument();
226 if (lastArg != null) {
229 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING,
230 "Point '%s' resolves to an empty path", value);
232 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "Point '%s' resolves to non-data %s",
236 public @NonNull Rpc normalizeRpcPath(final ApiPath apiPath) throws ServerException {
237 final var steps = apiPath.steps();
238 return switch (steps.size()) {
239 case 0 -> throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "RPC name must be present");
240 case 1 -> normalizeRpcPath(steps.get(0));
241 default -> throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING,
242 "%s does not refer to an RPC", apiPath);
246 public @NonNull Rpc normalizeRpcPath(final ApiPath.Step step) throws ServerException {
247 final var firstModule = step.module();
248 if (firstModule == null) {
249 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
250 "First member must use namespace-qualified form, '%s' does not", step.identifier());
253 final var namespace = resolveNamespace(firstModule);
254 final var qname = step.identifier().bindTo(namespace);
255 final var stack = SchemaInferenceStack.of(databind.modelContext());
256 final SchemaTreeEffectiveStatement<?> stmt;
258 stmt = stack.enterSchemaTree(qname);
259 } catch (IllegalArgumentException e) {
260 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, qname + " does not refer to an RPC",
263 if (stmt instanceof RpcEffectiveStatement rpc) {
264 return new Rpc(databind, stack.toInference(), rpc);
266 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "%s does not refer to an RPC", qname);
269 public @NonNull InstanceReference normalizeDataOrActionPath(final ApiPath apiPath) throws ServerException {
270 // FIXME: optimize this
271 final var path = normalizePath(apiPath);
272 if (path instanceof Data dataPath) {
275 if (path instanceof Action actionPath) {
278 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "Unexpected path %s", path);
281 private NodeIdentifierWithPredicates prepareNodeWithPredicates(final SchemaInferenceStack stack, final QName qname,
282 final @NonNull ListSchemaNode schema, final List<@NonNull String> keyValues) throws ServerException {
283 final var keyDef = schema.getKeyDefinition();
284 final var keySize = keyDef.size();
285 final var varSize = keyValues.size();
286 if (keySize != varSize) {
287 throw new ServerException(ErrorType.PROTOCOL,
288 keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE,
289 "Schema for %s requires %s key values, %s supplied", qname, keySize, varSize);
292 final var values = ImmutableMap.<QName, Object>builderWithExpectedSize(keySize);
293 final var tmp = stack.copy();
294 for (int i = 0; i < keySize; ++i) {
295 final QName keyName = keyDef.get(i);
296 final var child = schema.getDataChildByName(keyName);
297 tmp.enterSchemaTree(keyName);
298 values.put(keyName, prepareValueByType(tmp, child, keyValues.get(i)));
302 return NodeIdentifierWithPredicates.of(qname, values.build());
305 private Object prepareValueByType(final SchemaInferenceStack stack, final DataSchemaNode schemaNode,
306 final @NonNull String value) throws ServerException {
307 if (schemaNode instanceof TypedDataSchemaNode typedSchema) {
308 return parserJsonValue(stack, typedSchema, value);
310 throw new VerifyException("Unhandled schema " + schemaNode + " decoding '" + value + "'");
313 private Object parserJsonValue(final SchemaInferenceStack stack, final TypedDataSchemaNode schemaNode,
314 final String value) throws ServerException {
315 // As per https://www.rfc-editor.org/rfc/rfc8040#page-29:
317 // "api-identifier" and "key-value" MUST conform to the JSON identifier
318 // encoding rules in Section 4 of [RFC7951]: The RESTCONF root resource
319 // path is required. Additional sub-resource identifiers are optional.
320 // The characters in a key value string are constrained, and some
321 // characters need to be percent-encoded, as described in Section 3.5.3.
323 return databind.jsonCodecs().codecFor(schemaNode, stack).parseValue(value);
324 } catch (IllegalArgumentException e) {
325 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
326 "Invalid value '" + value + "' for " + schemaNode.getQName(), e);
330 private @NonNull QNameModule resolveNamespace(final String moduleName) throws ServerException {
331 final var it = databind.modelContext().findModuleStatements(moduleName).iterator();
333 return it.next().localQNameModule();
335 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT,
336 "Failed to lookup for module with name '%s'.", moduleName);