Remove the option to deliver streams over WebSockets
[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.base.VerifyException;
15 import com.google.common.collect.ImmutableMap;
16 import java.util.ArrayList;
17 import java.util.Iterator;
18 import java.util.List;
19 import org.eclipse.jdt.annotation.NonNull;
20 import org.opendaylight.restconf.api.ApiPath;
21 import org.opendaylight.restconf.api.ApiPath.ListInstance;
22 import org.opendaylight.restconf.api.ApiPath.Step;
23 import org.opendaylight.restconf.nb.rfc8040.Insert.PointNormalizer;
24 import org.opendaylight.restconf.server.api.DatabindAware;
25 import org.opendaylight.restconf.server.api.DatabindContext;
26 import org.opendaylight.restconf.server.api.DatabindPath;
27 import org.opendaylight.restconf.server.api.DatabindPath.Action;
28 import org.opendaylight.restconf.server.api.DatabindPath.Data;
29 import org.opendaylight.restconf.server.api.DatabindPath.InstanceReference;
30 import org.opendaylight.restconf.server.api.DatabindPath.Rpc;
31 import org.opendaylight.restconf.server.api.ServerException;
32 import org.opendaylight.yangtools.yang.common.ErrorTag;
33 import org.opendaylight.yangtools.yang.common.ErrorType;
34 import org.opendaylight.yangtools.yang.common.QName;
35 import org.opendaylight.yangtools.yang.common.QNameModule;
36 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
37 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
38 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
39 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
40 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
41 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
42 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
43 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
47 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
48 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
49 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
50
51 /**
52  * Utility for normalizing {@link ApiPath}s. An {@link ApiPath} can represent a number of different constructs, as
53  * denoted to in the {@link DatabindPath} interface hierarchy.
54  *
55  * <p>
56  * This process is governed by
57  * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.5.3">RFC8040, section 3.5.3</a>. The URI provides the
58  * equivalent of NETCONF XML filter encoding, with data values being escaped RFC7891 strings.
59  */
60 public final class ApiPathNormalizer implements DatabindAware, PointNormalizer {
61     private final @NonNull DatabindContext databind;
62
63     public ApiPathNormalizer(final DatabindContext databind) {
64         this.databind = requireNonNull(databind);
65     }
66
67     @Override
68     public DatabindContext databind() {
69         return databind;
70     }
71
72     public @NonNull DatabindPath normalizePath(final ApiPath apiPath) throws ServerException {
73         final var it = apiPath.steps().iterator();
74         if (!it.hasNext()) {
75             return new Data(databind);
76         }
77
78         // First step is somewhat special:
79         // - it has to contain a module qualifier
80         // - it has to consider RPCs, for which we need SchemaContext
81         //
82         // We therefore peel that first iteration here and not worry about those details in further iterations
83         final var firstStep = it.next();
84         final var firstModule = firstStep.module();
85         if (firstModule == null) {
86             throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
87                 "First member must use namespace-qualified form, '%s' does not", firstStep.identifier());
88         }
89
90         var namespace = resolveNamespace(firstModule);
91         var step = firstStep;
92         var qname = step.identifier().bindTo(namespace);
93
94         // We go through more modern APIs here to get this special out of the way quickly
95         final var modelContext = databind.modelContext();
96         final var optRpc = modelContext.findModuleStatement(namespace).orElseThrow()
97             .findSchemaTreeNode(RpcEffectiveStatement.class, qname);
98         if (optRpc.isPresent()) {
99             final var rpc = optRpc.orElseThrow();
100
101             // We have found an RPC match,
102             if (it.hasNext()) {
103                 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
104                     "First step in the path resolves to RPC '%s' and therefore it must be the only step present",
105                     qname);
106             }
107             if (step instanceof ListInstance) {
108                 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
109                     "First step in the path resolves to RPC '%s' and therefore it must not contain key values", qname);
110             }
111
112             final var stack = SchemaInferenceStack.of(modelContext);
113             final var stmt = stack.enterSchemaTree(rpc.argument());
114             verify(rpc.equals(stmt), "Expecting %s, inferred %s", rpc, stmt);
115             return new Rpc(databind, stack.toInference(), rpc);
116         }
117
118         return normalizeSteps(SchemaInferenceStack.of(modelContext), databind.schemaTree().getRoot(), List.of(),
119             namespace, firstStep, it);
120     }
121
122     @NonNull DatabindPath normalizeSteps(final SchemaInferenceStack stack, final @NonNull DataSchemaContext rootNode,
123             final @NonNull List<PathArgument> pathPrefix, final @NonNull QNameModule firstNamespace,
124             final @NonNull Step firstStep, final Iterator<@NonNull Step> it) throws ServerException {
125         var parentNode = rootNode;
126         var namespace = firstNamespace;
127         var step = firstStep;
128         var qname = step.identifier().bindTo(namespace);
129
130         final var path = new ArrayList<>(pathPrefix);
131         while (true) {
132             final var parentSchema = parentNode.dataSchemaNode();
133             if (parentSchema instanceof ActionNodeContainer actionParent) {
134                 final var optAction = actionParent.findAction(qname);
135                 if (optAction.isPresent()) {
136                     final var action = optAction.orElseThrow();
137
138                     if (it.hasNext()) {
139                         throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
140                             "Request path resolves to action '%s' and therefore it must not continue past it", qname);
141                     }
142                     if (step instanceof ListInstance) {
143                         throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
144                             "Request path resolves to action '%s' and therefore it must not contain key values", qname);
145                     }
146
147                     final var stmt = stack.enterSchemaTree(qname);
148                     final var actionStmt = action.asEffectiveStatement();
149                     verify(actionStmt.equals(stmt), "Expecting %s, inferred %s", actionStmt, stmt);
150
151                     return new Action(databind, stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
152                 }
153             }
154
155             // Resolve the child step with respect to data schema tree
156             final var found = parentNode instanceof DataSchemaContext.Composite composite
157                 ? composite.enterChild(stack, qname) : null;
158             if (found == null) {
159                 throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "Schema for '%s' not found",
160                     qname);
161             }
162
163             // Now add all mixins encountered to the path
164             var childNode = found;
165             while (childNode instanceof PathMixin currentMixin) {
166                 path.add(currentMixin.mixinPathStep());
167                 childNode = verifyNotNull(currentMixin.enterChild(stack, qname),
168                     "Mixin %s is missing child for %s while resolving %s", childNode, qname, found);
169             }
170
171             final PathArgument pathArg;
172             if (step instanceof ListInstance listStep) {
173                 final var values = listStep.keyValues();
174                 final var schema = childNode.dataSchemaNode();
175                 if (schema instanceof ListSchemaNode listSchema) {
176                     pathArg = prepareNodeWithPredicates(stack, qname, listSchema, values);
177                 } else if (schema instanceof LeafListSchemaNode leafListSchema) {
178                     if (values.size() != 1) {
179                         throw new ServerException(ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE,
180                             "Entry '%s' requires one value predicate.", qname);
181                     }
182                     pathArg = new NodeWithValue<>(qname, parserJsonValue(stack, leafListSchema, values.get(0)));
183                 } else {
184                     throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
185                         "Entry '%s' does not take a key or value predicate.", qname);
186                 }
187             } else {
188                 if (childNode.dataSchemaNode() instanceof ListSchemaNode list && !list.getKeyDefinition().isEmpty()) {
189                     throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
190                         "Entry '%s' requires key or value predicate to be present.", qname);
191                 }
192                 pathArg = childNode.getPathStep();
193             }
194
195             path.add(pathArg);
196
197             if (!it.hasNext()) {
198                 return new Data(databind, stack.toInference(), YangInstanceIdentifier.of(path), childNode);
199             }
200
201             parentNode = childNode;
202             step = it.next();
203             final var module = step.module();
204             if (module != null) {
205                 namespace = resolveNamespace(module);
206             }
207
208             qname = step.identifier().bindTo(namespace);
209         }
210     }
211
212     public @NonNull Data normalizeDataPath(final ApiPath apiPath) throws ServerException {
213         final var path = normalizePath(apiPath);
214         if (path instanceof Data dataPath) {
215             return dataPath;
216         }
217         throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "Point '%s' resolves to non-data %s",
218             apiPath, path);
219     }
220
221     @Override
222     public PathArgument normalizePoint(final ApiPath value) throws ServerException {
223         final var path = normalizePath(value);
224         if (path instanceof Data dataPath) {
225             final var lastArg = dataPath.instance().getLastPathArgument();
226             if (lastArg != null) {
227                 return lastArg;
228             }
229             throw new ServerException(ErrorType.PROTOCOL,  ErrorTag.DATA_MISSING,
230                 "Point '%s' resolves to an empty path", value);
231         }
232         throw new ServerException(ErrorType.PROTOCOL,  ErrorTag.DATA_MISSING, "Point '%s' resolves to non-data %s",
233             value, path);
234     }
235
236     public @NonNull Rpc normalizeRpcPath(final ApiPath apiPath) throws ServerException {
237         final var steps = apiPath.steps();
238         return switch (steps.size()) {
239             case 0 -> throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "RPC name must be present");
240             case 1 -> normalizeRpcPath(steps.get(0));
241             default -> throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING,
242                 "%s does not refer to an RPC", apiPath);
243         };
244     }
245
246     public @NonNull Rpc normalizeRpcPath(final ApiPath.Step step) throws ServerException {
247         final var firstModule = step.module();
248         if (firstModule == null) {
249             throw new ServerException(ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE,
250                 "First member must use namespace-qualified form, '%s' does not", step.identifier());
251         }
252
253         final var namespace = resolveNamespace(firstModule);
254         final var qname = step.identifier().bindTo(namespace);
255         final var stack = SchemaInferenceStack.of(databind.modelContext());
256         final SchemaTreeEffectiveStatement<?> stmt;
257         try {
258             stmt = stack.enterSchemaTree(qname);
259         } catch (IllegalArgumentException e) {
260             throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, qname + " does not refer to an RPC",
261                 e);
262         }
263         if (stmt instanceof RpcEffectiveStatement rpc) {
264             return new Rpc(databind, stack.toInference(), rpc);
265         }
266         throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "%s does not refer to an RPC", qname);
267     }
268
269     public @NonNull InstanceReference normalizeDataOrActionPath(final ApiPath apiPath) throws ServerException {
270         // FIXME: optimize this
271         final var path = normalizePath(apiPath);
272         if (path instanceof Data dataPath) {
273             return dataPath;
274         }
275         if (path instanceof Action actionPath) {
276             return actionPath;
277         }
278         throw new ServerException(ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, "Unexpected path %s", path);
279     }
280
281     private NodeIdentifierWithPredicates prepareNodeWithPredicates(final SchemaInferenceStack stack, final QName qname,
282             final @NonNull ListSchemaNode schema, final List<@NonNull String> keyValues) throws ServerException {
283         final var keyDef = schema.getKeyDefinition();
284         final var keySize = keyDef.size();
285         final var varSize = keyValues.size();
286         if (keySize != varSize) {
287             throw new ServerException(ErrorType.PROTOCOL,
288                 keySize > varSize ? ErrorTag.MISSING_ATTRIBUTE : ErrorTag.UNKNOWN_ATTRIBUTE,
289                 "Schema for %s requires %s key values, %s supplied", qname, keySize, varSize);
290         }
291
292         final var values = ImmutableMap.<QName, Object>builderWithExpectedSize(keySize);
293         final var tmp = stack.copy();
294         for (int i = 0; i < keySize; ++i) {
295             final QName keyName = keyDef.get(i);
296             final var child = schema.getDataChildByName(keyName);
297             tmp.enterSchemaTree(keyName);
298             values.put(keyName, prepareValueByType(tmp, child, keyValues.get(i)));
299             tmp.exit();
300         }
301
302         return NodeIdentifierWithPredicates.of(qname, values.build());
303     }
304
305     private Object prepareValueByType(final SchemaInferenceStack stack, final DataSchemaNode schemaNode,
306             final @NonNull String value) throws ServerException {
307         if (schemaNode instanceof TypedDataSchemaNode typedSchema) {
308             return parserJsonValue(stack, typedSchema, value);
309         }
310         throw new VerifyException("Unhandled schema " + schemaNode + " decoding '" + value + "'");
311     }
312
313     private Object parserJsonValue(final SchemaInferenceStack stack, final TypedDataSchemaNode schemaNode,
314             final String value) throws ServerException {
315         // As per https://www.rfc-editor.org/rfc/rfc8040#page-29:
316         //            The syntax for
317         //            "api-identifier" and "key-value" MUST conform to the JSON identifier
318         //            encoding rules in Section 4 of [RFC7951]: The RESTCONF root resource
319         //            path is required.  Additional sub-resource identifiers are optional.
320         //            The characters in a key value string are constrained, and some
321         //            characters need to be percent-encoded, as described in Section 3.5.3.
322         try {
323             return databind.jsonCodecs().codecFor(schemaNode, stack).parseValue(value);
324         } catch (IllegalArgumentException e) {
325             throw new ServerException(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
326                 "Invalid value '" + value + "' for " + schemaNode.getQName(), e);
327         }
328     }
329
330     private @NonNull QNameModule resolveNamespace(final String moduleName) throws ServerException {
331         final var it = databind.modelContext().findModuleStatements(moduleName).iterator();
332         if (it.hasNext()) {
333             return it.next().localQNameModule();
334         }
335         throw new ServerException(ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT,
336             "Failed to lookup for module with name '%s'.", moduleName);
337     }
338 }