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