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.collect.BiMap
12 import com.google.common.collect.FluentIterable
13 import com.google.common.collect.HashBiMap
15 import java.net.URLDecoder
16 import java.net.URLEncoder
17 import java.util.ArrayList
18 import java.util.HashMap
21 import java.util.concurrent.ConcurrentHashMap
22 import org.opendaylight.controller.sal.core.api.mount.MountInstance
23 import org.opendaylight.controller.sal.core.api.mount.MountService
24 import org.opendaylight.controller.sal.rest.impl.RestUtil
25 import org.opendaylight.controller.sal.rest.impl.RestconfProvider
26 import org.opendaylight.yangtools.yang.common.QName
27 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier
28 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.InstanceIdentifierBuilder
29 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeIdentifier
30 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.NodeIdentifierWithPredicates
31 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier.PathArgument
32 import org.opendaylight.yangtools.yang.data.impl.codec.TypeDefinitionAwareCodec
33 import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode
34 import org.opendaylight.yangtools.yang.model.api.ChoiceNode
35 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode
36 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer
37 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode
38 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode
39 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode
40 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode
41 import org.opendaylight.yangtools.yang.model.api.Module
42 import org.opendaylight.yangtools.yang.model.api.RpcDefinition
43 import org.opendaylight.yangtools.yang.model.api.SchemaContext
44 import org.opendaylight.yangtools.yang.model.api.SchemaServiceListener
45 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition
46 import org.slf4j.LoggerFactory
48 import static com.google.common.base.Preconditions.*
49 import static javax.ws.rs.core.Response.Status.*
51 class ControllerContext implements SchemaServiceListener {
52 val static LOG = LoggerFactory.getLogger(ControllerContext)
53 val static ControllerContext INSTANCE = new ControllerContext
54 val static NULL_VALUE = "null"
55 val static MOUNT_MODULE = "yang-ext"
56 val static MOUNT_NODE = "mount"
57 public val static MOUNT = "yang-ext:mount"
60 var SchemaContext globalSchema;
63 var MountService mountService;
65 private val BiMap<URI, String> uriToModuleName = HashBiMap.create();
66 private val Map<String, URI> moduleNameToUri = uriToModuleName.inverse();
67 private val Map<QName, RpcDefinition> qnameToRpc = new ConcurrentHashMap();
70 if (INSTANCE !== null) {
71 throw new IllegalStateException("Already instantiated");
75 static def getInstance() {
79 private def void checkPreconditions() {
80 if (globalSchema === null) {
81 throw new ResponseException(SERVICE_UNAVAILABLE, RestconfProvider::NOT_INITALIZED_MSG)
85 def setSchemas(SchemaContext schemas) {
86 onGlobalContextUpdated(schemas)
89 public def InstanceIdWithSchemaNode toInstanceIdentifier(String restconfInstance) {
91 val pathArgs = restconfInstance.split("/");
95 if (pathArgs.head.empty) {
98 val startModule = pathArgs.head.toModuleName();
99 if (startModule === null) {
100 throw new ResponseException(BAD_REQUEST, "First node in URI has to be in format \"moduleName:nodeName\"")
102 val iiWithSchemaNode = collectPathArguments(InstanceIdentifier.builder(), pathArgs,
103 globalSchema.getLatestModule(startModule), null);
104 if (iiWithSchemaNode === null) {
105 throw new ResponseException(BAD_REQUEST, "URI has bad format")
107 return iiWithSchemaNode
110 private def getLatestModule(SchemaContext schema, String moduleName) {
111 checkArgument(schema !== null);
112 checkArgument(moduleName !== null && !moduleName.empty)
113 val modules = schema.modules.filter[m|m.name == moduleName]
114 return modules.filterLatestModule
117 private def filterLatestModule(Iterable<Module> modules) {
118 var latestModule = modules.head
119 for (module : modules) {
120 if (module.revision.after(latestModule.revision)) {
121 latestModule = module
127 def findModuleByName(String moduleName) {
129 checkArgument(moduleName !== null && !moduleName.empty)
130 return globalSchema.getLatestModule(moduleName)
133 def findModuleByName(MountInstance mountPoint, String moduleName) {
134 checkArgument(moduleName !== null && mountPoint !== null)
135 val mountPointSchema = mountPoint.schemaContext;
136 return mountPointSchema?.getLatestModule(moduleName);
139 def findModuleByNamespace(URI namespace) {
141 checkArgument(namespace !== null)
142 val moduleSchemas = globalSchema.findModuleByNamespace(namespace)
143 return moduleSchemas?.filterLatestModule
146 def findModuleByNamespace(MountInstance mountPoint, URI namespace) {
147 checkArgument(namespace !== null && mountPoint !== null)
148 val mountPointSchema = mountPoint.schemaContext;
149 val moduleSchemas = mountPointSchema?.findModuleByNamespace(namespace)
150 return moduleSchemas?.filterLatestModule
153 def String toFullRestconfIdentifier(InstanceIdentifier path) {
155 val elements = path.path;
156 val ret = new StringBuilder();
157 val startQName = elements.get(0).nodeType;
158 val initialModule = globalSchema.findModuleByNamespaceAndRevision(startQName.namespace, startQName.revision)
159 var node = initialModule as DataSchemaNode;
160 for (element : elements) {
161 node = node.childByQName(element.nodeType);
162 ret.append(element.toRestconfIdentifier(node));
167 private def dispatch CharSequence toRestconfIdentifier(NodeIdentifier argument, DataSchemaNode node) {
168 '''/«argument.nodeType.toRestconfIdentifier()»'''
171 private def dispatch CharSequence toRestconfIdentifier(NodeIdentifierWithPredicates argument, ListSchemaNode node) {
172 val nodeIdentifier = argument.nodeType.toRestconfIdentifier();
173 val keyValues = argument.keyValues;
174 return '''/«nodeIdentifier»/«FOR key : node.keyDefinition SEPARATOR "/"»«keyValues.get(key).toUriString»«ENDFOR»'''
177 private def dispatch CharSequence toRestconfIdentifier(PathArgument argument, DataSchemaNode node) {
178 throw new IllegalArgumentException("Conversion of generic path argument is not supported");
181 def findModuleNameByNamespace(URI namespace) {
183 var moduleName = uriToModuleName.get(namespace)
184 if (moduleName === null) {
185 val module = findModuleByNamespace(namespace)
186 if (module === null) return null
187 moduleName = module.name
188 uriToModuleName.put(namespace, moduleName)
193 def findModuleNameByNamespace(MountInstance mountPoint, URI namespace) {
194 val module = mountPoint.findModuleByNamespace(namespace);
198 def findNamespaceByModuleName(String moduleName) {
199 var namespace = moduleNameToUri.get(moduleName)
200 if (namespace === null) {
201 var module = findModuleByName(moduleName)
202 if(module === null) return null
203 namespace = module.namespace
204 uriToModuleName.put(namespace, moduleName)
209 def findNamespaceByModuleName(MountInstance mountPoint, String moduleName) {
210 val module = mountPoint.findModuleByName(moduleName)
211 return module?.namespace
214 def CharSequence toRestconfIdentifier(QName qname) {
216 var module = uriToModuleName.get(qname.namespace)
217 if (module === null) {
218 val moduleSchema = globalSchema.findModuleByNamespaceAndRevision(qname.namespace, qname.revision);
219 if(moduleSchema === null) return null
220 uriToModuleName.put(qname.namespace, moduleSchema.name)
221 module = moduleSchema.name;
223 return '''«module»:«qname.localName»''';
226 def CharSequence toRestconfIdentifier(MountInstance mountPoint, QName qname) {
227 val moduleSchema = mountPoint?.schemaContext.findModuleByNamespaceAndRevision(qname.namespace, qname.revision);
228 if(moduleSchema === null) return null
229 val module = moduleSchema.name;
230 return '''«module»:«qname.localName»''';
233 private static dispatch def DataSchemaNode childByQName(ChoiceNode container, QName name) {
234 for (caze : container.cases) {
235 val ret = caze.childByQName(name)
243 private static dispatch def DataSchemaNode childByQName(ChoiceCaseNode container, QName name) {
244 val ret = container.getDataChildByName(name);
248 private static dispatch def DataSchemaNode childByQName(ContainerSchemaNode container, QName name) {
249 return container.dataNodeChildByQName(name);
252 private static dispatch def DataSchemaNode childByQName(ListSchemaNode container, QName name) {
253 return container.dataNodeChildByQName(name);
256 private static dispatch def DataSchemaNode childByQName(DataSchemaNode container, QName name) {
260 private static def DataSchemaNode dataNodeChildByQName(DataNodeContainer container, QName name) {
261 var ret = container.getDataChildByName(name);
264 // Find in Choice Cases
265 for (node : container.childNodes) {
266 if (node instanceof ChoiceCaseNode) {
267 val caseNode = (node as ChoiceCaseNode);
268 ret = caseNode.childByQName(name);
278 private def toUriString(Object object) {
279 if(object === null) return "";
280 return URLEncoder.encode(object.toString)
283 private def InstanceIdWithSchemaNode collectPathArguments(InstanceIdentifierBuilder builder, List<String> strings,
284 DataNodeContainer parentNode, MountInstance mountPoint) {
285 checkNotNull(strings)
286 if (parentNode === null) {
290 return new InstanceIdWithSchemaNode(builder.toInstance, parentNode as DataSchemaNode, mountPoint)
293 val nodeName = strings.head.toNodeName
294 val moduleName = strings.head.toModuleName
295 var DataSchemaNode targetNode = null
296 if (!moduleName.nullOrEmpty) {
297 // if it is mount point
298 if (moduleName == MOUNT_MODULE && nodeName == MOUNT_NODE) {
299 if (mountPoint !== null) {
300 throw new ResponseException(BAD_REQUEST, "Restconf supports just one mount point in URI.")
303 if (mountService === null) {
304 throw new ResponseException(SERVICE_UNAVAILABLE, "MountService was not found. "
305 + "Finding behind mount points does not work."
309 val partialPath = builder.toInstance;
310 val mount = mountService.getMountPoint(partialPath)
311 if (mount === null) {
312 LOG.debug("Instance identifier to missing mount point: {}", partialPath)
313 throw new ResponseException(BAD_REQUEST, "Mount point does not exist.")
316 val mountPointSchema = mount.schemaContext;
317 if (mountPointSchema === null) {
318 throw new ResponseException(BAD_REQUEST, "Mount point does not contain any schema with modules.")
321 if (strings.size == 1) { // any data node is not behind mount point
322 return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
325 val moduleNameBehindMountPoint = strings.get(1).toModuleName()
326 if (moduleNameBehindMountPoint === null) {
327 throw new ResponseException(BAD_REQUEST,
328 "First node after mount point in URI has to be in format \"moduleName:nodeName\"")
331 val moduleBehindMountPoint = mountPointSchema.getLatestModule(moduleNameBehindMountPoint)
332 if (moduleBehindMountPoint === null) {
333 throw new ResponseException(BAD_REQUEST,
334 "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
337 return collectPathArguments(InstanceIdentifier.builder(), strings.subList(1, strings.size),
338 moduleBehindMountPoint, mount);
341 var Module module = null;
342 if (mountPoint === null) {
343 module = globalSchema.getLatestModule(moduleName)
344 if (module === null) {
345 throw new ResponseException(BAD_REQUEST,
346 "URI has bad format. \"" + moduleName + "\" module does not exist.")
349 module = mountPoint.schemaContext?.getLatestModule(moduleName)
350 if (module === null) {
351 throw new ResponseException(BAD_REQUEST,
352 "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
355 targetNode = parentNode.findInstanceDataChildByNameAndNamespace(nodeName, module.namespace)
356 if (targetNode === null) {
357 throw new ResponseException(BAD_REQUEST, "URI has bad format. Possible reasons:\n" +
358 "1. \"" + strings.head + "\" was not found in parent data node.\n" +
359 "2. \"" + strings.head + "\" is behind mount point. Then it should be in format \"/" + MOUNT + "/" + strings.head + "\".")
361 } else { // string without module name
362 val potentialSchemaNodes = parentNode.findInstanceDataChildrenByName(nodeName)
363 if (potentialSchemaNodes.size > 1) {
364 val StringBuilder namespacesOfPotentialModules = new StringBuilder;
365 for (potentialNodeSchema : potentialSchemaNodes) {
366 namespacesOfPotentialModules.append(" ").append(potentialNodeSchema.QName.namespace.toString).append("\n")
368 throw new ResponseException(BAD_REQUEST, "URI has bad format. Node \"" + nodeName + "\" is added as augment from more than one module. "
369 + "Therefore the node must have module name and it has to be in format \"moduleName:nodeName\"."
370 + "\nThe node is added as augment from modules with namespaces:\n" + namespacesOfPotentialModules)
372 targetNode = potentialSchemaNodes.head
373 if (targetNode === null) {
374 throw new ResponseException(BAD_REQUEST, "URI has bad format. \"" + nodeName + "\" was not found in parent data node.\n")
378 if (!(targetNode instanceof ListSchemaNode) && !(targetNode instanceof ContainerSchemaNode)) {
379 throw new ResponseException(BAD_REQUEST,"URI has bad format. Node \"" + strings.head + "\" must be Container or List yang type.")
381 // Number of consumed elements
383 if (targetNode instanceof ListSchemaNode) {
384 val listNode = targetNode as ListSchemaNode;
385 val keysSize = listNode.keyDefinition.size
387 // every key has to be filled
388 if ((strings.length - consumed) < keysSize) {
389 throw new ResponseException(BAD_REQUEST,"Missing key for list \"" + listNode.QName.localName + "\".")
391 val uriKeyValues = strings.subList(consumed, consumed + keysSize);
392 val keyValues = new HashMap<QName, Object>();
394 for (key : listNode.keyDefinition) {
395 val uriKeyValue = uriKeyValues.get(i);
397 // key value cannot be NULL
398 if (uriKeyValue.equals(NULL_VALUE)) {
399 throw new ResponseException(BAD_REQUEST, "URI has bad format. List \"" + listNode.QName.localName
400 + "\" cannot contain \"null\" value as a key."
403 keyValues.addKeyValue(listNode.getDataChildByName(key), uriKeyValue);
406 consumed = consumed + i;
407 builder.nodeWithKey(targetNode.QName, keyValues);
410 // Only one instance of node is allowed
411 builder.node(targetNode.QName);
413 if (targetNode instanceof DataNodeContainer) {
414 val remaining = strings.subList(consumed, strings.length);
415 val result = builder.collectPathArguments(remaining, targetNode as DataNodeContainer, mountPoint);
419 return new InstanceIdWithSchemaNode(builder.toInstance, targetNode, mountPoint)
422 def DataSchemaNode findInstanceDataChildByNameAndNamespace(DataNodeContainer container,
423 String name, URI namespace) {
424 Preconditions.checkNotNull(namespace)
425 val potentialSchemaNodes = container.findInstanceDataChildrenByName(name)
426 return potentialSchemaNodes.filter[n|n.QName.namespace == namespace].head
429 def List<DataSchemaNode> findInstanceDataChildrenByName(DataNodeContainer container, String name) {
430 Preconditions.checkNotNull(container)
431 Preconditions.checkNotNull(name)
432 val instantiatedDataNodeContainers = new ArrayList
433 instantiatedDataNodeContainers.collectInstanceDataNodeContainers(container, name)
434 return instantiatedDataNodeContainers
437 private def void collectInstanceDataNodeContainers(List<DataSchemaNode> potentialSchemaNodes, DataNodeContainer container,
439 val nodes = container.childNodes.filter[n|n.QName.localName == name]
440 for (potentialNode : nodes) {
441 if (potentialNode.isInstantiatedDataSchema) {
442 potentialSchemaNodes.add(potentialNode)
445 val allCases = container.childNodes.filter(ChoiceNode).map[cases].flatten
446 for (caze : allCases) {
447 collectInstanceDataNodeContainers(potentialSchemaNodes, caze, name)
451 def boolean isInstantiatedDataSchema(DataSchemaNode node) {
453 LeafSchemaNode: return true
454 LeafListSchemaNode: return true
455 ContainerSchemaNode: return true
456 ListSchemaNode: return true
457 default: return false
461 private def void addKeyValue(HashMap<QName, Object> map, DataSchemaNode node, String uriValue) {
462 checkNotNull(uriValue);
463 checkArgument(node instanceof LeafSchemaNode);
464 val urlDecoded = URLDecoder.decode(uriValue);
465 val typedef = (node as LeafSchemaNode).type;
467 var decoded = TypeDefinitionAwareCodec.from(typedef)?.deserialize(urlDecoded)
468 if(decoded === null) {
469 var baseType = RestUtil.resolveBaseTypeFrom(typedef)
470 if(baseType instanceof IdentityrefTypeDefinition) {
471 decoded = toQName(urlDecoded)
474 map.put(node.QName, decoded);
477 private static def String toModuleName(String str) {
479 if (str.contains(":")) {
480 val args = str.split(":");
481 if (args.size === 2) {
488 private def String toNodeName(String str) {
489 if (str.contains(":")) {
490 val args = str.split(":");
491 if (args.size === 2) {
498 private def QName toQName(String name) {
499 val module = name.toModuleName;
500 val node = name.toNodeName;
501 val namespace = FluentIterable.from(globalSchema.modules.sort[o1,o2 | o1.revision.compareTo(o2.revision)]) //
502 .transform[QName.create(namespace,revision,it.name)].findFirst[module == localName]
504 return QName.create(namespace,node);
507 def getRpcDefinition(String name) {
508 return qnameToRpc.get(name.toQName)
511 override onGlobalContextUpdated(SchemaContext context) {
512 this.globalSchema = context;
513 for (operation : context.operations) {
514 val qname = operation.QName;
515 qnameToRpc.put(qname, operation);