Merge "BUG 392: Resolved translation to JSON behind mount point"
[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     public def InstanceIdWithSchemaNode toInstanceIdentifier(String restconfInstance) {
90         checkPreconditions
91         val pathArgs = restconfInstance.split("/");
92         if (pathArgs.empty) {
93             return null;
94         }
95         if (pathArgs.head.empty) {
96             pathArgs.remove(0)
97         }
98         val startModule = pathArgs.head.toModuleName();
99         if (startModule === null) {
100             throw new ResponseException(BAD_REQUEST, "First node in URI has to be in format \"moduleName:nodeName\"")
101         }
102         val iiWithSchemaNode = collectPathArguments(InstanceIdentifier.builder(), pathArgs,
103             globalSchema.getLatestModule(startModule), null);
104         if (iiWithSchemaNode === null) {
105             throw new ResponseException(BAD_REQUEST, "URI has bad format")
106         }
107         return iiWithSchemaNode
108     }
109
110     private def getLatestModule(SchemaContext schema, String moduleName) {
111         checkArgument(schema !== null);
112         checkArgument(moduleName !== null && !moduleName.empty)
113         val modules = schema.modules.filter[m|m.name == moduleName]
114         return modules.filterLatestModule
115     }
116     
117     private def filterLatestModule(Iterable<Module> modules) {
118         var latestModule = modules.head
119         for (module : modules) {
120             if (module.revision.after(latestModule.revision)) {
121                 latestModule = module
122             }
123         }
124         return latestModule
125     }
126     
127     def findModuleByName(String moduleName) {
128         checkPreconditions
129         checkArgument(moduleName !== null && !moduleName.empty)
130         return globalSchema.getLatestModule(moduleName)
131     }
132     
133     def findModuleByName(MountInstance mountPoint, String moduleName) {
134         checkArgument(moduleName !== null && mountPoint !== null)
135         val mountPointSchema = mountPoint.schemaContext;
136         return mountPointSchema?.getLatestModule(moduleName);
137     }
138     
139     def findModuleByNamespace(URI namespace) {
140         checkPreconditions
141         checkArgument(namespace !== null)
142         val moduleSchemas = globalSchema.findModuleByNamespace(namespace)
143         return moduleSchemas?.filterLatestModule
144     }
145     
146     def findModuleByNamespace(MountInstance mountPoint, URI namespace) {
147         checkArgument(namespace !== null && mountPoint !== null)
148         val mountPointSchema = mountPoint.schemaContext;
149         val moduleSchemas = mountPointSchema?.findModuleByNamespace(namespace)
150         return moduleSchemas?.filterLatestModule
151     }
152
153     def String toFullRestconfIdentifier(InstanceIdentifier path) {
154         checkPreconditions
155         val elements = path.path;
156         val ret = new StringBuilder();
157         val startQName = elements.get(0).nodeType;
158         val initialModule = globalSchema.findModuleByNamespaceAndRevision(startQName.namespace, startQName.revision)
159         var node = initialModule as DataSchemaNode;
160         for (element : elements) {
161             node = node.childByQName(element.nodeType);
162             ret.append(element.toRestconfIdentifier(node));
163         }
164         return ret.toString
165     }
166
167     private def dispatch CharSequence toRestconfIdentifier(NodeIdentifier argument, DataSchemaNode node) {
168         '''/«argument.nodeType.toRestconfIdentifier()»'''
169     }
170
171     private def dispatch CharSequence toRestconfIdentifier(NodeIdentifierWithPredicates argument, ListSchemaNode node) {
172         val nodeIdentifier = argument.nodeType.toRestconfIdentifier();
173         val keyValues = argument.keyValues;
174         return '''/«nodeIdentifier»/«FOR key : node.keyDefinition SEPARATOR "/"»«keyValues.get(key).toUriString»«ENDFOR»'''
175     }
176
177     private def dispatch CharSequence toRestconfIdentifier(PathArgument argument, DataSchemaNode node) {
178         throw new IllegalArgumentException("Conversion of generic path argument is not supported");
179     }
180
181     def findModuleNameByNamespace(URI namespace) {
182         checkPreconditions
183         var moduleName = uriToModuleName.get(namespace)
184         if (moduleName === null) {
185             val module = findModuleByNamespace(namespace)
186             if (module === null) return null
187             moduleName = module.name
188             uriToModuleName.put(namespace, moduleName)
189         }
190         return moduleName
191     }
192     
193     def findModuleNameByNamespace(MountInstance mountPoint, URI namespace) {
194         val module = mountPoint.findModuleByNamespace(namespace);
195         return module?.name
196     }
197
198     def findNamespaceByModuleName(String moduleName) {
199         var namespace = moduleNameToUri.get(moduleName)
200         if (namespace === null) {
201             var module = findModuleByName(moduleName)
202             if(module === null) return null
203             namespace = module.namespace
204             uriToModuleName.put(namespace, moduleName)
205         }
206         return namespace
207     }
208     
209     def findNamespaceByModuleName(MountInstance mountPoint, String moduleName) {
210         val module = mountPoint.findModuleByName(moduleName)
211         return module?.namespace
212     }
213
214     def CharSequence toRestconfIdentifier(QName qname) {
215         checkPreconditions
216         var module = uriToModuleName.get(qname.namespace)
217         if (module === null) {
218             val moduleSchema = globalSchema.findModuleByNamespaceAndRevision(qname.namespace, qname.revision);
219             if(moduleSchema === null) return null
220             uriToModuleName.put(qname.namespace, moduleSchema.name)
221             module = moduleSchema.name;
222         }
223         return '''«module»:«qname.localName»''';
224     }
225
226     def CharSequence toRestconfIdentifier(MountInstance mountPoint, QName qname) {
227         val moduleSchema = mountPoint?.schemaContext.findModuleByNamespaceAndRevision(qname.namespace, qname.revision);
228         if(moduleSchema === null) return null
229         val module = moduleSchema.name;
230         return '''«module»:«qname.localName»''';
231     }
232
233     private static dispatch def DataSchemaNode childByQName(ChoiceNode container, QName name) {
234         for (caze : container.cases) {
235             val ret = caze.childByQName(name)
236             if (ret !== null) {
237                 return ret;
238             }
239         }
240         return null;
241     }
242
243     private static dispatch def DataSchemaNode childByQName(ChoiceCaseNode container, QName name) {
244         val ret = container.getDataChildByName(name);
245         return ret;
246     }
247
248     private static dispatch def DataSchemaNode childByQName(ContainerSchemaNode container, QName name) {
249         return container.dataNodeChildByQName(name);
250     }
251
252     private static dispatch def DataSchemaNode childByQName(ListSchemaNode container, QName name) {
253         return container.dataNodeChildByQName(name);
254     }
255
256     private static dispatch def DataSchemaNode childByQName(DataSchemaNode container, QName name) {
257         return null;
258     }
259
260     private static def DataSchemaNode dataNodeChildByQName(DataNodeContainer container, QName name) {
261         var ret = container.getDataChildByName(name);
262         if (ret === null) {
263
264             // Find in Choice Cases
265             for (node : container.childNodes) {
266                 if (node instanceof ChoiceCaseNode) {
267                     val caseNode = (node as ChoiceCaseNode);
268                     ret = caseNode.childByQName(name);
269                     if (ret !== null) {
270                         return ret;
271                     }
272                 }
273             }
274         }
275         return ret;
276     }
277
278     private def toUriString(Object object) {
279         if(object === null) return "";
280         return URLEncoder.encode(object.toString)
281     }
282     
283     private def InstanceIdWithSchemaNode collectPathArguments(InstanceIdentifierBuilder builder, List<String> strings,
284         DataNodeContainer parentNode, MountInstance mountPoint) {
285         checkNotNull(strings)
286         if (parentNode === null) {
287             return null;
288         }
289         if (strings.empty) {
290             return new InstanceIdWithSchemaNode(builder.toInstance, parentNode as DataSchemaNode, mountPoint)
291         }
292         
293         val nodeName = strings.head.toNodeName
294         val moduleName = strings.head.toModuleName
295         var DataSchemaNode targetNode = null
296         if (!moduleName.nullOrEmpty) {
297             // if it is mount point
298             if (moduleName == MOUNT_MODULE && nodeName == MOUNT_NODE) {
299                 if (mountPoint !== null) {
300                     throw new ResponseException(BAD_REQUEST, "Restconf supports just one mount point in URI.")
301                 }
302                 
303                 if (mountService === null) {
304                     throw new ResponseException(SERVICE_UNAVAILABLE, "MountService was not found. " 
305                         + "Finding behind mount points does not work."
306                     )
307                 }
308                 
309                 val partialPath = builder.toInstance;
310                 val mount = mountService.getMountPoint(partialPath)
311                 if (mount === null) {
312                     LOG.debug("Instance identifier to missing mount point: {}", partialPath)
313                     throw new ResponseException(BAD_REQUEST, "Mount point does not exist.")
314                 }
315                 
316                 val mountPointSchema = mount.schemaContext;
317                 if (mountPointSchema === null) {
318                     throw new ResponseException(BAD_REQUEST, "Mount point does not contain any schema with modules.")
319                 }
320                 
321                 if (strings.size == 1) { // any data node is not behind mount point
322                     return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
323                 }
324                 
325                 val moduleNameBehindMountPoint = strings.get(1).toModuleName()
326                 if (moduleNameBehindMountPoint === null) {
327                     throw new ResponseException(BAD_REQUEST,
328                         "First node after mount point in URI has to be in format \"moduleName:nodeName\"")
329                 }
330                 
331                 val moduleBehindMountPoint = mountPointSchema.getLatestModule(moduleNameBehindMountPoint)
332                 if (moduleBehindMountPoint === null) {
333                     throw new ResponseException(BAD_REQUEST,
334                         "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
335                 }
336                 
337                 return collectPathArguments(InstanceIdentifier.builder(), strings.subList(1, strings.size),
338                     moduleBehindMountPoint, mount);
339             }
340             
341             var Module module = null;
342             if (mountPoint === null) {
343                 module = globalSchema.getLatestModule(moduleName)
344                 if (module === null) {
345                     throw new ResponseException(BAD_REQUEST,
346                         "URI has bad format. \"" + moduleName + "\" module does not exist.")
347                 }
348             } else {
349                 module = mountPoint.schemaContext?.getLatestModule(moduleName)
350                 if (module === null) {
351                     throw new ResponseException(BAD_REQUEST,
352                         "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
353                 }
354             }
355             targetNode = parentNode.findInstanceDataChildByNameAndNamespace(nodeName, module.namespace)
356             if (targetNode === null) {
357                 throw new ResponseException(BAD_REQUEST, "URI has bad format. Possible reasons:\n" + 
358                     "1. \"" + strings.head + "\" was not found in parent data node.\n" + 
359                     "2. \"" + strings.head + "\" is behind mount point. Then it should be in format \"/" + MOUNT + "/" + strings.head + "\".")
360             }
361         } else { // string without module name
362             val potentialSchemaNodes = parentNode.findInstanceDataChildrenByName(nodeName)
363             if (potentialSchemaNodes.size > 1) {
364                 val StringBuilder namespacesOfPotentialModules = new StringBuilder;
365                 for (potentialNodeSchema : potentialSchemaNodes) {
366                     namespacesOfPotentialModules.append("   ").append(potentialNodeSchema.QName.namespace.toString).append("\n")
367                 }
368                 throw new ResponseException(BAD_REQUEST, "URI has bad format. Node \"" + nodeName + "\" is added as augment from more than one module. " 
369                         + "Therefore the node must have module name and it has to be in format \"moduleName:nodeName\"."
370                         + "\nThe node is added as augment from modules with namespaces:\n" + namespacesOfPotentialModules)
371             }
372             targetNode = potentialSchemaNodes.head
373             if (targetNode === null) {
374                 throw new ResponseException(BAD_REQUEST, "URI has bad format. \"" + nodeName + "\" was not found in parent data node.\n")
375             }
376         }
377         
378         if (!(targetNode instanceof ListSchemaNode) && !(targetNode instanceof ContainerSchemaNode)) {
379             throw new ResponseException(BAD_REQUEST,"URI has bad format. Node \"" + strings.head + "\" must be Container or List yang type.")
380         }
381         // Number of consumed elements
382         var consumed = 1;
383         if (targetNode instanceof ListSchemaNode) {
384             val listNode = targetNode as ListSchemaNode;
385             val keysSize = listNode.keyDefinition.size
386
387             // every key has to be filled
388             if ((strings.length - consumed) < keysSize) {
389                 throw new ResponseException(BAD_REQUEST,"Missing key for list \"" + listNode.QName.localName + "\".")
390             }
391             val uriKeyValues = strings.subList(consumed, consumed + keysSize);
392             val keyValues = new HashMap<QName, Object>();
393             var i = 0;
394             for (key : listNode.keyDefinition) {
395                 val uriKeyValue = uriKeyValues.get(i);
396
397                 // key value cannot be NULL
398                 if (uriKeyValue.equals(NULL_VALUE)) {
399                     throw new ResponseException(BAD_REQUEST, "URI has bad format. List \"" + listNode.QName.localName 
400                         + "\" cannot contain \"null\" value as a key."
401                     )
402                 }
403                 keyValues.addKeyValue(listNode.getDataChildByName(key), uriKeyValue);
404                 i = i + 1;
405             }
406             consumed = consumed + i;
407             builder.nodeWithKey(targetNode.QName, keyValues);
408         } else {
409
410             // Only one instance of node is allowed
411             builder.node(targetNode.QName);
412         }
413         if (targetNode instanceof DataNodeContainer) {
414             val remaining = strings.subList(consumed, strings.length);
415             val result = builder.collectPathArguments(remaining, targetNode as DataNodeContainer, mountPoint);
416             return result
417         }
418
419         return new InstanceIdWithSchemaNode(builder.toInstance, targetNode, mountPoint)
420     }
421
422     def DataSchemaNode findInstanceDataChildByNameAndNamespace(DataNodeContainer container,
423         String name, URI namespace) {
424         Preconditions.checkNotNull(namespace)
425         val potentialSchemaNodes = container.findInstanceDataChildrenByName(name)
426         return potentialSchemaNodes.filter[n|n.QName.namespace == namespace].head
427     }
428     
429     def List<DataSchemaNode> findInstanceDataChildrenByName(DataNodeContainer container, String name) {
430         Preconditions.checkNotNull(container)
431         Preconditions.checkNotNull(name)
432         val instantiatedDataNodeContainers = new ArrayList
433         instantiatedDataNodeContainers.collectInstanceDataNodeContainers(container, name)
434         return instantiatedDataNodeContainers
435     }
436     
437     private def void collectInstanceDataNodeContainers(List<DataSchemaNode> potentialSchemaNodes, DataNodeContainer container,
438         String name) {
439         val nodes = container.childNodes.filter[n|n.QName.localName == name]
440         for (potentialNode : nodes) {
441             if (potentialNode.isInstantiatedDataSchema) {
442                 potentialSchemaNodes.add(potentialNode)
443             }
444         }
445         val allCases = container.childNodes.filter(ChoiceNode).map[cases].flatten
446         for (caze : allCases) {
447             collectInstanceDataNodeContainers(potentialSchemaNodes, caze, name)
448         }
449     }
450     
451     def boolean isInstantiatedDataSchema(DataSchemaNode node) {
452         switch node {
453             LeafSchemaNode: return true
454             LeafListSchemaNode: return true
455             ContainerSchemaNode: return true
456             ListSchemaNode: return true
457             default: return false
458         }
459     }
460     
461     private def void addKeyValue(HashMap<QName, Object> map, DataSchemaNode node, String uriValue) {
462         checkNotNull(uriValue);
463         checkArgument(node instanceof LeafSchemaNode);
464         val urlDecoded = URLDecoder.decode(uriValue);
465         val typedef = (node as LeafSchemaNode).type;
466         
467         var decoded = TypeDefinitionAwareCodec.from(typedef)?.deserialize(urlDecoded)
468         if(decoded === null) {
469             var baseType = RestUtil.resolveBaseTypeFrom(typedef)
470             if(baseType instanceof IdentityrefTypeDefinition) {
471                 decoded = toQName(urlDecoded)
472             }
473         }
474         map.put(node.QName, decoded);
475     }
476
477     private static def String toModuleName(String str) {
478         checkNotNull(str)
479         if (str.contains(":")) {
480             val args = str.split(":");
481             if (args.size === 2) {
482                 return args.get(0);
483             }
484         }
485         return null;
486     }
487
488     private def String toNodeName(String str) {
489         if (str.contains(":")) {
490             val args = str.split(":");
491             if (args.size === 2) {
492                 return args.get(1);
493             }
494         }
495         return str;
496     }
497
498     private def QName toQName(String name) {
499         val module = name.toModuleName;
500         val node = name.toNodeName;
501         val namespace = FluentIterable.from(globalSchema.modules.sort[o1,o2 | o1.revision.compareTo(o2.revision)]) //
502             .transform[QName.create(namespace,revision,it.name)].findFirst[module == localName]
503         ;
504         return QName.create(namespace,node);
505     }
506
507     def getRpcDefinition(String name) {
508         return qnameToRpc.get(name.toQName)
509     }
510
511     override onGlobalContextUpdated(SchemaContext context) {
512         this.globalSchema = context;
513         for (operation : context.operations) {
514             val qname = operation.QName;
515             qnameToRpc.put(qname, operation);
516         }
517     }
518
519 }