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) throw new IllegalArgumentException()
220 uriToModuleName.put(qname.namespace, moduleSchema.name)
221 module = moduleSchema.name;
223 return '''«module»:«qname.localName»''';
226 private static dispatch def DataSchemaNode childByQName(ChoiceNode container, QName name) {
227 for (caze : container.cases) {
228 val ret = caze.childByQName(name)
236 private static dispatch def DataSchemaNode childByQName(ChoiceCaseNode container, QName name) {
237 val ret = container.getDataChildByName(name);
241 private static dispatch def DataSchemaNode childByQName(ContainerSchemaNode container, QName name) {
242 return container.dataNodeChildByQName(name);
245 private static dispatch def DataSchemaNode childByQName(ListSchemaNode container, QName name) {
246 return container.dataNodeChildByQName(name);
249 private static dispatch def DataSchemaNode childByQName(DataSchemaNode container, QName name) {
253 private static def DataSchemaNode dataNodeChildByQName(DataNodeContainer container, QName name) {
254 var ret = container.getDataChildByName(name);
257 // Find in Choice Cases
258 for (node : container.childNodes) {
259 if (node instanceof ChoiceCaseNode) {
260 val caseNode = (node as ChoiceCaseNode);
261 ret = caseNode.childByQName(name);
271 private def toUriString(Object object) {
272 if(object === null) return "";
273 return URLEncoder.encode(object.toString)
276 private def InstanceIdWithSchemaNode collectPathArguments(InstanceIdentifierBuilder builder, List<String> strings,
277 DataNodeContainer parentNode, MountInstance mountPoint) {
278 checkNotNull(strings)
279 if (parentNode === null) {
283 return new InstanceIdWithSchemaNode(builder.toInstance, parentNode as DataSchemaNode, mountPoint)
286 val nodeName = strings.head.toNodeName
287 val moduleName = strings.head.toModuleName
288 var DataSchemaNode targetNode = null
289 if (!moduleName.nullOrEmpty) {
290 // if it is mount point
291 if (moduleName == MOUNT_MODULE && nodeName == MOUNT_NODE) {
292 if (mountPoint !== null) {
293 throw new ResponseException(BAD_REQUEST, "Restconf supports just one mount point in URI.")
296 if (mountService === null) {
297 throw new ResponseException(SERVICE_UNAVAILABLE, "MountService was not found. "
298 + "Finding behind mount points does not work."
302 val partialPath = builder.toInstance;
303 val mount = mountService.getMountPoint(partialPath)
304 if (mount === null) {
305 LOG.debug("Instance identifier to missing mount point: {}", partialPath)
306 throw new ResponseException(BAD_REQUEST, "Mount point does not exist.")
309 val mountPointSchema = mount.schemaContext;
310 if (mountPointSchema === null) {
311 throw new ResponseException(BAD_REQUEST, "Mount point does not contain any schema with modules.")
314 if (strings.size == 1) { // any data node is not behind mount point
315 return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount)
318 val moduleNameBehindMountPoint = strings.get(1).toModuleName()
319 if (moduleNameBehindMountPoint === null) {
320 throw new ResponseException(BAD_REQUEST,
321 "First node after mount point in URI has to be in format \"moduleName:nodeName\"")
324 val moduleBehindMountPoint = mountPointSchema.getLatestModule(moduleNameBehindMountPoint)
325 if (moduleBehindMountPoint === null) {
326 throw new ResponseException(BAD_REQUEST,
327 "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
330 return collectPathArguments(InstanceIdentifier.builder(), strings.subList(1, strings.size),
331 moduleBehindMountPoint, mount);
334 var Module module = null;
335 if (mountPoint === null) {
336 module = globalSchema.getLatestModule(moduleName)
337 if (module === null) {
338 throw new ResponseException(BAD_REQUEST,
339 "URI has bad format. \"" + moduleName + "\" module does not exist.")
342 module = mountPoint.schemaContext?.getLatestModule(moduleName)
343 if (module === null) {
344 throw new ResponseException(BAD_REQUEST,
345 "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.")
348 targetNode = parentNode.findInstanceDataChildByNameAndNamespace(nodeName, module.namespace)
349 if (targetNode === null) {
350 throw new ResponseException(BAD_REQUEST, "URI has bad format. Possible reasons:\n" +
351 "1. \"" + strings.head + "\" was not found in parent data node.\n" +
352 "2. \"" + strings.head + "\" is behind mount point. Then it should be in format \"/" + MOUNT + "/" + strings.head + "\".")
354 } else { // string without module name
355 val potentialSchemaNodes = parentNode.findInstanceDataChildrenByName(nodeName)
356 if (potentialSchemaNodes.size > 1) {
357 val StringBuilder namespacesOfPotentialModules = new StringBuilder;
358 for (potentialNodeSchema : potentialSchemaNodes) {
359 namespacesOfPotentialModules.append(" ").append(potentialNodeSchema.QName.namespace.toString).append("\n")
361 throw new ResponseException(BAD_REQUEST, "URI has bad format. Node \"" + nodeName + "\" is added as augment from more than one module. "
362 + "Therefore the node must have module name and it has to be in format \"moduleName:nodeName\"."
363 + "\nThe node is added as augment from modules with namespaces:\n" + namespacesOfPotentialModules)
365 targetNode = potentialSchemaNodes.head
366 if (targetNode === null) {
367 throw new ResponseException(BAD_REQUEST, "URI has bad format. \"" + nodeName + "\" was not found in parent data node.\n")
371 if (!(targetNode instanceof ListSchemaNode) && !(targetNode instanceof ContainerSchemaNode)) {
372 throw new ResponseException(BAD_REQUEST,"URI has bad format. Node \"" + strings.head + "\" must be Container or List yang type.")
374 // Number of consumed elements
376 if (targetNode instanceof ListSchemaNode) {
377 val listNode = targetNode as ListSchemaNode;
378 val keysSize = listNode.keyDefinition.size
380 // every key has to be filled
381 if ((strings.length - consumed) < keysSize) {
382 throw new ResponseException(BAD_REQUEST,"Missing key for list \"" + listNode.QName.localName + "\".")
384 val uriKeyValues = strings.subList(consumed, consumed + keysSize);
385 val keyValues = new HashMap<QName, Object>();
387 for (key : listNode.keyDefinition) {
388 val uriKeyValue = uriKeyValues.get(i);
390 // key value cannot be NULL
391 if (uriKeyValue.equals(NULL_VALUE)) {
392 throw new ResponseException(BAD_REQUEST, "URI has bad format. List \"" + listNode.QName.localName
393 + "\" cannot contain \"null\" value as a key."
396 keyValues.addKeyValue(listNode.getDataChildByName(key), uriKeyValue);
399 consumed = consumed + i;
400 builder.nodeWithKey(targetNode.QName, keyValues);
403 // Only one instance of node is allowed
404 builder.node(targetNode.QName);
406 if (targetNode instanceof DataNodeContainer) {
407 val remaining = strings.subList(consumed, strings.length);
408 val result = builder.collectPathArguments(remaining, targetNode as DataNodeContainer, mountPoint);
412 return new InstanceIdWithSchemaNode(builder.toInstance, targetNode, mountPoint)
415 def DataSchemaNode findInstanceDataChildByNameAndNamespace(DataNodeContainer container,
416 String name, URI namespace) {
417 Preconditions.checkNotNull(namespace)
418 val potentialSchemaNodes = container.findInstanceDataChildrenByName(name)
419 return potentialSchemaNodes.filter[n|n.QName.namespace == namespace].head
422 def List<DataSchemaNode> findInstanceDataChildrenByName(DataNodeContainer container, String name) {
423 Preconditions.checkNotNull(container)
424 Preconditions.checkNotNull(name)
425 val instantiatedDataNodeContainers = new ArrayList
426 instantiatedDataNodeContainers.collectInstanceDataNodeContainers(container, name)
427 return instantiatedDataNodeContainers
430 private def void collectInstanceDataNodeContainers(List<DataSchemaNode> potentialSchemaNodes, DataNodeContainer container,
432 val nodes = container.childNodes.filter[n|n.QName.localName == name]
433 for (potentialNode : nodes) {
434 if (potentialNode.isInstantiatedDataSchema) {
435 potentialSchemaNodes.add(potentialNode)
438 val allCases = container.childNodes.filter(ChoiceNode).map[cases].flatten
439 for (caze : allCases) {
440 collectInstanceDataNodeContainers(potentialSchemaNodes, caze, name)
444 def boolean isInstantiatedDataSchema(DataSchemaNode node) {
446 LeafSchemaNode: return true
447 LeafListSchemaNode: return true
448 ContainerSchemaNode: return true
449 ListSchemaNode: return true
450 default: return false
454 private def void addKeyValue(HashMap<QName, Object> map, DataSchemaNode node, String uriValue) {
455 checkNotNull(uriValue);
456 checkArgument(node instanceof LeafSchemaNode);
457 val urlDecoded = URLDecoder.decode(uriValue);
458 val typedef = (node as LeafSchemaNode).type;
460 var decoded = TypeDefinitionAwareCodec.from(typedef)?.deserialize(urlDecoded)
461 if(decoded === null) {
462 var baseType = RestUtil.resolveBaseTypeFrom(typedef)
463 if(baseType instanceof IdentityrefTypeDefinition) {
464 decoded = toQName(urlDecoded)
467 map.put(node.QName, decoded);
470 private static def String toModuleName(String str) {
472 if (str.contains(":")) {
473 val args = str.split(":");
474 if (args.size === 2) {
481 private def String toNodeName(String str) {
482 if (str.contains(":")) {
483 val args = str.split(":");
484 if (args.size === 2) {
491 private def QName toQName(String name) {
492 val module = name.toModuleName;
493 val node = name.toNodeName;
494 val namespace = FluentIterable.from(globalSchema.modules.sort[o1,o2 | o1.revision.compareTo(o2.revision)]) //
495 .transform[QName.create(namespace,revision,it.name)].findFirst[module == localName]
497 return QName.create(namespace,node);
500 def getRpcDefinition(String name) {
501 return qnameToRpc.get(name.toQName)
504 override onGlobalContextUpdated(SchemaContext context) {
505 this.globalSchema = context;
506 for (operation : context.operations) {
507 val qname = operation.QName;
508 qnameToRpc.put(qname, operation);