7d32194b1f386ca1a7e228ea25de24dbf6dc15e1
[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) throw new IllegalArgumentException()
220             uriToModuleName.put(qname.namespace, moduleSchema.name)
221             module = moduleSchema.name;
222         }
223         return '''«module»:«qname.localName»''';
224     }
225
226     private static dispatch def DataSchemaNode childByQName(ChoiceNode container, QName name) {
227         for (caze : container.cases) {
228             val ret = caze.childByQName(name)
229             if (ret !== null) {
230                 return ret;
231             }
232         }
233         return null;
234     }
235
236     private static dispatch def DataSchemaNode childByQName(ChoiceCaseNode container, QName name) {
237         val ret = container.getDataChildByName(name);
238         return ret;
239     }
240
241     private static dispatch def DataSchemaNode childByQName(ContainerSchemaNode container, QName name) {
242         return container.dataNodeChildByQName(name);
243     }
244
245     private static dispatch def DataSchemaNode childByQName(ListSchemaNode container, QName name) {
246         return container.dataNodeChildByQName(name);
247     }
248
249     private static dispatch def DataSchemaNode childByQName(DataSchemaNode container, QName name) {
250         return null;
251     }
252
253     private static def DataSchemaNode dataNodeChildByQName(DataNodeContainer container, QName name) {
254         var ret = container.getDataChildByName(name);
255         if (ret === null) {
256
257             // Find in Choice Cases
258             for (node : container.childNodes) {
259                 if (node instanceof ChoiceCaseNode) {
260                     val caseNode = (node as ChoiceCaseNode);
261                     ret = caseNode.childByQName(name);
262                     if (ret !== null) {
263                         return ret;
264                     }
265                 }
266             }
267         }
268         return ret;
269     }
270
271     private def toUriString(Object object) {
272         if(object === null) return "";
273         return URLEncoder.encode(object.toString)
274     }
275     
276     private def InstanceIdWithSchemaNode collectPathArguments(InstanceIdentifierBuilder builder, List<String> strings,
277         DataNodeContainer parentNode, MountInstance mountPoint) {
278         checkNotNull(strings)
279         if (parentNode === null) {
280             return null;
281         }
282         if (strings.empty) {
283             return new InstanceIdWithSchemaNode(builder.toInstance, parentNode as DataSchemaNode, mountPoint)
284         }
285         
286         val nodeName = strings.head.toNodeName
287         val moduleName = strings.head.toModuleName
288         var DataSchemaNode targetNode = null
289         if (!moduleName.nullOrEmpty) {
290             // if it is mount point
291             if (moduleName == MOUNT_MODULE && nodeName == MOUNT_NODE) {
292                 if (mountPoint !== null) {
293                     throw new ResponseException(BAD_REQUEST, "Restconf supports just one mount point in URI.")
294                 }
295                 
296                 if (mountService === null) {
297                     throw new ResponseException(SERVICE_UNAVAILABLE, "MountService was not found. " 
298                         + "Finding behind mount points does not work."
299                     )
300                 }
301                 
302                 val partialPath = builder.toInstance;
303                 val mount = mountService.getMountPoint(partialPath)
304                 if (mount === null) {
305                     LOG.debug("Instance identifier to missing mount point: {}", partialPath)
306                     throw new ResponseException(BAD_REQUEST, "Mount point does not exist.")
307                 }
308                 
309                 val mountPointSchema = mount.schemaContext;
310                 if (mountPointSchema === null) {
311                     throw new ResponseException(BAD_REQUEST, "Mount point does not contain any schema with modules.")
312                 }
313                 
314                 if (strings.size == 1) { // any data node is not behind mount point
315                     return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
316                 }
317                 
318                 val moduleNameBehindMountPoint = strings.get(1).toModuleName()
319                 if (moduleNameBehindMountPoint === null) {
320                     throw new ResponseException(BAD_REQUEST,
321                         "First node after mount point in URI has to be in format \"moduleName:nodeName\"")
322                 }
323                 
324                 val moduleBehindMountPoint = mountPointSchema.getLatestModule(moduleNameBehindMountPoint)
325                 if (moduleBehindMountPoint === null) {
326                     throw new ResponseException(BAD_REQUEST,
327                         "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
328                 }
329                 
330                 return collectPathArguments(InstanceIdentifier.builder(), strings.subList(1, strings.size),
331                     moduleBehindMountPoint, mount);
332             }
333             
334             var Module module = null;
335             if (mountPoint === null) {
336                 module = globalSchema.getLatestModule(moduleName)
337                 if (module === null) {
338                     throw new ResponseException(BAD_REQUEST,
339                         "URI has bad format. \"" + moduleName + "\" module does not exist.")
340                 }
341             } else {
342                 module = mountPoint.schemaContext?.getLatestModule(moduleName)
343                 if (module === null) {
344                     throw new ResponseException(BAD_REQUEST,
345                         "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
346                 }
347             }
348             targetNode = parentNode.findInstanceDataChildByNameAndNamespace(nodeName, module.namespace)
349             if (targetNode === null) {
350                 throw new ResponseException(BAD_REQUEST, "URI has bad format. Possible reasons:\n" + 
351                     "1. \"" + strings.head + "\" was not found in parent data node.\n" + 
352                     "2. \"" + strings.head + "\" is behind mount point. Then it should be in format \"/" + MOUNT + "/" + strings.head + "\".")
353             }
354         } else { // string without module name
355             val potentialSchemaNodes = parentNode.findInstanceDataChildrenByName(nodeName)
356             if (potentialSchemaNodes.size > 1) {
357                 val StringBuilder namespacesOfPotentialModules = new StringBuilder;
358                 for (potentialNodeSchema : potentialSchemaNodes) {
359                     namespacesOfPotentialModules.append("   ").append(potentialNodeSchema.QName.namespace.toString).append("\n")
360                 }
361                 throw new ResponseException(BAD_REQUEST, "URI has bad format. Node \"" + nodeName + "\" is added as augment from more than one module. " 
362                         + "Therefore the node must have module name and it has to be in format \"moduleName:nodeName\"."
363                         + "\nThe node is added as augment from modules with namespaces:\n" + namespacesOfPotentialModules)
364             }
365             targetNode = potentialSchemaNodes.head
366             if (targetNode === null) {
367                 throw new ResponseException(BAD_REQUEST, "URI has bad format. \"" + nodeName + "\" was not found in parent data node.\n")
368             }
369         }
370         
371         if (!(targetNode instanceof ListSchemaNode) && !(targetNode instanceof ContainerSchemaNode)) {
372             throw new ResponseException(BAD_REQUEST,"URI has bad format. Node \"" + strings.head + "\" must be Container or List yang type.")
373         }
374         // Number of consumed elements
375         var consumed = 1;
376         if (targetNode instanceof ListSchemaNode) {
377             val listNode = targetNode as ListSchemaNode;
378             val keysSize = listNode.keyDefinition.size
379
380             // every key has to be filled
381             if ((strings.length - consumed) < keysSize) {
382                 throw new ResponseException(BAD_REQUEST,"Missing key for list \"" + listNode.QName.localName + "\".")
383             }
384             val uriKeyValues = strings.subList(consumed, consumed + keysSize);
385             val keyValues = new HashMap<QName, Object>();
386             var i = 0;
387             for (key : listNode.keyDefinition) {
388                 val uriKeyValue = uriKeyValues.get(i);
389
390                 // key value cannot be NULL
391                 if (uriKeyValue.equals(NULL_VALUE)) {
392                     throw new ResponseException(BAD_REQUEST, "URI has bad format. List \"" + listNode.QName.localName 
393                         + "\" cannot contain \"null\" value as a key."
394                     )
395                 }
396                 keyValues.addKeyValue(listNode.getDataChildByName(key), uriKeyValue);
397                 i = i + 1;
398             }
399             consumed = consumed + i;
400             builder.nodeWithKey(targetNode.QName, keyValues);
401         } else {
402
403             // Only one instance of node is allowed
404             builder.node(targetNode.QName);
405         }
406         if (targetNode instanceof DataNodeContainer) {
407             val remaining = strings.subList(consumed, strings.length);
408             val result = builder.collectPathArguments(remaining, targetNode as DataNodeContainer, mountPoint);
409             return result
410         }
411
412         return new InstanceIdWithSchemaNode(builder.toInstance, targetNode, mountPoint)
413     }
414
415     def DataSchemaNode findInstanceDataChildByNameAndNamespace(DataNodeContainer container,
416         String name, URI namespace) {
417         Preconditions.checkNotNull(namespace)
418         val potentialSchemaNodes = container.findInstanceDataChildrenByName(name)
419         return potentialSchemaNodes.filter[n|n.QName.namespace == namespace].head
420     }
421     
422     def List<DataSchemaNode> findInstanceDataChildrenByName(DataNodeContainer container, String name) {
423         Preconditions.checkNotNull(container)
424         Preconditions.checkNotNull(name)
425         val instantiatedDataNodeContainers = new ArrayList
426         instantiatedDataNodeContainers.collectInstanceDataNodeContainers(container, name)
427         return instantiatedDataNodeContainers
428     }
429     
430     private def void collectInstanceDataNodeContainers(List<DataSchemaNode> potentialSchemaNodes, DataNodeContainer container,
431         String name) {
432         val nodes = container.childNodes.filter[n|n.QName.localName == name]
433         for (potentialNode : nodes) {
434             if (potentialNode.isInstantiatedDataSchema) {
435                 potentialSchemaNodes.add(potentialNode)
436             }
437         }
438         val allCases = container.childNodes.filter(ChoiceNode).map[cases].flatten
439         for (caze : allCases) {
440             collectInstanceDataNodeContainers(potentialSchemaNodes, caze, name)
441         }
442     }
443     
444     def boolean isInstantiatedDataSchema(DataSchemaNode node) {
445         switch node {
446             LeafSchemaNode: return true
447             LeafListSchemaNode: return true
448             ContainerSchemaNode: return true
449             ListSchemaNode: return true
450             default: return false
451         }
452     }
453     
454     private def void addKeyValue(HashMap<QName, Object> map, DataSchemaNode node, String uriValue) {
455         checkNotNull(uriValue);
456         checkArgument(node instanceof LeafSchemaNode);
457         val urlDecoded = URLDecoder.decode(uriValue);
458         val typedef = (node as LeafSchemaNode).type;
459         
460         var decoded = TypeDefinitionAwareCodec.from(typedef)?.deserialize(urlDecoded)
461         if(decoded === null) {
462             var baseType = RestUtil.resolveBaseTypeFrom(typedef)
463             if(baseType instanceof IdentityrefTypeDefinition) {
464                 decoded = toQName(urlDecoded)
465             }
466         }
467         map.put(node.QName, decoded);
468     }
469
470     private static def String toModuleName(String str) {
471         checkNotNull(str)
472         if (str.contains(":")) {
473             val args = str.split(":");
474             if (args.size === 2) {
475                 return args.get(0);
476             }
477         }
478         return null;
479     }
480
481     private def String toNodeName(String str) {
482         if (str.contains(":")) {
483             val args = str.split(":");
484             if (args.size === 2) {
485                 return args.get(1);
486             }
487         }
488         return str;
489     }
490
491     private def QName toQName(String name) {
492         val module = name.toModuleName;
493         val node = name.toNodeName;
494         val namespace = FluentIterable.from(globalSchema.modules.sort[o1,o2 | o1.revision.compareTo(o2.revision)]) //
495             .transform[QName.create(namespace,revision,it.name)].findFirst[module == localName]
496         ;
497         return QName.create(namespace,node);
498     }
499
500     def getRpcDefinition(String name) {
501         return qnameToRpc.get(name.toQName)
502     }
503
504     override onGlobalContextUpdated(SchemaContext context) {
505         this.globalSchema = context;
506         for (operation : context.operations) {
507             val qname = operation.QName;
508             qnameToRpc.put(qname, operation);
509         }
510     }
511
512 }