2 * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.controller.sal.restconf.impl
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
17 import java.net.URLDecoder
18 import java.net.URLEncoder
19 import java.util.ArrayList
20 import java.util.HashMap
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
50 import static com.google.common.base.Preconditions.*
51 import static javax.ws.rs.core.Response.Status.*
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"
62 var SchemaContext globalSchema;
65 var MountService mountService;
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();
72 if (INSTANCE !== null) {
73 throw new IllegalStateException("Already instantiated");
77 static def getInstance() {
81 private def void checkPreconditions() {
82 if (globalSchema === null) {
83 throw new ResponseException(SERVICE_UNAVAILABLE, RestconfProvider::NOT_INITALIZED_MSG)
87 def setSchemas(SchemaContext schemas) {
88 onGlobalContextUpdated(schemas)
91 def InstanceIdWithSchemaNode toInstanceIdentifier(String restconfInstance) {
92 return restconfInstance.toIdentifier(false)
95 def InstanceIdWithSchemaNode toMountPointIdentifier(String restconfInstance) {
96 return restconfInstance.toIdentifier(true)
99 private def InstanceIdWithSchemaNode toIdentifier(String restconfInstance, boolean toMountPointIdentifier) {
101 val pathArgs = Lists.newArrayList(Splitter.on("/").split(restconfInstance))
102 pathArgs.omitFirstAndLastEmptyString
103 if (pathArgs.empty) {
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\"")
110 var InstanceIdWithSchemaNode iiWithSchemaNode = null;
111 if (toMountPointIdentifier) {
112 iiWithSchemaNode = collectPathArguments(InstanceIdentifier.builder(), pathArgs,
113 globalSchema.getLatestModule(startModule), null, true);
115 iiWithSchemaNode = collectPathArguments(InstanceIdentifier.builder(), pathArgs,
116 globalSchema.getLatestModule(startModule), null, false);
118 if (iiWithSchemaNode === null) {
119 throw new ResponseException(BAD_REQUEST, "URI has bad format")
121 return iiWithSchemaNode
124 private def omitFirstAndLastEmptyString(List<String> list) {
128 if (list.head.empty) {
134 if (list.last.empty) {
135 list.remove(list.indexOf(list.last))
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
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
157 def findModuleByName(String moduleName) {
159 checkArgument(moduleName !== null && !moduleName.empty)
160 return globalSchema.getLatestModule(moduleName)
163 def findModuleByName(MountInstance mountPoint, String moduleName) {
164 checkArgument(moduleName !== null && mountPoint !== null)
165 val mountPointSchema = mountPoint.schemaContext;
166 return mountPointSchema?.getLatestModule(moduleName);
169 def findModuleByNamespace(URI namespace) {
171 checkArgument(namespace !== null)
172 val moduleSchemas = globalSchema.findModuleByNamespace(namespace)
173 return moduleSchemas?.filterLatestModule
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
183 def findModuleByNameAndRevision(QName module) {
185 checkArgument(module !== null && module.localName !== null && module.revision !== null)
186 return globalSchema.findModuleByName(module.localName, module.revision)
189 def findModuleByNameAndRevision(MountInstance mountPoint, QName module) {
191 checkArgument(module !== null && module.localName !== null && module.revision !== null && mountPoint !== null)
192 return mountPoint.schemaContext?.findModuleByName(module.localName, module.revision)
195 def getDataNodeContainerFor(InstanceIdentifier path) {
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) {
206 node = potentialNode as DataNodeContainer
211 def String toFullRestconfIdentifier(InstanceIdentifier path) {
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) {
223 node = potentialNode as DataNodeContainer
224 ret.append(element.convertToRestconfIdentifier(node));
229 private def dispatch CharSequence convertToRestconfIdentifier(NodeIdentifier argument, ContainerSchemaNode node) {
230 '''/«argument.nodeType.toRestconfIdentifier()»'''
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»'''
239 private def dispatch CharSequence convertToRestconfIdentifier(PathArgument argument, DataNodeContainer node) {
240 throw new IllegalArgumentException("Conversion of generic path argument is not supported");
243 def findModuleNameByNamespace(URI namespace) {
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)
255 def findModuleNameByNamespace(MountInstance mountPoint, URI namespace) {
256 val module = mountPoint.findModuleByNamespace(namespace);
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)
271 def findNamespaceByModuleName(MountInstance mountPoint, String moduleName) {
272 val module = mountPoint.findModuleByName(moduleName)
273 return module?.namespace
276 def getAllModules(MountInstance mountPoint) {
278 return mountPoint?.schemaContext?.modules
281 def getAllModules() {
283 return globalSchema.modules
286 def CharSequence toRestconfIdentifier(QName qname) {
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;
295 return '''«module»:«qname.localName»''';
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»''';
305 private static dispatch def DataSchemaNode childByQName(ChoiceNode container, QName name) {
306 for (caze : container.cases) {
307 val ret = caze.childByQName(name)
315 private static dispatch def DataSchemaNode childByQName(ChoiceCaseNode container, QName name) {
316 val ret = container.getDataChildByName(name);
320 private static dispatch def DataSchemaNode childByQName(ContainerSchemaNode container, QName name) {
321 return container.dataNodeChildByQName(name);
324 private static dispatch def DataSchemaNode childByQName(ListSchemaNode container, QName name) {
325 return container.dataNodeChildByQName(name);
328 private static dispatch def DataSchemaNode childByQName(Module container, QName name) {
329 return container.dataNodeChildByQName(name);
332 private static dispatch def DataSchemaNode childByQName(DataSchemaNode container, QName name) {
336 private static def DataSchemaNode dataNodeChildByQName(DataNodeContainer container, QName name) {
337 var ret = container.getDataChildByName(name);
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);
354 private def toUriString(Object object) {
355 if(object === null) return "";
356 return URLEncoder.encode(object.toString)
359 private def InstanceIdWithSchemaNode collectPathArguments(InstanceIdentifierBuilder builder, List<String> strings,
360 DataNodeContainer parentNode, MountInstance mountPoint, boolean returnJustMountPoint) {
361 checkNotNull(strings)
362 if (parentNode === null) {
366 return new InstanceIdWithSchemaNode(builder.toInstance, parentNode as DataSchemaNode, mountPoint)
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.")
379 if (mountService === null) {
380 throw new ResponseException(SERVICE_UNAVAILABLE, "MountService was not found. "
381 + "Finding behind mount points does not work."
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.")
392 val mountPointSchema = mount.schemaContext;
393 if (mountPointSchema === null) {
394 throw new ResponseException(BAD_REQUEST, "Mount point does not contain any schema with modules.")
397 if (returnJustMountPoint) {
398 return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
401 if (strings.size == 1) { // any data node is not behind mount point
402 return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
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\"")
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.")
417 return collectPathArguments(InstanceIdentifier.builder(), strings.subList(1, strings.size),
418 moduleBehindMountPoint, mount, returnJustMountPoint);
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.")
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.")
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 + "\".")
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")
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)
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")
458 if (!targetNode.isListOrContainer) {
459 throw new ResponseException(BAD_REQUEST,"URI has bad format. Node \"" + strings.head + "\" must be Container or List yang type.")
461 // Number of consumed elements
463 if (targetNode instanceof ListSchemaNode) {
464 val listNode = targetNode as ListSchemaNode;
465 val keysSize = listNode.keyDefinition.size
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 + "\".")
471 val uriKeyValues = strings.subList(consumed, consumed + keysSize);
472 val keyValues = new HashMap<QName, Object>();
474 for (key : listNode.keyDefinition) {
475 val uriKeyValue = uriKeyValues.get(i);
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."
483 keyValues.addKeyValue(listNode.getDataChildByName(key), uriKeyValue);
486 consumed = consumed + i;
487 builder.nodeWithKey(targetNode.QName, keyValues);
490 // Only one instance of node is allowed
491 builder.node(targetNode.QName);
493 if (targetNode instanceof DataNodeContainer) {
494 val remaining = strings.subList(consumed, strings.length);
495 val result = builder.collectPathArguments(remaining, targetNode as DataNodeContainer, mountPoint, returnJustMountPoint);
499 return new InstanceIdWithSchemaNode(builder.toInstance, targetNode, mountPoint)
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
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
517 private def void collectInstanceDataNodeContainers(List<DataSchemaNode> potentialSchemaNodes, DataNodeContainer container,
519 val nodes = container.childNodes.filter[n|n.QName.localName == name]
520 for (potentialNode : nodes) {
521 if (potentialNode.isInstantiatedDataSchema) {
522 potentialSchemaNodes.add(potentialNode)
525 val allCases = container.childNodes.filter(ChoiceNode).map[cases].flatten
526 for (caze : allCases) {
527 collectInstanceDataNodeContainers(potentialSchemaNodes, caze, name)
531 def boolean isInstantiatedDataSchema(DataSchemaNode node) {
533 LeafSchemaNode: return true
534 LeafListSchemaNode: return true
535 ContainerSchemaNode: return true
536 ListSchemaNode: return true
537 default: return false
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;
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)
554 map.put(node.QName, decoded);
557 private static def String toModuleName(String str) {
559 if (str.contains(":")) {
560 val args = str.split(":");
561 if (args.size === 2) {
568 private def String toNodeName(String str) {
569 if (str.contains(":")) {
570 val args = str.split(":");
571 if (args.size === 2) {
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) {
586 return QName.create(namespace, node);
589 private def boolean isListOrContainer(DataSchemaNode node) {
590 return ((node instanceof ListSchemaNode) || (node instanceof ContainerSchemaNode))
593 def getRpcDefinition(String name) {
594 val validName = name.toQName
595 if (validName === null) {
598 return qnameToRpc.get(validName)
601 override onGlobalContextUpdated(SchemaContext context) {
602 this.globalSchema = context;
603 for (operation : context.operations) {
604 val qname = operation.QName;
605 qnameToRpc.put(qname, operation);