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