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