X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=blobdiff_plain;f=opendaylight%2Fmd-sal%2Fsal-rest-connector%2Fsrc%2Fmain%2Fjava%2Forg%2Fopendaylight%2Fcontroller%2Fsal%2Frestconf%2Fimpl%2FControllerContext.xtend;h=9b5c507811c18257f19abf7dacaf2452d849081e;hp=308975c8c599a595d24f928cd6dbe4f4c76b8b9b;hb=a812fd97808299ed90e388e83c469d5f3d8348c3;hpb=817e66a52d537af6127472fa6ca7b460ce30f938 diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.xtend b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.xtend index 308975c8c5..9b5c507811 100644 --- a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.xtend +++ b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.xtend @@ -1,17 +1,27 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ package org.opendaylight.controller.sal.restconf.impl +import com.google.common.base.Preconditions +import com.google.common.base.Splitter import com.google.common.collect.BiMap import com.google.common.collect.FluentIterable import com.google.common.collect.HashBiMap +import com.google.common.collect.Lists import java.net.URI import java.net.URLDecoder import java.net.URLEncoder +import java.util.ArrayList import java.util.HashMap import java.util.List import java.util.Map import java.util.concurrent.ConcurrentHashMap -import javax.ws.rs.core.Response -import org.opendaylight.controller.sal.core.api.model.SchemaServiceListener +import org.opendaylight.controller.sal.core.api.mount.MountInstance import org.opendaylight.controller.sal.core.api.mount.MountService import org.opendaylight.controller.sal.rest.impl.RestUtil import org.opendaylight.controller.sal.rest.impl.RestconfProvider @@ -33,16 +43,22 @@ import org.opendaylight.yangtools.yang.model.api.ListSchemaNode import org.opendaylight.yangtools.yang.model.api.Module import org.opendaylight.yangtools.yang.model.api.RpcDefinition import org.opendaylight.yangtools.yang.model.api.SchemaContext +import org.opendaylight.yangtools.yang.model.api.SchemaServiceListener import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition import org.slf4j.LoggerFactory import static com.google.common.base.Preconditions.* -import java.util.ArrayList +import static javax.ws.rs.core.Response.Status.* class ControllerContext implements SchemaServiceListener { val static LOG = LoggerFactory.getLogger(ControllerContext) val static ControllerContext INSTANCE = new ControllerContext val static NULL_VALUE = "null" + val static MOUNT_MODULE = "yang-ext" + val static MOUNT_NODE = "mount" + public val static MOUNT = "yang-ext:mount" + val static URI_ENCODING_CHAR_SET = "ISO-8859-1" + val static URI_SLASH_PLACEHOLDER = "%2F"; @Property var SchemaContext globalSchema; @@ -66,7 +82,7 @@ class ControllerContext implements SchemaServiceListener { private def void checkPreconditions() { if (globalSchema === null) { - throw new ResponseException(Response.Status.SERVICE_UNAVAILABLE, RestconfProvider::NOT_INITALIZED_MSG) + throw new ResponseException(SERVICE_UNAVAILABLE, RestconfProvider::NOT_INITALIZED_MSG) } } @@ -74,31 +90,57 @@ class ControllerContext implements SchemaServiceListener { onGlobalContextUpdated(schemas) } - public def InstanceIdWithSchemaNode toInstanceIdentifier(String restconfInstance) { + def InstanceIdWithSchemaNode toInstanceIdentifier(String restconfInstance) { + return restconfInstance.toIdentifier(false) + } + + def InstanceIdWithSchemaNode toMountPointIdentifier(String restconfInstance) { + return restconfInstance.toIdentifier(true) + } + + private def InstanceIdWithSchemaNode toIdentifier(String restconfInstance, boolean toMountPointIdentifier) { checkPreconditions - val ret = InstanceIdentifier.builder(); - val pathArgs = restconfInstance.split("/"); + val encodedPathArgs = Lists.newArrayList(Splitter.on("/").split(restconfInstance)) + val pathArgs = urlPathArgsDecode(encodedPathArgs) + pathArgs.omitFirstAndLastEmptyString if (pathArgs.empty) { return null; } - if (pathArgs.head.empty) { - pathArgs.remove(0) + val startModule = pathArgs.head.toModuleName(); + if (startModule === null) { + throw new ResponseException(BAD_REQUEST, "First node in URI has to be in format \"moduleName:nodeName\"") } - val mountPoints = new ArrayList - val schemaNode = ret.collectPathArguments(pathArgs, globalSchema.findModule(pathArgs.head), mountPoints); - if (schemaNode === null) { - return null + var InstanceIdWithSchemaNode iiWithSchemaNode = null; + if (toMountPointIdentifier) { + iiWithSchemaNode = collectPathArguments(InstanceIdentifier.builder(), pathArgs, + globalSchema.getLatestModule(startModule), null, true); + } else { + iiWithSchemaNode = collectPathArguments(InstanceIdentifier.builder(), pathArgs, + globalSchema.getLatestModule(startModule), null, false); + } + if (iiWithSchemaNode === null) { + throw new ResponseException(BAD_REQUEST, "URI has bad format") } - return new InstanceIdWithSchemaNode(ret.toInstance, schemaNode, mountPoints.last) + return iiWithSchemaNode } - private def findModule(SchemaContext context,String argument) { - checkNotNull(argument); - val startModule = argument.toModuleName(); - return context.getLatestModule(startModule) + private def omitFirstAndLastEmptyString(List list) { + if (list.empty) { + return list; + } + if (list.head.empty) { + list.remove(0) + } + if (list.empty) { + return list; + } + if (list.last.empty) { + list.remove(list.indexOf(list.last)) + } + return list; } - - private def getLatestModule(SchemaContext schema,String moduleName) { + + private def getLatestModule(SchemaContext schema, String moduleName) { checkArgument(schema !== null); checkArgument(moduleName !== null && !moduleName.empty) val modules = schema.modules.filter[m|m.name == moduleName] @@ -121,90 +163,148 @@ class ControllerContext implements SchemaServiceListener { return globalSchema.getLatestModule(moduleName) } - def findModuleByName(String moduleName, InstanceIdentifier partialPath) { - checkArgument(moduleName !== null && !moduleName.empty && partialPath !== null && !partialPath.path.empty) - val mountPointSchema = mountService?.getMountPoint(partialPath)?.schemaContext; + def findModuleByName(MountInstance mountPoint, String moduleName) { + checkArgument(moduleName !== null && mountPoint !== null) + val mountPointSchema = mountPoint.schemaContext; return mountPointSchema?.getLatestModule(moduleName); } def findModuleByNamespace(URI namespace) { checkPreconditions + checkArgument(namespace !== null) val moduleSchemas = globalSchema.findModuleByNamespace(namespace) return moduleSchemas?.filterLatestModule } - def findModuleByNamespace(URI namespace, InstanceIdentifier partialPath) { - checkArgument(namespace !== null && !namespace.toString.empty && partialPath !== null && !partialPath.path.empty) - val mountPointSchema = mountService?.getMountPoint(partialPath)?.schemaContext; + def findModuleByNamespace(MountInstance mountPoint, URI namespace) { + checkArgument(namespace !== null && mountPoint !== null) + val mountPointSchema = mountPoint.schemaContext; val moduleSchemas = mountPointSchema?.findModuleByNamespace(namespace) return moduleSchemas?.filterLatestModule } + def findModuleByNameAndRevision(QName module) { + checkPreconditions + checkArgument(module !== null && module.localName !== null && module.revision !== null) + return globalSchema.findModuleByName(module.localName, module.revision) + } + + def findModuleByNameAndRevision(MountInstance mountPoint, QName module) { + checkPreconditions + checkArgument(module !== null && module.localName !== null && module.revision !== null && mountPoint !== null) + return mountPoint.schemaContext?.findModuleByName(module.localName, module.revision) + } + + def getDataNodeContainerFor(InstanceIdentifier path) { + checkPreconditions + val elements = path.path; + val startQName = elements.head.nodeType; + val initialModule = globalSchema.findModuleByNamespaceAndRevision(startQName.namespace, startQName.revision) + var node = initialModule as DataNodeContainer; + for (element : elements) { + val potentialNode = node.childByQName(element.nodeType); + if (potentialNode === null || !potentialNode.listOrContainer) { + return null + } + node = potentialNode as DataNodeContainer + } + return node + } + def String toFullRestconfIdentifier(InstanceIdentifier path) { checkPreconditions val elements = path.path; val ret = new StringBuilder(); - val startQName = elements.get(0).nodeType; + val startQName = elements.head.nodeType; val initialModule = globalSchema.findModuleByNamespaceAndRevision(startQName.namespace, startQName.revision) - var node = initialModule as DataSchemaNode; + var node = initialModule as DataNodeContainer; for (element : elements) { - node = node.childByQName(element.nodeType); - ret.append(element.toRestconfIdentifier(node)); + val potentialNode = node.childByQName(element.nodeType); + if (!potentialNode.listOrContainer) { + return null + } + node = potentialNode as DataNodeContainer + ret.append(element.convertToRestconfIdentifier(node)); } return ret.toString } - private def dispatch CharSequence toRestconfIdentifier(NodeIdentifier argument, DataSchemaNode node) { + private def dispatch CharSequence convertToRestconfIdentifier(NodeIdentifier argument, ContainerSchemaNode node) { '''/«argument.nodeType.toRestconfIdentifier()»''' } - private def dispatch CharSequence toRestconfIdentifier(NodeIdentifierWithPredicates argument, ListSchemaNode node) { + private def dispatch CharSequence convertToRestconfIdentifier(NodeIdentifierWithPredicates argument, ListSchemaNode node) { val nodeIdentifier = argument.nodeType.toRestconfIdentifier(); val keyValues = argument.keyValues; return '''/«nodeIdentifier»/«FOR key : node.keyDefinition SEPARATOR "/"»«keyValues.get(key).toUriString»«ENDFOR»''' } - private def dispatch CharSequence toRestconfIdentifier(PathArgument argument, DataSchemaNode node) { + private def dispatch CharSequence convertToRestconfIdentifier(PathArgument argument, DataNodeContainer node) { throw new IllegalArgumentException("Conversion of generic path argument is not supported"); } def findModuleNameByNamespace(URI namespace) { checkPreconditions - var module = uriToModuleName.get(namespace) - if (module === null) { - val moduleSchemas = globalSchema.findModuleByNamespace(namespace); - if(moduleSchemas === null) return null - var latestModule = moduleSchemas.filterLatestModule - if(latestModule === null) return null - uriToModuleName.put(namespace, latestModule.name) - module = latestModule.name; + var moduleName = uriToModuleName.get(namespace) + if (moduleName === null) { + val module = findModuleByNamespace(namespace) + if (module === null) return null + moduleName = module.name + uriToModuleName.put(namespace, moduleName) } - return module + return moduleName + } + + def findModuleNameByNamespace(MountInstance mountPoint, URI namespace) { + val module = mountPoint.findModuleByNamespace(namespace); + return module?.name } - def findNamespaceByModuleName(String module) { - var namespace = moduleNameToUri.get(module) + def findNamespaceByModuleName(String moduleName) { + var namespace = moduleNameToUri.get(moduleName) if (namespace === null) { - var latestModule = globalSchema.getLatestModule(module) - if(latestModule === null) return null - namespace = latestModule.namespace - uriToModuleName.put(namespace, latestModule.name) + var module = findModuleByName(moduleName) + if(module === null) return null + namespace = module.namespace + uriToModuleName.put(namespace, moduleName) } return namespace } + + def findNamespaceByModuleName(MountInstance mountPoint, String moduleName) { + val module = mountPoint.findModuleByName(moduleName) + return module?.namespace + } + + def getAllModules(MountInstance mountPoint) { + checkPreconditions + return mountPoint?.schemaContext?.modules + } + + def getAllModules() { + checkPreconditions + return globalSchema.modules + } def CharSequence toRestconfIdentifier(QName qname) { checkPreconditions var module = uriToModuleName.get(qname.namespace) if (module === null) { val moduleSchema = globalSchema.findModuleByNamespaceAndRevision(qname.namespace, qname.revision); - if(moduleSchema === null) throw new IllegalArgumentException() + if(moduleSchema === null) return null uriToModuleName.put(qname.namespace, moduleSchema.name) module = moduleSchema.name; } return '''«module»:«qname.localName»'''; } + def CharSequence toRestconfIdentifier(MountInstance mountPoint, QName qname) { + val moduleSchema = mountPoint?.schemaContext.findModuleByNamespaceAndRevision(qname.namespace, qname.revision); + if(moduleSchema === null) return null + val module = moduleSchema.name; + return '''«module»:«qname.localName»'''; + } + private static dispatch def DataSchemaNode childByQName(ChoiceNode container, QName name) { for (caze : container.cases) { val ret = caze.childByQName(name) @@ -228,6 +328,10 @@ class ControllerContext implements SchemaServiceListener { return container.dataNodeChildByQName(name); } + private static dispatch def DataSchemaNode childByQName(Module container, QName name) { + return container.dataNodeChildByQName(name); + } + private static dispatch def DataSchemaNode childByQName(DataSchemaNode container, QName name) { return null; } @@ -252,41 +356,111 @@ class ControllerContext implements SchemaServiceListener { private def toUriString(Object object) { if(object === null) return ""; - return URLEncoder.encode(object.toString) + return URLEncoder.encode(object.toString,URI_ENCODING_CHAR_SET) } - - private def DataSchemaNode collectPathArguments(InstanceIdentifierBuilder builder, List strings, - DataNodeContainer parentNode, List mountPoints) { + + private def InstanceIdWithSchemaNode collectPathArguments(InstanceIdentifierBuilder builder, List strings, + DataNodeContainer parentNode, MountInstance mountPoint, boolean returnJustMountPoint) { checkNotNull(strings) if (parentNode === null) { return null; } if (strings.empty) { - return parentNode as DataSchemaNode; - } - val nodeRef = strings.head; - - val nodeName = nodeRef.toNodeName; - var targetNode = parentNode.findInstanceDataChild(nodeName); - if (targetNode instanceof ChoiceNode) { - return null + return new InstanceIdWithSchemaNode(builder.toInstance, parentNode as DataSchemaNode, mountPoint) } - if (targetNode === null) { - // Node is possibly in other mount point - val partialPath = builder.toInstance; - val mountPointSchema = mountService?.getMountPoint(partialPath)?.schemaContext; - if(mountPointSchema !== null) { - val module = mountPointSchema.findModule(strings.head) - if (module !== null) { - mountPoints.add(partialPath) + val nodeName = strings.head.toNodeName + val moduleName = strings.head.toModuleName + var DataSchemaNode targetNode = null + if (!moduleName.nullOrEmpty) { + // if it is mount point + if (moduleName == MOUNT_MODULE && nodeName == MOUNT_NODE) { + if (mountPoint !== null) { + throw new ResponseException(BAD_REQUEST, "Restconf supports just one mount point in URI.") + } + + if (mountService === null) { + throw new ResponseException(SERVICE_UNAVAILABLE, "MountService was not found. " + + "Finding behind mount points does not work." + ) + } + + val partialPath = builder.toInstance; + val mount = mountService.getMountPoint(partialPath) + if (mount === null) { + LOG.debug("Instance identifier to missing mount point: {}", partialPath) + throw new ResponseException(BAD_REQUEST, "Mount point does not exist.") + } + + val mountPointSchema = mount.schemaContext; + if (mountPointSchema === null) { + throw new ResponseException(BAD_REQUEST, "Mount point does not contain any schema with modules.") + } + + if (returnJustMountPoint) { + return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount) } - return builder.collectPathArguments(strings, module, mountPoints); + + if (strings.size == 1) { // any data node is not behind mount point + return new InstanceIdWithSchemaNode(InstanceIdentifier.builder().toInstance, mountPointSchema, mount) + } + + val moduleNameBehindMountPoint = strings.get(1).toModuleName() + if (moduleNameBehindMountPoint === null) { + throw new ResponseException(BAD_REQUEST, + "First node after mount point in URI has to be in format \"moduleName:nodeName\"") + } + + val moduleBehindMountPoint = mountPointSchema.getLatestModule(moduleNameBehindMountPoint) + if (moduleBehindMountPoint === null) { + throw new ResponseException(BAD_REQUEST, + "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.") + } + + return collectPathArguments(InstanceIdentifier.builder(), strings.subList(1, strings.size), + moduleBehindMountPoint, mount, returnJustMountPoint); + } + + var Module module = null; + if (mountPoint === null) { + module = globalSchema.getLatestModule(moduleName) + if (module === null) { + throw new ResponseException(BAD_REQUEST, + "URI has bad format. \"" + moduleName + "\" module does not exist.") + } + } else { + module = mountPoint.schemaContext?.getLatestModule(moduleName) + if (module === null) { + throw new ResponseException(BAD_REQUEST, + "URI has bad format. \"" + moduleName + "\" module does not exist in mount point.") + } + } + targetNode = parentNode.findInstanceDataChildByNameAndNamespace(nodeName, module.namespace) + if (targetNode === null) { + throw new ResponseException(BAD_REQUEST, "URI has bad format. Possible reasons:\n" + + "1. \"" + strings.head + "\" was not found in parent data node.\n" + + "2. \"" + strings.head + "\" is behind mount point. Then it should be in format \"/" + MOUNT + "/" + strings.head + "\".") + } + } else { // string without module name + val potentialSchemaNodes = parentNode.findInstanceDataChildrenByName(nodeName) + if (potentialSchemaNodes.size > 1) { + val StringBuilder namespacesOfPotentialModules = new StringBuilder; + for (potentialNodeSchema : potentialSchemaNodes) { + namespacesOfPotentialModules.append(" ").append(potentialNodeSchema.QName.namespace.toString).append("\n") + } + throw new ResponseException(BAD_REQUEST, "URI has bad format. Node \"" + nodeName + "\" is added as augment from more than one module. " + + "Therefore the node must have module name and it has to be in format \"moduleName:nodeName\"." + + "\nThe node is added as augment from modules with namespaces:\n" + namespacesOfPotentialModules) + } + targetNode = potentialSchemaNodes.head + if (targetNode === null) { + throw new ResponseException(BAD_REQUEST, "URI has bad format. \"" + nodeName + "\" was not found in parent data node.\n") } - return null } - + if (!targetNode.isListOrContainer) { + throw new ResponseException(BAD_REQUEST,"URI has bad format. Node \"" + strings.head + "\" must be Container or List yang type.") + } // Number of consumed elements var consumed = 1; if (targetNode instanceof ListSchemaNode) { @@ -295,7 +469,7 @@ class ControllerContext implements SchemaServiceListener { // every key has to be filled if ((strings.length - consumed) < keysSize) { - return null; + throw new ResponseException(BAD_REQUEST,"Missing key for list \"" + listNode.QName.localName + "\".") } val uriKeyValues = strings.subList(consumed, consumed + keysSize); val keyValues = new HashMap(); @@ -305,7 +479,9 @@ class ControllerContext implements SchemaServiceListener { // key value cannot be NULL if (uriKeyValue.equals(NULL_VALUE)) { - return null + throw new ResponseException(BAD_REQUEST, "URI has bad format. List \"" + listNode.QName.localName + + "\" cannot contain \"null\" value as a key." + ) } keyValues.addKeyValue(listNode.getDataChildByName(key), uriKeyValue); i = i + 1; @@ -319,30 +495,43 @@ class ControllerContext implements SchemaServiceListener { } if (targetNode instanceof DataNodeContainer) { val remaining = strings.subList(consumed, strings.length); - val result = builder.collectPathArguments(remaining, targetNode as DataNodeContainer, mountPoints); + val result = builder.collectPathArguments(remaining, targetNode as DataNodeContainer, mountPoint, returnJustMountPoint); return result } - return targetNode + return new InstanceIdWithSchemaNode(builder.toInstance, targetNode, mountPoint) + } + + def DataSchemaNode findInstanceDataChildByNameAndNamespace(DataNodeContainer container, + String name, URI namespace) { + Preconditions.checkNotNull(namespace) + val potentialSchemaNodes = container.findInstanceDataChildrenByName(name) + return potentialSchemaNodes.filter[n|n.QName.namespace == namespace].head } - static def DataSchemaNode findInstanceDataChild(DataNodeContainer container, String name) { - // FIXME: Add namespace comparison - var potentialNode = container.getDataChildByName(name); - if(potentialNode.instantiatedDataSchema) { - return potentialNode; + def List findInstanceDataChildrenByName(DataNodeContainer container, String name) { + Preconditions.checkNotNull(container) + Preconditions.checkNotNull(name) + val instantiatedDataNodeContainers = new ArrayList + instantiatedDataNodeContainers.collectInstanceDataNodeContainers(container, name) + return instantiatedDataNodeContainers + } + + private def void collectInstanceDataNodeContainers(List potentialSchemaNodes, DataNodeContainer container, + String name) { + val nodes = container.childNodes.filter[n|n.QName.localName == name] + for (potentialNode : nodes) { + if (potentialNode.isInstantiatedDataSchema) { + potentialSchemaNodes.add(potentialNode) + } } val allCases = container.childNodes.filter(ChoiceNode).map[cases].flatten for (caze : allCases) { - potentialNode = caze.findInstanceDataChild(name); - if(potentialNode !== null) { - return potentialNode; - } + collectInstanceDataNodeContainers(potentialSchemaNodes, caze, name) } - return null; } - static def boolean isInstantiatedDataSchema(DataSchemaNode node) { + def boolean isInstantiatedDataSchema(DataSchemaNode node) { switch node { LeafSchemaNode: return true LeafListSchemaNode: return true @@ -351,7 +540,7 @@ class ControllerContext implements SchemaServiceListener { default: return false } } - + private def void addKeyValue(HashMap map, DataSchemaNode node, String uriValue) { checkNotNull(uriValue); checkArgument(node instanceof LeafSchemaNode); @@ -372,42 +561,71 @@ class ControllerContext implements SchemaServiceListener { checkNotNull(str) if (str.contains(":")) { val args = str.split(":"); - checkArgument(args.size === 2); - return args.get(0); - } else { - return null; + if (args.size === 2) { + return args.get(0); + } } + return null; } private def String toNodeName(String str) { if (str.contains(":")) { val args = str.split(":"); - checkArgument(args.size === 2); - return args.get(1); - } else { - return str; + if (args.size === 2) { + return args.get(1); + } } + return str; } private def QName toQName(String name) { val module = name.toModuleName; val node = name.toNodeName; - val namespace = FluentIterable.from(globalSchema.modules.sort[o1,o2 | o1.revision.compareTo(o2.revision)]) // + val namespace = FluentIterable.from(globalSchema.modules.sort[o1,o2 | o1.revision.compareTo(o2.revision)]) .transform[QName.create(namespace,revision,it.name)].findFirst[module == localName] - ; - return QName.create(namespace,node); + if (namespace === null) { + return null + } + return QName.create(namespace, node); + } + + private def boolean isListOrContainer(DataSchemaNode node) { + return ((node instanceof ListSchemaNode) || (node instanceof ContainerSchemaNode)) } def getRpcDefinition(String name) { - return qnameToRpc.get(name.toQName) + val validName = name.toQName + if (validName === null) { + return null + } + return qnameToRpc.get(validName) } override onGlobalContextUpdated(SchemaContext context) { - this.globalSchema = context; - for (operation : context.operations) { - val qname = operation.QName; - qnameToRpc.put(qname, operation); + if (context !== null) { + qnameToRpc.clear + this.globalSchema = context; + for (operation : context.operations) { + val qname = operation.QName; + qnameToRpc.put(qname, operation); + } } } + + def urlPathArgsDecode(List strings) { + val List decodedPathArgs = new ArrayList(); + for (pathArg : strings) { + decodedPathArgs.add(URLDecoder.decode(pathArg, URI_ENCODING_CHAR_SET)) + } + return decodedPathArgs + } + + def urlPathArgDecode(String pathArg) { + if (pathArg !== null) { + return URLDecoder.decode(pathArg, URI_ENCODING_CHAR_SET) + } + return null + } + }