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"
60 val static URI_ENCODING_CHAR_SET = "ISO-8859-1"
61 val static URI_SLASH_PLACEHOLDER = "%2F";
64 var SchemaContext globalSchema;
67 var MountService mountService;
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();
74 if (INSTANCE !== null) {
75 throw new IllegalStateException("Already instantiated");
79 static def getInstance() {
83 private def void checkPreconditions() {
84 if (globalSchema === null) {
85 throw new ResponseException(SERVICE_UNAVAILABLE, RestconfProvider::NOT_INITALIZED_MSG)
89 def setSchemas(SchemaContext schemas) {
90 onGlobalContextUpdated(schemas)
93 def InstanceIdWithSchemaNode toInstanceIdentifier(String restconfInstance) {
94 return restconfInstance.toIdentifier(false)
97 def InstanceIdWithSchemaNode toMountPointIdentifier(String restconfInstance) {
98 return restconfInstance.toIdentifier(true)
101 private def InstanceIdWithSchemaNode toIdentifier(String restconfInstance, boolean toMountPointIdentifier) {
103 val encodedPathArgs = Lists.newArrayList(Splitter.on("/").split(restconfInstance))
104 val pathArgs = urlPathArgsDecode(encodedPathArgs)
105 pathArgs.omitFirstAndLastEmptyString
106 if (pathArgs.empty) {
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\"")
113 var InstanceIdWithSchemaNode iiWithSchemaNode = null;
114 if (toMountPointIdentifier) {
115 iiWithSchemaNode = collectPathArguments(InstanceIdentifier.builder(), pathArgs,
116 globalSchema.getLatestModule(startModule), null, true);
118 iiWithSchemaNode = collectPathArguments(InstanceIdentifier.builder(), pathArgs,
119 globalSchema.getLatestModule(startModule), null, false);
121 if (iiWithSchemaNode === null) {
122 throw new ResponseException(BAD_REQUEST, "URI has bad format")
124 return iiWithSchemaNode
127 private def omitFirstAndLastEmptyString(List<String> list) {
131 if (list.head.empty) {
137 if (list.last.empty) {
138 list.remove(list.indexOf(list.last))
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
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
160 def findModuleByName(String moduleName) {
162 checkArgument(moduleName !== null && !moduleName.empty)
163 return globalSchema.getLatestModule(moduleName)
166 def findModuleByName(MountInstance mountPoint, String moduleName) {
167 checkArgument(moduleName !== null && mountPoint !== null)
168 val mountPointSchema = mountPoint.schemaContext;
169 return mountPointSchema?.getLatestModule(moduleName);
172 def findModuleByNamespace(URI namespace) {
174 checkArgument(namespace !== null)
175 val moduleSchemas = globalSchema.findModuleByNamespace(namespace)
176 return moduleSchemas?.filterLatestModule
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
186 def findModuleByNameAndRevision(QName module) {
188 checkArgument(module !== null && module.localName !== null && module.revision !== null)
189 return globalSchema.findModuleByName(module.localName, module.revision)
192 def findModuleByNameAndRevision(MountInstance mountPoint, QName module) {
194 checkArgument(module !== null && module.localName !== null && module.revision !== null && mountPoint !== null)
195 return mountPoint.schemaContext?.findModuleByName(module.localName, module.revision)
198 def getDataNodeContainerFor(InstanceIdentifier path) {
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) {
209 node = potentialNode as DataNodeContainer
214 def String toFullRestconfIdentifier(InstanceIdentifier path) {
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) {
226 node = potentialNode as DataNodeContainer
227 ret.append(element.convertToRestconfIdentifier(node));
232 private def dispatch CharSequence convertToRestconfIdentifier(NodeIdentifier argument, ContainerSchemaNode node) {
233 '''/«argument.nodeType.toRestconfIdentifier()»'''
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»'''
242 private def dispatch CharSequence convertToRestconfIdentifier(PathArgument argument, DataNodeContainer node) {
243 throw new IllegalArgumentException("Conversion of generic path argument is not supported");
246 def findModuleNameByNamespace(URI namespace) {
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)
258 def findModuleNameByNamespace(MountInstance mountPoint, URI namespace) {
259 val module = mountPoint.findModuleByNamespace(namespace);
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)
274 def findNamespaceByModuleName(MountInstance mountPoint, String moduleName) {
275 val module = mountPoint.findModuleByName(moduleName)
276 return module?.namespace
279 def getAllModules(MountInstance mountPoint) {
281 return mountPoint?.schemaContext?.modules
284 def getAllModules() {
286 return globalSchema.modules
289 def CharSequence toRestconfIdentifier(QName qname) {
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;
298 return '''«module»:«qname.localName»''';
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»''';
308 private static dispatch def DataSchemaNode childByQName(ChoiceNode container, QName name) {
309 for (caze : container.cases) {
310 val ret = caze.childByQName(name)
318 private static dispatch def DataSchemaNode childByQName(ChoiceCaseNode container, QName name) {
319 val ret = container.getDataChildByName(name);
323 private static dispatch def DataSchemaNode childByQName(ContainerSchemaNode container, QName name) {
324 return container.dataNodeChildByQName(name);
327 private static dispatch def DataSchemaNode childByQName(ListSchemaNode container, QName name) {
328 return container.dataNodeChildByQName(name);
331 private static dispatch def DataSchemaNode childByQName(Module container, QName name) {
332 return container.dataNodeChildByQName(name);
335 private static dispatch def DataSchemaNode childByQName(DataSchemaNode container, QName name) {
339 private static def DataSchemaNode dataNodeChildByQName(DataNodeContainer container, QName name) {
340 var ret = container.getDataChildByName(name);
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);
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)
363 private def InstanceIdWithSchemaNode collectPathArguments(InstanceIdentifierBuilder builder, List<String> strings,
364 DataNodeContainer parentNode, MountInstance mountPoint, boolean returnJustMountPoint) {
365 checkNotNull(strings)
366 if (parentNode === null) {
370 return new InstanceIdWithSchemaNode(builder.toInstance, parentNode as DataSchemaNode, mountPoint)
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.")
383 if (mountService === null) {
384 throw new ResponseException(SERVICE_UNAVAILABLE, "MountService was not found. "
385 + "Finding behind mount points does not work."
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.")
396 val mountPointSchema = mount.schemaContext;
397 if (mountPointSchema === null) {
398 throw new ResponseException(BAD_REQUEST, "Mount point does not contain any schema with modules.")
401 if (returnJustMountPoint) {
402 return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
405 if (strings.size == 1) { // any data node is not behind mount point
406 return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
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\"")
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.")
421 return collectPathArguments(InstanceIdentifier.builder(), strings.subList(1, strings.size),
422 moduleBehindMountPoint, mount, returnJustMountPoint);
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.")
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.")
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 + "\".")
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")
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)
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")
462 if (!targetNode.isListOrContainer) {
463 throw new ResponseException(BAD_REQUEST,"URI has bad format. Node \"" + strings.head + "\" must be Container or List yang type.")
465 // Number of consumed elements
467 if (targetNode instanceof ListSchemaNode) {
468 val listNode = targetNode as ListSchemaNode;
469 val keysSize = listNode.keyDefinition.size
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 + "\".")
475 val uriKeyValues = strings.subList(consumed, consumed + keysSize);
476 val keyValues = new HashMap<QName, Object>();
478 for (key : listNode.keyDefinition) {
479 val uriKeyValue = uriKeyValues.get(i);
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."
487 keyValues.addKeyValue(listNode.getDataChildByName(key), uriKeyValue);
490 consumed = consumed + i;
491 builder.nodeWithKey(targetNode.QName, keyValues);
494 // Only one instance of node is allowed
495 builder.node(targetNode.QName);
497 if (targetNode instanceof DataNodeContainer) {
498 val remaining = strings.subList(consumed, strings.length);
499 val result = builder.collectPathArguments(remaining, targetNode as DataNodeContainer, mountPoint, returnJustMountPoint);
503 return new InstanceIdWithSchemaNode(builder.toInstance, targetNode, mountPoint)
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
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
521 private def void collectInstanceDataNodeContainers(List<DataSchemaNode> potentialSchemaNodes, DataNodeContainer container,
523 val nodes = container.childNodes.filter[n|n.QName.localName == name]
524 for (potentialNode : nodes) {
525 if (potentialNode.isInstantiatedDataSchema) {
526 potentialSchemaNodes.add(potentialNode)
529 val allCases = container.childNodes.filter(ChoiceNode).map[cases].flatten
530 for (caze : allCases) {
531 collectInstanceDataNodeContainers(potentialSchemaNodes, caze, name)
535 def boolean isInstantiatedDataSchema(DataSchemaNode node) {
537 LeafSchemaNode: return true
538 LeafListSchemaNode: return true
539 ContainerSchemaNode: return true
540 ListSchemaNode: return true
541 default: return false
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;
551 var decoded = TypeDefinitionAwareCodec.from(typedef)?.deserialize(urlDecoded)
552 if(decoded === null) {
553 var baseType = RestUtil.resolveBaseTypeFrom(typedef)
554 if(baseType instanceof IdentityrefTypeDefinition) {
555 decoded = toQName(urlDecoded)
558 map.put(node.QName, decoded);
561 private static def String toModuleName(String str) {
563 if (str.contains(":")) {
564 val args = str.split(":");
565 if (args.size === 2) {
572 private def String toNodeName(String str) {
573 if (str.contains(":")) {
574 val args = str.split(":");
575 if (args.size === 2) {
582 private def QName toQName(String name) {
583 val module = name.toModuleName;
584 val node = name.toNodeName;
585 val namespace = FluentIterable.from(globalSchema.modules.sort[o1,o2 | o1.revision.compareTo(o2.revision)])
586 .transform[QName.create(namespace,revision,it.name)].findFirst[module == localName]
587 if (namespace === null) {
590 return QName.create(namespace, node);
593 private def boolean isListOrContainer(DataSchemaNode node) {
594 return ((node instanceof ListSchemaNode) || (node instanceof ContainerSchemaNode))
597 def getRpcDefinition(String name) {
598 val validName = name.toQName
599 if (validName === null) {
602 return qnameToRpc.get(validName)
605 override onGlobalContextUpdated(SchemaContext context) {
606 if (context !== null) {
608 this.globalSchema = context;
609 for (operation : context.operations) {
610 val qname = operation.QName;
611 qnameToRpc.put(qname, operation);
617 def urlPathArgsDecode(List<String> strings) {
618 val List<String> decodedPathArgs = new ArrayList();
619 for (pathArg : strings) {
620 decodedPathArgs.add(URLDecoder.decode(pathArg, URI_ENCODING_CHAR_SET))
622 return decodedPathArgs