Eliminate use of SchemaNode.getPath() in YII deserializer
[netconf.git] / restconf / restconf-nb-rfc8040 / src / main / java / org / opendaylight / restconf / nb / rfc8040 / utils / parser / YangInstanceIdentifierDeserializer.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.nb.rfc8040.utils.parser;
9
10 import static com.google.common.base.Verify.verifyNotNull;
11 import static java.util.Objects.requireNonNull;
12
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.stmt.SchemaNodeIdentifier.Absolute;
49 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
50 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
51 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
52
53 /**
54  * Deserializer for {@link String} to {@link YangInstanceIdentifier} for restconf.
55  */
56 public final class YangInstanceIdentifierDeserializer {
57     private final @NonNull EffectiveModelContext schemaContext;
58     private final @NonNull ApiPath apiPath;
59
60     private YangInstanceIdentifierDeserializer(final EffectiveModelContext schemaContext, final ApiPath apiPath) {
61         this.schemaContext = requireNonNull(schemaContext);
62         this.apiPath = requireNonNull(apiPath);
63     }
64
65     /**
66      * Method to create {@link List} from {@link PathArgument} which are parsing from data by {@link SchemaContext}.
67      *
68      * @param schemaContext for validate of parsing path arguments
69      * @param data path to data, in URL string form
70      * @return {@link Iterable} of {@link PathArgument}
71      * @throws RestconfDocumentedException the path is not valid
72      */
73     public static List<PathArgument> create(final EffectiveModelContext schemaContext, final String data) {
74         final ApiPath path;
75         try {
76             path = ApiPath.parse(requireNonNull(data));
77         } catch (ParseException e) {
78             throw new RestconfDocumentedException("Invalid path '" + data + "' at offset " + e.getErrorOffset(),
79                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, e);
80         }
81         return create(schemaContext, path);
82     }
83
84     public static List<PathArgument> create(final EffectiveModelContext schemaContext, final ApiPath path) {
85         return new YangInstanceIdentifierDeserializer(schemaContext, path).parse();
86     }
87
88     private ImmutableList<PathArgument> parse() {
89         final var steps = apiPath.steps();
90         if (steps.isEmpty()) {
91             return ImmutableList.of();
92         }
93
94         final List<PathArgument> path = new ArrayList<>();
95         DataSchemaContextNode<?> parentNode = DataSchemaContextTree.from(schemaContext).getRoot();
96         QNameModule parentNs = null;
97         for (Step step : steps) {
98             final var module = step.module();
99             final QNameModule ns;
100             if (module == null) {
101                 if (parentNs == null) {
102                     throw new RestconfDocumentedException(
103                         "First member must use namespace-qualified form, '" + step.identifier() + "' does not",
104                         ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
105                 }
106                 ns = parentNs;
107             } else {
108                 ns = resolveNamespace(module);
109             }
110
111             final QName qname = step.identifier().bindTo(ns);
112             final DataSchemaContextNode<?> childNode = nextContextNode(parentNode, qname, path);
113             final PathArgument pathArg;
114             if (step instanceof ListInstance) {
115                 final var values = ((ListInstance) step).keyValues();
116                 final var schema = childNode.getDataSchemaNode();
117                 final var absolute = Absolute.of(parentNode.getIdentifier().getNodeType(), qname);
118                 pathArg = schema instanceof ListSchemaNode
119                     ? prepareNodeWithPredicates(qname, (ListSchemaNode) schema, absolute, values)
120                         : prepareNodeWithValue(qname, schema, absolute, values);
121             } else if (childNode != null) {
122                 RestconfDocumentedException.throwIf(childNode.isKeyedEntry(),
123                     ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
124                     "Entry '%s' requires key or value predicate to be present.", qname);
125                 pathArg = childNode.getIdentifier();
126             } else {
127                 // FIXME: this should be a hard error here, as we cannot resolve the node correctly!
128                 pathArg = NodeIdentifier.create(qname);
129             }
130
131             path.add(pathArg);
132             parentNode = childNode;
133             parentNs = ns;
134         }
135
136         return ImmutableList.copyOf(path);
137     }
138
139     private NodeIdentifierWithPredicates prepareNodeWithPredicates(final QName qname,
140             final @NonNull ListSchemaNode schema, final Absolute absolute, final List<@NonNull String> keyValues) {
141         final var keyDef = schema.getKeyDefinition();
142         final var keySize = keyDef.size();
143         final var varSize = keyValues.size();
144         if (keySize != varSize) {
145             throw new RestconfDocumentedException(
146                 "Schema for " + qname + " requires " + keySize + " key values, " + varSize + " supplied",
147                 ErrorType.PROTOCOL, keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE);
148         }
149
150         final var values = ImmutableMap.<QName, Object>builderWithExpectedSize(keySize);
151         for (int i = 0; i < keySize; ++i) {
152             final QName keyName = keyDef.get(i);
153             final List<QName> qNames = new ArrayList<>(absolute.getNodeIdentifiers());
154             qNames.add(keyName);
155             final Absolute path = Absolute.of(qNames);
156             values.put(keyName, prepareValueByType(schema.getDataChildByName(keyName), path, keyValues.get(i)));
157         }
158
159         return NodeIdentifierWithPredicates.of(qname, values.build());
160     }
161
162     private Object prepareValueByType(final DataSchemaNode schemaNode, final Absolute absolute,
163             final @NonNull String value) {
164         TypeDefinition<? extends TypeDefinition<?>> typedef;
165         if (schemaNode instanceof LeafListSchemaNode) {
166             typedef = ((LeafListSchemaNode) schemaNode).getType();
167         } else {
168             typedef = ((LeafSchemaNode) schemaNode).getType();
169         }
170         final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef);
171         if (baseType instanceof LeafrefTypeDefinition) {
172             typedef = SchemaInferenceStack.of(schemaContext, absolute).resolveLeafref((LeafrefTypeDefinition) baseType);
173         }
174
175         if (typedef instanceof IdentityrefTypeDefinition) {
176             return toIdentityrefQName(value, schemaNode);
177         }
178         try {
179             return RestCodec.from(typedef, null, schemaContext).deserialize(value);
180         } catch (IllegalArgumentException e) {
181             throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
182                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
183         }
184     }
185
186     private NodeWithValue<?> prepareNodeWithValue(final QName qname, final DataSchemaNode schema,
187             final Absolute absolute, final List<String> keyValues) {
188         // TODO: qname should be always equal to schema.getQName(), right?
189         return new NodeWithValue<>(qname, prepareValueByType(schema, absolute,
190             // FIXME: ahem: we probably want to do something differently here
191             keyValues.get(0)));
192     }
193
194     private DataSchemaContextNode<?> nextContextNode(final DataSchemaContextNode<?> parent, final QName qname,
195             final List<PathArgument> path) {
196         final var found = parent.getChild(qname);
197         if (found == null) {
198             // FIXME: why are we making this special case here, especially with ...
199             final var module = schemaContext.findModule(qname.getModule());
200             if (module.isPresent()) {
201                 for (final RpcDefinition rpcDefinition : module.get().getRpcs()) {
202                     // ... this comparison?
203                     if (rpcDefinition.getQName().getLocalName().equals(qname.getLocalName())) {
204                         return null;
205                     }
206                 }
207             }
208             if (findActionDefinition(parent.getDataSchemaNode(), qname.getLocalName()).isPresent()) {
209                 return null;
210             }
211
212             throw new RestconfDocumentedException("Schema for '" + qname + "' not found",
213                 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
214         }
215
216         var result = found;
217         while (result.isMixin()) {
218             path.add(result.getIdentifier());
219             result = verifyNotNull(result.getChild(qname), "Mixin %s is missing child for %s while resolving %s",
220                 result, qname, found);
221         }
222         return result;
223     }
224
225     private static Optional<? extends ActionDefinition> findActionDefinition(final DataSchemaNode dataSchemaNode,
226             // FIXME: this should be using a namespace
227             final String nodeName) {
228         if (dataSchemaNode instanceof ActionNodeContainer) {
229             return ((ActionNodeContainer) dataSchemaNode).getActions().stream()
230                     .filter(actionDef -> actionDef.getQName().getLocalName().equals(nodeName))
231                     .findFirst();
232         }
233         return Optional.empty();
234     }
235
236     private QName toIdentityrefQName(final String value, final DataSchemaNode schemaNode) {
237         final QNameModule namespace;
238         final String localName;
239         final int firstColon = value.indexOf(':');
240         if (firstColon != -1) {
241             namespace = resolveNamespace(value.substring(0, firstColon));
242             localName = value.substring(firstColon + 1);
243         } else {
244             namespace = schemaNode.getQName().getModule();
245             localName = value;
246         }
247
248         return schemaContext.getModuleStatement(namespace)
249             .streamEffectiveSubstatements(IdentityEffectiveStatement.class)
250             .map(IdentityEffectiveStatement::argument)
251             .filter(qname -> localName.equals(qname.getLocalName()))
252             .findFirst()
253             .orElseThrow(() -> new RestconfDocumentedException(
254                 "No identity found for '" + localName + "' in namespace " + namespace,
255                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
256     }
257
258     private @NonNull QNameModule resolveNamespace(final String moduleName) {
259         final var modules = schemaContext.findModules(moduleName);
260         RestconfDocumentedException.throwIf(modules.isEmpty(), ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT,
261             "Failed to lookup for module with name '%s'.", moduleName);
262         return modules.iterator().next().getQNameModule();
263     }
264 }