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