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.SchemaContextListener
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 SchemaContextListener {
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 URLEncoder.encode(object.toString,URI_ENCODING_CHAR_SET)
362 private def InstanceIdWithSchemaNode collectPathArguments(InstanceIdentifierBuilder builder, List<String> strings,
363 DataNodeContainer parentNode, MountInstance mountPoint, boolean returnJustMountPoint) {
364 checkNotNull(strings)
365 if (parentNode === null) {
369 return new InstanceIdWithSchemaNode(builder.toInstance, parentNode as DataSchemaNode, mountPoint)
372 val nodeName = strings.head.toNodeName
373 val moduleName = strings.head.toModuleName
374 var DataSchemaNode targetNode = null
375 if (!moduleName.nullOrEmpty) {
376 // if it is mount point
377 if (moduleName == MOUNT_MODULE && nodeName == MOUNT_NODE) {
378 if (mountPoint !== null) {
379 throw new ResponseException(BAD_REQUEST, "Restconf supports just one mount point in URI.")
382 if (mountService === null) {
383 throw new ResponseException(SERVICE_UNAVAILABLE, "MountService was not found. "
384 + "Finding behind mount points does not work."
388 val partialPath = builder.toInstance;
389 val mount = mountService.getMountPoint(partialPath)
390 if (mount === null) {
391 LOG.debug("Instance identifier to missing mount point: {}", partialPath)
392 throw new ResponseException(BAD_REQUEST, "Mount point does not exist.")
395 val mountPointSchema = mount.schemaContext;
396 if (mountPointSchema === null) {
397 throw new ResponseException(BAD_REQUEST, "Mount point does not contain any schema with modules.")
400 if (returnJustMountPoint) {
401 return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
404 if (strings.size == 1) { // any data node is not behind mount point
405 return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
408 val moduleNameBehindMountPoint = strings.get(1).toModuleName()
409 if (moduleNameBehindMountPoint === null) {
410 throw new ResponseException(BAD_REQUEST,
411 "First node after mount point in URI has to be in format \"moduleName:nodeName\"")
414 val moduleBehindMountPoint = mountPointSchema.getLatestModule(moduleNameBehindMountPoint)
415 if (moduleBehindMountPoint === null) {
416 throw new ResponseException(BAD_REQUEST,
417 "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
420 return collectPathArguments(InstanceIdentifier.builder(), strings.subList(1, strings.size),
421 moduleBehindMountPoint, mount, returnJustMountPoint);
424 var Module module = null;
425 if (mountPoint === null) {
426 module = globalSchema.getLatestModule(moduleName)
427 if (module === null) {
428 throw new ResponseException(BAD_REQUEST,
429 "URI has bad format. \"" + moduleName + "\" module does not exist.")
432 module = mountPoint.schemaContext?.getLatestModule(moduleName)
433 if (module === null) {
434 throw new ResponseException(BAD_REQUEST,
435 "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
438 targetNode = parentNode.findInstanceDataChildByNameAndNamespace(nodeName, module.namespace)
439 if (targetNode === null) {
440 throw new ResponseException(BAD_REQUEST, "URI has bad format. Possible reasons:\n" +
441 "1. \"" + strings.head + "\" was not found in parent data node.\n" +
442 "2. \"" + strings.head + "\" is behind mount point. Then it should be in format \"/" + MOUNT + "/" + strings.head + "\".")
444 } else { // string without module name
445 val potentialSchemaNodes = parentNode.findInstanceDataChildrenByName(nodeName)
446 if (potentialSchemaNodes.size > 1) {
447 val StringBuilder namespacesOfPotentialModules = new StringBuilder;
448 for (potentialNodeSchema : potentialSchemaNodes) {
449 namespacesOfPotentialModules.append(" ").append(potentialNodeSchema.QName.namespace.toString).append("\n")
451 throw new ResponseException(BAD_REQUEST, "URI has bad format. Node \"" + nodeName + "\" is added as augment from more than one module. "
452 + "Therefore the node must have module name and it has to be in format \"moduleName:nodeName\"."
453 + "\nThe node is added as augment from modules with namespaces:\n" + namespacesOfPotentialModules)
455 targetNode = potentialSchemaNodes.head
456 if (targetNode === null) {
457 throw new ResponseException(BAD_REQUEST, "URI has bad format. \"" + nodeName + "\" was not found in parent data node.\n")
461 if (!targetNode.isListOrContainer) {
462 throw new ResponseException(BAD_REQUEST,"URI has bad format. Node \"" + strings.head + "\" must be Container or List yang type.")
464 // Number of consumed elements
466 if (targetNode instanceof ListSchemaNode) {
467 val listNode = targetNode as ListSchemaNode;
468 val keysSize = listNode.keyDefinition.size
470 // every key has to be filled
471 if ((strings.length - consumed) < keysSize) {
472 throw new ResponseException(BAD_REQUEST,"Missing key for list \"" + listNode.QName.localName + "\".")
474 val uriKeyValues = strings.subList(consumed, consumed + keysSize);
475 val keyValues = new HashMap<QName, Object>();
477 for (key : listNode.keyDefinition) {
478 val uriKeyValue = uriKeyValues.get(i);
480 // key value cannot be NULL
481 if (uriKeyValue.equals(NULL_VALUE)) {
482 throw new ResponseException(BAD_REQUEST, "URI has bad format. List \"" + listNode.QName.localName
483 + "\" cannot contain \"null\" value as a key."
486 keyValues.addKeyValue(listNode.getDataChildByName(key), uriKeyValue, mountPoint);
489 consumed = consumed + i;
490 builder.nodeWithKey(targetNode.QName, keyValues);
493 // Only one instance of node is allowed
494 builder.node(targetNode.QName);
496 if (targetNode instanceof DataNodeContainer) {
497 val remaining = strings.subList(consumed, strings.length);
498 val result = builder.collectPathArguments(remaining, targetNode as DataNodeContainer, mountPoint, returnJustMountPoint);
502 return new InstanceIdWithSchemaNode(builder.toInstance, targetNode, mountPoint)
505 def DataSchemaNode findInstanceDataChildByNameAndNamespace(DataNodeContainer container,
506 String name, URI namespace) {
507 Preconditions.checkNotNull(namespace)
508 val potentialSchemaNodes = container.findInstanceDataChildrenByName(name)
509 return potentialSchemaNodes.filter[n|n.QName.namespace == namespace].head
512 def List<DataSchemaNode> findInstanceDataChildrenByName(DataNodeContainer container, String name) {
513 Preconditions.checkNotNull(container)
514 Preconditions.checkNotNull(name)
515 val instantiatedDataNodeContainers = new ArrayList
516 instantiatedDataNodeContainers.collectInstanceDataNodeContainers(container, name)
517 return instantiatedDataNodeContainers
520 private def void collectInstanceDataNodeContainers(List<DataSchemaNode> potentialSchemaNodes, DataNodeContainer container,
522 val nodes = container.childNodes.filter[n|n.QName.localName == name]
523 for (potentialNode : nodes) {
524 if (potentialNode.isInstantiatedDataSchema) {
525 potentialSchemaNodes.add(potentialNode)
528 val allCases = container.childNodes.filter(ChoiceNode).map[cases].flatten
529 for (caze : allCases) {
530 collectInstanceDataNodeContainers(potentialSchemaNodes, caze, name)
534 def boolean isInstantiatedDataSchema(DataSchemaNode node) {
536 LeafSchemaNode: return true
537 LeafListSchemaNode: return true
538 ContainerSchemaNode: return true
539 ListSchemaNode: return true
540 default: return false
544 private def void addKeyValue(HashMap<QName, Object> map, DataSchemaNode node, String uriValue, MountInstance mountPoint) {
545 checkNotNull(uriValue);
546 checkArgument(node instanceof LeafSchemaNode);
547 val urlDecoded = URLDecoder.decode(uriValue);
548 val typedef = (node as LeafSchemaNode).type;
550 var decoded = RestCodec.from(typedef, mountPoint)?.deserialize(urlDecoded)
551 var additionalInfo = ""
552 if(decoded === null) {
553 var baseType = RestUtil.resolveBaseTypeFrom(typedef)
554 if(baseType instanceof IdentityrefTypeDefinition) {
555 decoded = toQName(urlDecoded)
556 additionalInfo = "For key which is of type identityref it should be in format module_name:identity_name."
559 if (decoded === null) {
560 throw new ResponseException(BAD_REQUEST, uriValue + " from URI can't be resolved. "+ additionalInfo )
563 map.put(node.QName, decoded);
566 private static def String toModuleName(String str) {
568 if (str.contains(":")) {
569 val args = str.split(":");
570 if (args.size === 2) {
577 private def String toNodeName(String str) {
578 if (str.contains(":")) {
579 val args = str.split(":");
580 if (args.size === 2) {
587 private def QName toQName(String name) {
588 val module = name.toModuleName;
589 val node = name.toNodeName;
590 val namespace = FluentIterable.from(globalSchema.modules.sort[o1,o2 | o1.revision.compareTo(o2.revision)])
591 .transform[QName.create(namespace,revision,it.name)].findFirst[module == localName]
592 if (namespace === null) {
595 return QName.create(namespace, node);
598 private def boolean isListOrContainer(DataSchemaNode node) {
599 return ((node instanceof ListSchemaNode) || (node instanceof ContainerSchemaNode))
602 def getRpcDefinition(String name) {
603 val validName = name.toQName
604 if (validName === null) {
607 return qnameToRpc.get(validName)
610 override onGlobalContextUpdated(SchemaContext context) {
611 if (context !== null) {
613 this.globalSchema = context;
614 for (operation : context.operations) {
615 val qname = operation.QName;
616 qnameToRpc.put(qname, operation);
622 def urlPathArgsDecode(List<String> strings) {
623 val List<String> decodedPathArgs = new ArrayList();
624 for (pathArg : strings) {
625 decodedPathArgs.add(URLDecoder.decode(pathArg, URI_ENCODING_CHAR_SET))
627 return decodedPathArgs
630 def urlPathArgDecode(String pathArg) {
631 if (pathArg !== null) {
632 return URLDecoder.decode(pathArg, URI_ENCODING_CHAR_SET)