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.nb.rfc8040.utils.parser;
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.collect.ImmutableMap;
15 import java.text.ParseException;
16 import java.util.ArrayList;
17 import java.util.List;
18 import org.eclipse.jdt.annotation.NonNull;
19 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
20 import org.opendaylight.restconf.nb.rfc8040.ApiPath;
21 import org.opendaylight.restconf.nb.rfc8040.ApiPath.ListInstance;
22 import org.opendaylight.yangtools.yang.common.ErrorTag;
23 import org.opendaylight.yangtools.yang.common.ErrorType;
24 import org.opendaylight.yangtools.yang.common.QName;
25 import org.opendaylight.yangtools.yang.common.QNameModule;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
29 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
31 import org.opendaylight.yangtools.yang.data.impl.codec.TypeDefinitionAwareCodec;
32 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
33 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
34 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
36 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
37 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
40 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
41 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
43 import org.opendaylight.yangtools.yang.model.api.stmt.IdentityEffectiveStatement;
44 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
45 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
46 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
47 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
48 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
51 * Deserializer for {@link String} to {@link YangInstanceIdentifier} for restconf.
53 public final class YangInstanceIdentifierDeserializer {
54 public static final class Result {
55 public final @NonNull YangInstanceIdentifier path;
56 public final @NonNull SchemaInferenceStack stack;
57 public final @NonNull SchemaNode node;
59 Result(final EffectiveModelContext context) {
60 path = YangInstanceIdentifier.empty();
61 node = requireNonNull(context);
62 stack = SchemaInferenceStack.of(context);
65 Result(final EffectiveModelContext context, final QName qname) {
66 // Legacy behavior: RPCs do not really have a YangInstanceIdentifier, but the rest of the code expects it
67 path = YangInstanceIdentifier.of(qname);
68 stack = SchemaInferenceStack.of(context);
70 final var stmt = stack.enterSchemaTree(qname);
71 verify(stmt instanceof RpcDefinition, "Unexpected statement %s", stmt);
72 node = (RpcDefinition) stmt;
75 Result(final List<PathArgument> steps, final SchemaInferenceStack stack, final SchemaNode node) {
76 path = YangInstanceIdentifier.create(steps);
77 this.stack = requireNonNull(stack);
78 this.node = requireNonNull(node);
82 private final @NonNull EffectiveModelContext schemaContext;
83 private final @NonNull ApiPath apiPath;
85 private YangInstanceIdentifierDeserializer(final EffectiveModelContext schemaContext, final ApiPath apiPath) {
86 this.schemaContext = requireNonNull(schemaContext);
87 this.apiPath = requireNonNull(apiPath);
91 * Method to create {@link List} from {@link PathArgument} which are parsing from data by {@link SchemaContext}.
93 * @param schemaContext for validate of parsing path arguments
94 * @param data path to data, in URL string form
95 * @return {@link Iterable} of {@link PathArgument}
96 * @throws RestconfDocumentedException the path is not valid
98 public static Result create(final EffectiveModelContext schemaContext, final String data) {
101 path = ApiPath.parse(requireNonNull(data));
102 } catch (ParseException e) {
103 throw new RestconfDocumentedException("Invalid path '" + data + "' at offset " + e.getErrorOffset(),
104 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e);
106 return create(schemaContext, path);
109 public static Result create(final EffectiveModelContext schemaContext, final ApiPath path) {
110 return new YangInstanceIdentifierDeserializer(schemaContext, path).parse();
113 // FIXME: NETCONF-818: this method really needs to report an Inference and optionally a YangInstanceIdentifier
114 // - we need the inference for discerning the correct context
115 // - RPCs do not have a YangInstanceIdentifier
116 // - Actions always have a YangInstanceIdentifier, but it points to their parent
117 // - we need to discern the cases RPC invocation, Action invocation and data tree access quickly
119 // All of this really is an utter mess because we end up calling into this code from various places which,
120 // for example, should not allow RPCs to be valid targets
121 private Result parse() {
122 final var it = apiPath.steps().iterator();
124 return new Result(schemaContext);
127 // First step is somewhat special:
128 // - it has to contain a module qualifier
129 // - it has to consider RPCs, for which we need SchemaContext
131 // We therefore peel that first iteration here and not worry about those details in further iterations
132 var step = it.next();
133 final var firstModule = RestconfDocumentedException.throwIfNull(step.module(),
134 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
135 "First member must use namespace-qualified form, '%s' does not", step.identifier());
136 var namespace = resolveNamespace(firstModule);
137 var qname = step.identifier().bindTo(namespace);
139 // We go through more modern APIs here to get this special out of the way quickly
140 final var optRpc = schemaContext.findModuleStatement(namespace).orElseThrow()
141 .findSchemaTreeNode(RpcEffectiveStatement.class, qname);
142 if (optRpc.isPresent()) {
143 // We have found an RPC match,
145 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
146 + "therefore it must be the only step present", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
148 if (step instanceof ListInstance) {
149 throw new RestconfDocumentedException("First step in the path resolves to RPC '" + qname + "' and "
150 + "therefore it must not contain key values", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
153 return new Result(schemaContext, optRpc.orElseThrow().argument());
156 final var stack = SchemaInferenceStack.of(schemaContext);
157 final var path = new ArrayList<PathArgument>();
158 final SchemaNode node;
160 var parentNode = DataSchemaContextTree.from(schemaContext).getRoot();
162 final var parentSchema = parentNode.getDataSchemaNode();
163 if (parentSchema instanceof ActionNodeContainer) {
164 final var optAction = ((ActionNodeContainer) parentSchema).findAction(qname);
165 if (optAction.isPresent()) {
167 throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
168 + "therefore it must not continue past it", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
170 if (step instanceof ListInstance) {
171 throw new RestconfDocumentedException("Request path resolves to action '" + qname + "' and "
172 + "therefore it must not contain key values",
173 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
176 // Legacy behavior: Action's path should not include its path, but the rest of the code expects it
177 path.add(new NodeIdentifier(qname));
178 stack.enterSchemaTree(qname);
179 node = optAction.orElseThrow();
184 // Resolve the child step with respect to data schema tree
185 final var found = RestconfDocumentedException.throwIfNull(parentNode.enterChild(stack, qname),
186 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "Schema for '%s' not found", qname);
188 // Now add all mixins encountered to the path
189 var childNode = found;
190 while (childNode.isMixin()) {
191 path.add(childNode.getIdentifier());
192 childNode = verifyNotNull(childNode.enterChild(stack, qname),
193 "Mixin %s is missing child for %s while resolving %s", childNode, qname, found);
196 final PathArgument pathArg;
197 if (step instanceof ListInstance) {
198 final var values = ((ListInstance) step).keyValues();
199 final var schema = childNode.getDataSchemaNode();
200 pathArg = schema instanceof ListSchemaNode
201 ? prepareNodeWithPredicates(stack, qname, (ListSchemaNode) schema, values)
202 : prepareNodeWithValue(stack, qname, schema, values);
204 RestconfDocumentedException.throwIf(childNode.isKeyedEntry(),
205 ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
206 "Entry '%s' requires key or value predicate to be present.", qname);
207 pathArg = childNode.getIdentifier();
213 node = childNode.getDataSchemaNode();
217 parentNode = childNode;
219 final var module = step.module();
220 if (module != null) {
221 namespace = resolveNamespace(module);
224 qname = step.identifier().bindTo(namespace);
227 return new Result(path, stack, node);
230 private NodeIdentifierWithPredicates prepareNodeWithPredicates(final SchemaInferenceStack stack, final QName qname,
231 final @NonNull ListSchemaNode schema, final List<@NonNull String> keyValues) {
232 final var keyDef = schema.getKeyDefinition();
233 final var keySize = keyDef.size();
234 final var varSize = keyValues.size();
235 if (keySize != varSize) {
236 throw new RestconfDocumentedException(
237 "Schema for " + qname + " requires " + keySize + " key values, " + varSize + " supplied",
238 ErrorType.PROTOCOL, keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE);
241 final var values = ImmutableMap.<QName, Object>builderWithExpectedSize(keySize);
242 final var tmp = stack.copy();
243 for (int i = 0; i < keySize; ++i) {
244 final QName keyName = keyDef.get(i);
245 final var child = schema.getDataChildByName(keyName);
246 tmp.enterSchemaTree(keyName);
247 values.put(keyName, prepareValueByType(tmp, child, keyValues.get(i)));
251 return NodeIdentifierWithPredicates.of(qname, values.build());
254 private Object prepareValueByType(final SchemaInferenceStack stack, final DataSchemaNode schemaNode,
255 final @NonNull String value) {
257 TypeDefinition<? extends TypeDefinition<?>> typedef;
258 if (schemaNode instanceof LeafListSchemaNode) {
259 typedef = ((LeafListSchemaNode) schemaNode).getType();
261 typedef = ((LeafSchemaNode) schemaNode).getType();
263 if (typedef instanceof LeafrefTypeDefinition) {
264 typedef = stack.resolveLeafref((LeafrefTypeDefinition) typedef);
267 if (typedef instanceof IdentityrefTypeDefinition) {
268 return toIdentityrefQName(value, schemaNode);
272 if (typedef instanceof InstanceIdentifierTypeDefinition) {
273 return new StringModuleInstanceIdentifierCodec(schemaContext).deserialize(value);
276 return verifyNotNull(TypeDefinitionAwareCodec.from(typedef),
277 "Unhandled type %s decoding %s", typedef, value).deserialize(value);
278 } catch (IllegalArgumentException e) {
279 throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
280 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
284 private NodeWithValue<?> prepareNodeWithValue(final SchemaInferenceStack stack, final QName qname,
285 final DataSchemaNode schema, final List<String> keyValues) {
286 // TODO: qname should be always equal to schema.getQName(), right?
287 return new NodeWithValue<>(qname, prepareValueByType(stack, schema,
288 // FIXME: ahem: we probably want to do something differently here
292 private QName toIdentityrefQName(final String value, final DataSchemaNode schemaNode) {
293 final QNameModule namespace;
294 final String localName;
295 final int firstColon = value.indexOf(':');
296 if (firstColon != -1) {
297 namespace = resolveNamespace(value.substring(0, firstColon));
298 localName = value.substring(firstColon + 1);
300 namespace = schemaNode.getQName().getModule();
304 return schemaContext.getModuleStatement(namespace)
305 .streamEffectiveSubstatements(IdentityEffectiveStatement.class)
306 .map(IdentityEffectiveStatement::argument)
307 .filter(qname -> localName.equals(qname.getLocalName()))
309 .orElseThrow(() -> new RestconfDocumentedException(
310 "No identity found for '" + localName + "' in namespace " + namespace,
311 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
314 private @NonNull QNameModule resolveNamespace(final String moduleName) {
315 final var modules = schemaContext.findModules(moduleName);
316 RestconfDocumentedException.throwIf(modules.isEmpty(), ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT,
317 "Failed to lookup for module with name '%s'.", moduleName);
318 return modules.iterator().next().getQNameModule();