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