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.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
13 import com.google.common.collect.ImmutableList;
14 import com.google.common.collect.ImmutableMap;
15 import java.text.ParseException;
16 import java.util.ArrayList;
17 import java.util.List;
18 import java.util.Optional;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
21 import org.opendaylight.restconf.common.util.RestUtil;
22 import org.opendaylight.restconf.nb.rfc8040.ApiPath;
23 import org.opendaylight.restconf.nb.rfc8040.ApiPath.ListInstance;
24 import org.opendaylight.restconf.nb.rfc8040.ApiPath.Step;
25 import org.opendaylight.restconf.nb.rfc8040.codecs.RestCodec;
26 import org.opendaylight.yangtools.yang.common.ErrorTag;
27 import org.opendaylight.yangtools.yang.common.ErrorType;
28 import org.opendaylight.yangtools.yang.common.QName;
29 import org.opendaylight.yangtools.yang.common.QNameModule;
30 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
31 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
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.util.DataSchemaContextNode;
36 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree;
37 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
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.LeafListSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
45 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
46 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
47 import org.opendaylight.yangtools.yang.model.api.stmt.IdentityEffectiveStatement;
48 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
49 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
50 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
53 * Deserializer for {@link String} to {@link YangInstanceIdentifier} for restconf.
55 public final class YangInstanceIdentifierDeserializer {
56 private final @NonNull EffectiveModelContext schemaContext;
57 private final @NonNull ApiPath apiPath;
59 private YangInstanceIdentifierDeserializer(final EffectiveModelContext schemaContext, final ApiPath apiPath) {
60 this.schemaContext = requireNonNull(schemaContext);
61 this.apiPath = requireNonNull(apiPath);
65 * Method to create {@link List} from {@link PathArgument} which are parsing from data by {@link SchemaContext}.
67 * @param schemaContext for validate of parsing path arguments
68 * @param data path to data, in URL string form
69 * @return {@link Iterable} of {@link PathArgument}
70 * @throws RestconfDocumentedException the path is not valid
72 public static List<PathArgument> create(final EffectiveModelContext schemaContext, final String data) {
75 path = ApiPath.parse(requireNonNull(data));
76 } catch (ParseException e) {
77 throw new RestconfDocumentedException("Invalid path '" + data + "' at offset " + e.getErrorOffset(),
78 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e);
80 return create(schemaContext, path);
83 public static List<PathArgument> create(final EffectiveModelContext schemaContext, final ApiPath path) {
84 return new YangInstanceIdentifierDeserializer(schemaContext, path).parse();
87 private ImmutableList<PathArgument> parse() {
88 final var steps = apiPath.steps();
89 if (steps.isEmpty()) {
90 return ImmutableList.of();
93 final List<PathArgument> path = new ArrayList<>();
94 DataSchemaContextNode<?> parentNode = DataSchemaContextTree.from(schemaContext).getRoot();
95 QNameModule parentNs = null;
96 for (Step step : steps) {
97 final var module = step.module();
100 if (parentNs == null) {
101 throw new RestconfDocumentedException(
102 "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
103 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
107 ns = resolveNamespace(module);
110 final QName qname = step.identifier().bindTo(ns);
111 final DataSchemaContextNode<?> childNode = nextContextNode(parentNode, qname, path);
112 final PathArgument pathArg;
113 if (step instanceof ListInstance) {
114 final var values = ((ListInstance) step).keyValues();
115 final var schema = childNode.getDataSchemaNode();
116 pathArg = schema instanceof ListSchemaNode
117 ? prepareNodeWithPredicates(qname, (ListSchemaNode) schema, values)
118 : prepareNodeWithValue(qname, schema, values);
119 } else if (childNode != null) {
120 RestconfDocumentedException.throwIf(childNode.isKeyedEntry(),
121 ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
122 "Entry '%s' requires key or value predicate to be present.", qname);
123 pathArg = childNode.getIdentifier();
125 // FIXME: this should be a hard error here, as we cannot resolve the node correctly!
126 pathArg = NodeIdentifier.create(qname);
130 parentNode = childNode;
134 return ImmutableList.copyOf(path);
137 private NodeIdentifierWithPredicates prepareNodeWithPredicates(final QName qname,
138 final @NonNull ListSchemaNode schema, final List<@NonNull String> keyValues) {
139 final var keyDef = schema.getKeyDefinition();
140 final var keySize = keyDef.size();
141 final var varSize = keyValues.size();
142 if (keySize != varSize) {
143 throw new RestconfDocumentedException(
144 "Schema for " + qname + " requires " + keySize + " key values, " + varSize + " supplied",
145 ErrorType.PROTOCOL, keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE);
148 final var values = ImmutableMap.<QName, Object>builderWithExpectedSize(keySize);
149 for (int i = 0; i < keySize; ++i) {
150 final QName keyName = keyDef.get(i);
151 values.put(keyName, prepareValueByType(schema.getDataChildByName(keyName), keyValues.get(i)));
154 return NodeIdentifierWithPredicates.of(qname, values.build());
157 private Object prepareValueByType(final DataSchemaNode schemaNode, final @NonNull String value) {
159 TypeDefinition<? extends TypeDefinition<?>> typedef;
160 if (schemaNode instanceof LeafListSchemaNode) {
161 typedef = ((LeafListSchemaNode) schemaNode).getType();
163 typedef = ((LeafSchemaNode) schemaNode).getType();
165 final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
166 if (baseType instanceof LeafrefTypeDefinition) {
167 typedef = SchemaInferenceStack.ofInstantiatedPath(schemaContext, schemaNode.getPath())
168 .resolveLeafref((LeafrefTypeDefinition) baseType);
171 if (typedef instanceof IdentityrefTypeDefinition) {
172 return toIdentityrefQName(value, schemaNode);
175 return RestCodec.from(typedef, null, schemaContext).deserialize(value);
176 } catch (IllegalArgumentException e) {
177 throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
178 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
182 private NodeWithValue<?> prepareNodeWithValue(final QName qname, final DataSchemaNode schema,
183 final List<String> keyValues) {
184 // TODO: qname should be always equal to schema.getQName(), right?
185 return new NodeWithValue<>(qname, prepareValueByType(schema,
186 // FIXME: ahem: we probably want to do something differently here
190 private DataSchemaContextNode<?> nextContextNode(final DataSchemaContextNode<?> parent, final QName qname,
191 final List<PathArgument> path) {
192 final var found = parent.getChild(qname);
194 // FIXME: why are we making this special case here, especially with ...
195 final var module = schemaContext.findModule(qname.getModule());
196 if (module.isPresent()) {
197 for (final RpcDefinition rpcDefinition : module.get().getRpcs()) {
198 // ... this comparison?
199 if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
204 if (findActionDefinition(parent.getDataSchemaNode(), qname.getLocalName()).isPresent()) {
208 throw new RestconfDocumentedException("Schema for '" + qname + "' not found",
209 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
213 while (result.isMixin()) {
214 path.add(result.getIdentifier());
215 result = verifyNotNull(found.getChild(qname), "Mixin %s is missing child for %s while resolving %s",
216 result, qname, found);
221 private static Optional<? extends ActionDefinition> findActionDefinition(final DataSchemaNode dataSchemaNode,
222 // FIXME: this should be using a namespace
223 final String nodeName) {
224 if (dataSchemaNode instanceof ActionNodeContainer) {
225 return ((ActionNodeContainer) dataSchemaNode).getActions().stream()
226 .filter(actionDef -> actionDef.getQName().getLocalName().equals(nodeName))
229 return Optional.empty();
232 private QName toIdentityrefQName(final String value, final DataSchemaNode schemaNode) {
233 final QNameModule namespace;
234 final String localName;
235 final int firstColon = value.indexOf(':');
236 if (firstColon != -1) {
237 namespace = resolveNamespace(value.substring(0, firstColon));
238 localName = value.substring(firstColon + 1);
240 namespace = schemaNode.getQName().getModule();
244 return schemaContext.getModuleStatement(namespace)
245 .streamEffectiveSubstatements(IdentityEffectiveStatement.class)
246 .map(IdentityEffectiveStatement::argument)
247 .filter(qname -> localName.equals(qname.getLocalName()))
249 .orElseThrow(() -> new RestconfDocumentedException(
250 "No identity found for '" + localName + "' in namespace " + namespace,
251 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
254 private @NonNull QNameModule resolveNamespace(final String moduleName) {
255 final var modules = schemaContext.findModules(moduleName);
256 RestconfDocumentedException.throwIf(modules.isEmpty(), ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT,
257 "Failed to lookup for module with name '%s'.", moduleName);
258 return modules.iterator().next().getQNameModule();