Added support for /modules, /modules/module, /operations resources
[controller.git] / opendaylight / md-sal / sal-rest-connector / src / main / java / org / opendaylight / controller / sal / restconf / impl / ControllerContext.xtend
1 /*
2  * Copyright (c) 2014 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.controller.sal.restconf.impl
9
10 import com.google.common.base.Preconditions
11 import com.google.common.collect.BiMap
12 import com.google.common.collect.FluentIterable
13 import com.google.common.collect.HashBiMap
14 import java.net.URI
15 import java.net.URLDecoder
16 import java.net.URLEncoder
17 import java.util.ArrayList
18 import java.util.HashMap
19 import java.util.List
20 import java.util.Map
21 import java.util.concurrent.ConcurrentHashMap
22 import org.opendaylight.controller.sal.core.api.mount.MountInstance
23 import org.opendaylight.controller.sal.core.api.mount.MountService
24 import org.opendaylight.controller.sal.rest.impl.RestUtil
25 import org.opendaylight.controller.sal.rest.impl.RestconfProvider
26 import org.opendaylight.yangtools.yang.common.QName
27 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier
28 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.InstanceIdentifierBuilder
29 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeIdentifier
30 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeIdentifierWithPredicates
31 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.PathArgument
32 import org.opendaylight.yangtools.yang.data.impl.codec.TypeDefinitionAwareCodec
33 import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode
34 import org.opendaylight.yangtools.yang.model.api.ChoiceNode
35 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode
36 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer
37 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode
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.Module
42 import org.opendaylight.yangtools.yang.model.api.RpcDefinition
43 import org.opendaylight.yangtools.yang.model.api.SchemaContext
44 import org.opendaylight.yangtools.yang.model.api.SchemaServiceListener
45 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition
46 import org.slf4j.LoggerFactory
47
48 import static com.google.common.base.Preconditions.*
49 import static javax.ws.rs.core.Response.Status.*
50
51 class ControllerContext implements SchemaServiceListener {
52     val static LOG = LoggerFactory.getLogger(ControllerContext)
53     val static ControllerContext INSTANCE = new ControllerContext
54     val static NULL_VALUE = "null"
55     val static MOUNT_MODULE = "yang-ext"
56     val static MOUNT_NODE = "mount"
57     public val static MOUNT = "yang-ext:mount"
58
59     @Property
60     var SchemaContext globalSchema;
61     
62     @Property
63     var MountService mountService;
64
65     private val BiMap<URI, String> uriToModuleName = HashBiMap.create();
66     private val Map<String, URI> moduleNameToUri = uriToModuleName.inverse();
67     private val Map<QName, RpcDefinition> qnameToRpc = new ConcurrentHashMap();
68
69     private new() {
70         if (INSTANCE !== null) {
71             throw new IllegalStateException("Already instantiated");
72         }
73     }
74
75     static def getInstance() {
76         return INSTANCE
77     }
78
79     private def void checkPreconditions() {
80         if (globalSchema === null) {
81             throw new ResponseException(SERVICE_UNAVAILABLE, RestconfProvider::NOT_INITALIZED_MSG)
82         }
83     }
84
85     def setSchemas(SchemaContext schemas) {
86         onGlobalContextUpdated(schemas)
87     }
88
89     def InstanceIdWithSchemaNode toInstanceIdentifier(String restconfInstance) {
90         return restconfInstance.toIdentifier(false)
91     }
92
93     def InstanceIdWithSchemaNode toMountPointIdentifier(String restconfInstance) {
94         return restconfInstance.toIdentifier(true)
95     }
96
97     private def InstanceIdWithSchemaNode toIdentifier(String restconfInstance, boolean toMountPointIdentifier) {
98         checkPreconditions
99         val pathArgs = restconfInstance.split("/");
100         if (pathArgs.empty) {
101             return null;
102         }
103         if (pathArgs.head.empty) {
104             pathArgs.remove(0)
105         }
106         val startModule = pathArgs.head.toModuleName();
107         if (startModule === null) {
108             throw new ResponseException(BAD_REQUEST, "First node in URI has to be in format \"moduleName:nodeName\"")
109         }
110         var InstanceIdWithSchemaNode iiWithSchemaNode = null;
111         if (toMountPointIdentifier) {
112             iiWithSchemaNode = collectPathArguments(InstanceIdentifier.builder(), pathArgs,
113             globalSchema.getLatestModule(startModule), null, true);
114         } else {
115             iiWithSchemaNode = collectPathArguments(InstanceIdentifier.builder(), pathArgs,
116             globalSchema.getLatestModule(startModule), null, false);
117         }
118         if (iiWithSchemaNode === null) {
119             throw new ResponseException(BAD_REQUEST, "URI has bad format")
120         }
121         return iiWithSchemaNode
122     }
123
124     private def getLatestModule(SchemaContext schema, String moduleName) {
125         checkArgument(schema !== null);
126         checkArgument(moduleName !== null && !moduleName.empty)
127         val modules = schema.modules.filter[m|m.name == moduleName]
128         return modules.filterLatestModule
129     }
130     
131     private def filterLatestModule(Iterable<Module> modules) {
132         var latestModule = modules.head
133         for (module : modules) {
134             if (module.revision.after(latestModule.revision)) {
135                 latestModule = module
136             }
137         }
138         return latestModule
139     }
140     
141     def findModuleByName(String moduleName) {
142         checkPreconditions
143         checkArgument(moduleName !== null && !moduleName.empty)
144         return globalSchema.getLatestModule(moduleName)
145     }
146     
147     def findModuleByName(MountInstance mountPoint, String moduleName) {
148         checkArgument(moduleName !== null && mountPoint !== null)
149         val mountPointSchema = mountPoint.schemaContext;
150         return mountPointSchema?.getLatestModule(moduleName);
151     }
152     
153     def findModuleByNamespace(URI namespace) {
154         checkPreconditions
155         checkArgument(namespace !== null)
156         val moduleSchemas = globalSchema.findModuleByNamespace(namespace)
157         return moduleSchemas?.filterLatestModule
158     }
159     
160     def findModuleByNamespace(MountInstance mountPoint, URI namespace) {
161         checkArgument(namespace !== null && mountPoint !== null)
162         val mountPointSchema = mountPoint.schemaContext;
163         val moduleSchemas = mountPointSchema?.findModuleByNamespace(namespace)
164         return moduleSchemas?.filterLatestModule
165     }
166
167     def findModuleByNameAndRevision(QName module) {
168         checkPreconditions
169         checkArgument(module !== null && module.localName !== null && module.revision !== null)
170         return globalSchema.findModuleByName(module.localName, module.revision)
171     }
172
173     def findModuleByNameAndRevision(MountInstance mountPoint, QName module) {
174         checkPreconditions
175         checkArgument(module !== null && module.localName !== null && module.revision !== null && mountPoint !== null)
176         return mountPoint.schemaContext?.findModuleByName(module.localName, module.revision)
177     }
178
179     def String toFullRestconfIdentifier(InstanceIdentifier path) {
180         checkPreconditions
181         val elements = path.path;
182         val ret = new StringBuilder();
183         val startQName = elements.get(0).nodeType;
184         val initialModule = globalSchema.findModuleByNamespaceAndRevision(startQName.namespace, startQName.revision)
185         var node = initialModule as DataSchemaNode;
186         for (element : elements) {
187             node = node.childByQName(element.nodeType);
188             ret.append(element.toRestconfIdentifier(node));
189         }
190         return ret.toString
191     }
192
193     private def dispatch CharSequence toRestconfIdentifier(NodeIdentifier argument, DataSchemaNode node) {
194         '''/«argument.nodeType.toRestconfIdentifier()»'''
195     }
196
197     private def dispatch CharSequence toRestconfIdentifier(NodeIdentifierWithPredicates argument, ListSchemaNode node) {
198         val nodeIdentifier = argument.nodeType.toRestconfIdentifier();
199         val keyValues = argument.keyValues;
200         return '''/«nodeIdentifier»/«FOR key : node.keyDefinition SEPARATOR "/"»«keyValues.get(key).toUriString»«ENDFOR»'''
201     }
202
203     private def dispatch CharSequence toRestconfIdentifier(PathArgument argument, DataSchemaNode node) {
204         throw new IllegalArgumentException("Conversion of generic path argument is not supported");
205     }
206
207     def findModuleNameByNamespace(URI namespace) {
208         checkPreconditions
209         var moduleName = uriToModuleName.get(namespace)
210         if (moduleName === null) {
211             val module = findModuleByNamespace(namespace)
212             if (module === null) return null
213             moduleName = module.name
214             uriToModuleName.put(namespace, moduleName)
215         }
216         return moduleName
217     }
218     
219     def findModuleNameByNamespace(MountInstance mountPoint, URI namespace) {
220         val module = mountPoint.findModuleByNamespace(namespace);
221         return module?.name
222     }
223
224     def findNamespaceByModuleName(String moduleName) {
225         var namespace = moduleNameToUri.get(moduleName)
226         if (namespace === null) {
227             var module = findModuleByName(moduleName)
228             if(module === null) return null
229             namespace = module.namespace
230             uriToModuleName.put(namespace, moduleName)
231         }
232         return namespace
233     }
234     
235     def findNamespaceByModuleName(MountInstance mountPoint, String moduleName) {
236         val module = mountPoint.findModuleByName(moduleName)
237         return module?.namespace
238     }
239
240     def getAllModules(MountInstance mountPoint) {
241         checkPreconditions
242         return mountPoint?.schemaContext?.modules
243     }
244     
245     def getAllModules() {
246         checkPreconditions
247         return globalSchema.modules
248     }
249
250     def CharSequence toRestconfIdentifier(QName qname) {
251         checkPreconditions
252         var module = uriToModuleName.get(qname.namespace)
253         if (module === null) {
254             val moduleSchema = globalSchema.findModuleByNamespaceAndRevision(qname.namespace, qname.revision);
255             if(moduleSchema === null) return null
256             uriToModuleName.put(qname.namespace, moduleSchema.name)
257             module = moduleSchema.name;
258         }
259         return '''«module»:«qname.localName»''';
260     }
261
262     def CharSequence toRestconfIdentifier(MountInstance mountPoint, QName qname) {
263         val moduleSchema = mountPoint?.schemaContext.findModuleByNamespaceAndRevision(qname.namespace, qname.revision);
264         if(moduleSchema === null) return null
265         val module = moduleSchema.name;
266         return '''«module»:«qname.localName»''';
267     }
268
269     private static dispatch def DataSchemaNode childByQName(ChoiceNode container, QName name) {
270         for (caze : container.cases) {
271             val ret = caze.childByQName(name)
272             if (ret !== null) {
273                 return ret;
274             }
275         }
276         return null;
277     }
278
279     private static dispatch def DataSchemaNode childByQName(ChoiceCaseNode container, QName name) {
280         val ret = container.getDataChildByName(name);
281         return ret;
282     }
283
284     private static dispatch def DataSchemaNode childByQName(ContainerSchemaNode container, QName name) {
285         return container.dataNodeChildByQName(name);
286     }
287
288     private static dispatch def DataSchemaNode childByQName(ListSchemaNode container, QName name) {
289         return container.dataNodeChildByQName(name);
290     }
291
292     private static dispatch def DataSchemaNode childByQName(DataSchemaNode container, QName name) {
293         return null;
294     }
295
296     private static def DataSchemaNode dataNodeChildByQName(DataNodeContainer container, QName name) {
297         var ret = container.getDataChildByName(name);
298         if (ret === null) {
299
300             // Find in Choice Cases
301             for (node : container.childNodes) {
302                 if (node instanceof ChoiceCaseNode) {
303                     val caseNode = (node as ChoiceCaseNode);
304                     ret = caseNode.childByQName(name);
305                     if (ret !== null) {
306                         return ret;
307                     }
308                 }
309             }
310         }
311         return ret;
312     }
313
314     private def toUriString(Object object) {
315         if(object === null) return "";
316         return URLEncoder.encode(object.toString)
317     }
318     
319     private def InstanceIdWithSchemaNode collectPathArguments(InstanceIdentifierBuilder builder, List<String> strings,
320         DataNodeContainer parentNode, MountInstance mountPoint, boolean returnJustMountPoint) {
321         checkNotNull(strings)
322         if (parentNode === null) {
323             return null;
324         }
325         if (strings.empty) {
326             return new InstanceIdWithSchemaNode(builder.toInstance, parentNode as DataSchemaNode, mountPoint)
327         }
328         
329         val nodeName = strings.head.toNodeName
330         val moduleName = strings.head.toModuleName
331         var DataSchemaNode targetNode = null
332         if (!moduleName.nullOrEmpty) {
333             // if it is mount point
334             if (moduleName == MOUNT_MODULE && nodeName == MOUNT_NODE) {
335                 if (mountPoint !== null) {
336                     throw new ResponseException(BAD_REQUEST, "Restconf supports just one mount point in URI.")
337                 }
338                 
339                 if (mountService === null) {
340                     throw new ResponseException(SERVICE_UNAVAILABLE, "MountService was not found. " 
341                         + "Finding behind mount points does not work."
342                     )
343                 }
344                 
345                 val partialPath = builder.toInstance;
346                 val mount = mountService.getMountPoint(partialPath)
347                 if (mount === null) {
348                     LOG.debug("Instance identifier to missing mount point: {}", partialPath)
349                     throw new ResponseException(BAD_REQUEST, "Mount point does not exist.")
350                 }
351                 
352                 val mountPointSchema = mount.schemaContext;
353                 if (mountPointSchema === null) {
354                     throw new ResponseException(BAD_REQUEST, "Mount point does not contain any schema with modules.")
355                 }
356                 
357                 if (returnJustMountPoint) {
358                     return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
359                 }
360                 
361                 if (strings.size == 1) { // any data node is not behind mount point
362                     return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
363                 }
364                 
365                 val moduleNameBehindMountPoint = strings.get(1).toModuleName()
366                 if (moduleNameBehindMountPoint === null) {
367                     throw new ResponseException(BAD_REQUEST,
368                         "First node after mount point in URI has to be in format \"moduleName:nodeName\"")
369                 }
370                 
371                 val moduleBehindMountPoint = mountPointSchema.getLatestModule(moduleNameBehindMountPoint)
372                 if (moduleBehindMountPoint === null) {
373                     throw new ResponseException(BAD_REQUEST,
374                         "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
375                 }
376                 
377                 return collectPathArguments(InstanceIdentifier.builder(), strings.subList(1, strings.size),
378                     moduleBehindMountPoint, mount, returnJustMountPoint);
379             }
380             
381             var Module module = null;
382             if (mountPoint === null) {
383                 module = globalSchema.getLatestModule(moduleName)
384                 if (module === null) {
385                     throw new ResponseException(BAD_REQUEST,
386                         "URI has bad format. \"" + moduleName + "\" module does not exist.")
387                 }
388             } else {
389                 module = mountPoint.schemaContext?.getLatestModule(moduleName)
390                 if (module === null) {
391                     throw new ResponseException(BAD_REQUEST,
392                         "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
393                 }
394             }
395             targetNode = parentNode.findInstanceDataChildByNameAndNamespace(nodeName, module.namespace)
396             if (targetNode === null) {
397                 throw new ResponseException(BAD_REQUEST, "URI has bad format. Possible reasons:\n" + 
398                     "1. \"" + strings.head + "\" was not found in parent data node.\n" + 
399                     "2. \"" + strings.head + "\" is behind mount point. Then it should be in format \"/" + MOUNT + "/" + strings.head + "\".")
400             }
401         } else { // string without module name
402             val potentialSchemaNodes = parentNode.findInstanceDataChildrenByName(nodeName)
403             if (potentialSchemaNodes.size > 1) {
404                 val StringBuilder namespacesOfPotentialModules = new StringBuilder;
405                 for (potentialNodeSchema : potentialSchemaNodes) {
406                     namespacesOfPotentialModules.append("   ").append(potentialNodeSchema.QName.namespace.toString).append("\n")
407                 }
408                 throw new ResponseException(BAD_REQUEST, "URI has bad format. Node \"" + nodeName + "\" is added as augment from more than one module. " 
409                         + "Therefore the node must have module name and it has to be in format \"moduleName:nodeName\"."
410                         + "\nThe node is added as augment from modules with namespaces:\n" + namespacesOfPotentialModules)
411             }
412             targetNode = potentialSchemaNodes.head
413             if (targetNode === null) {
414                 throw new ResponseException(BAD_REQUEST, "URI has bad format. \"" + nodeName + "\" was not found in parent data node.\n")
415             }
416         }
417         
418         if (!(targetNode instanceof ListSchemaNode) && !(targetNode instanceof ContainerSchemaNode)) {
419             throw new ResponseException(BAD_REQUEST,"URI has bad format. Node \"" + strings.head + "\" must be Container or List yang type.")
420         }
421         // Number of consumed elements
422         var consumed = 1;
423         if (targetNode instanceof ListSchemaNode) {
424             val listNode = targetNode as ListSchemaNode;
425             val keysSize = listNode.keyDefinition.size
426
427             // every key has to be filled
428             if ((strings.length - consumed) < keysSize) {
429                 throw new ResponseException(BAD_REQUEST,"Missing key for list \"" + listNode.QName.localName + "\".")
430             }
431             val uriKeyValues = strings.subList(consumed, consumed + keysSize);
432             val keyValues = new HashMap<QName, Object>();
433             var i = 0;
434             for (key : listNode.keyDefinition) {
435                 val uriKeyValue = uriKeyValues.get(i);
436
437                 // key value cannot be NULL
438                 if (uriKeyValue.equals(NULL_VALUE)) {
439                     throw new ResponseException(BAD_REQUEST, "URI has bad format. List \"" + listNode.QName.localName 
440                         + "\" cannot contain \"null\" value as a key."
441                     )
442                 }
443                 keyValues.addKeyValue(listNode.getDataChildByName(key), uriKeyValue);
444                 i = i + 1;
445             }
446             consumed = consumed + i;
447             builder.nodeWithKey(targetNode.QName, keyValues);
448         } else {
449
450             // Only one instance of node is allowed
451             builder.node(targetNode.QName);
452         }
453         if (targetNode instanceof DataNodeContainer) {
454             val remaining = strings.subList(consumed, strings.length);
455             val result = builder.collectPathArguments(remaining, targetNode as DataNodeContainer, mountPoint, returnJustMountPoint);
456             return result
457         }
458
459         return new InstanceIdWithSchemaNode(builder.toInstance, targetNode, mountPoint)
460     }
461
462     def DataSchemaNode findInstanceDataChildByNameAndNamespace(DataNodeContainer container,
463         String name, URI namespace) {
464         Preconditions.checkNotNull(namespace)
465         val potentialSchemaNodes = container.findInstanceDataChildrenByName(name)
466         return potentialSchemaNodes.filter[n|n.QName.namespace == namespace].head
467     }
468     
469     def List<DataSchemaNode> findInstanceDataChildrenByName(DataNodeContainer container, String name) {
470         Preconditions.checkNotNull(container)
471         Preconditions.checkNotNull(name)
472         val instantiatedDataNodeContainers = new ArrayList
473         instantiatedDataNodeContainers.collectInstanceDataNodeContainers(container, name)
474         return instantiatedDataNodeContainers
475     }
476     
477     private def void collectInstanceDataNodeContainers(List<DataSchemaNode> potentialSchemaNodes, DataNodeContainer container,
478         String name) {
479         val nodes = container.childNodes.filter[n|n.QName.localName == name]
480         for (potentialNode : nodes) {
481             if (potentialNode.isInstantiatedDataSchema) {
482                 potentialSchemaNodes.add(potentialNode)
483             }
484         }
485         val allCases = container.childNodes.filter(ChoiceNode).map[cases].flatten
486         for (caze : allCases) {
487             collectInstanceDataNodeContainers(potentialSchemaNodes, caze, name)
488         }
489     }
490     
491     def boolean isInstantiatedDataSchema(DataSchemaNode node) {
492         switch node {
493             LeafSchemaNode: return true
494             LeafListSchemaNode: return true
495             ContainerSchemaNode: return true
496             ListSchemaNode: return true
497             default: return false
498         }
499     }
500     
501     private def void addKeyValue(HashMap<QName, Object> map, DataSchemaNode node, String uriValue) {
502         checkNotNull(uriValue);
503         checkArgument(node instanceof LeafSchemaNode);
504         val urlDecoded = URLDecoder.decode(uriValue);
505         val typedef = (node as LeafSchemaNode).type;
506         
507         var decoded = TypeDefinitionAwareCodec.from(typedef)?.deserialize(urlDecoded)
508         if(decoded === null) {
509             var baseType = RestUtil.resolveBaseTypeFrom(typedef)
510             if(baseType instanceof IdentityrefTypeDefinition) {
511                 decoded = toQName(urlDecoded)
512             }
513         }
514         map.put(node.QName, decoded);
515     }
516
517     private static def String toModuleName(String str) {
518         checkNotNull(str)
519         if (str.contains(":")) {
520             val args = str.split(":");
521             if (args.size === 2) {
522                 return args.get(0);
523             }
524         }
525         return null;
526     }
527
528     private def String toNodeName(String str) {
529         if (str.contains(":")) {
530             val args = str.split(":");
531             if (args.size === 2) {
532                 return args.get(1);
533             }
534         }
535         return str;
536     }
537
538     private def QName toQName(String name) {
539         val module = name.toModuleName;
540         val node = name.toNodeName;
541         val namespace = FluentIterable.from(globalSchema.modules.sort[o1,o2 | o1.revision.compareTo(o2.revision)]) //
542             .transform[QName.create(namespace,revision,it.name)].findFirst[module == localName]
543         ;
544         return QName.create(namespace,node);
545     }
546
547     def getRpcDefinition(String name) {
548         return qnameToRpc.get(name.toQName)
549     }
550
551     override onGlobalContextUpdated(SchemaContext context) {
552         this.globalSchema = context;
553         for (operation : context.operations) {
554             val qname = operation.QName;
555             qnameToRpc.put(qname, operation);
556         }
557     }
558
559 }