2 * Copyright (c) 2014 Brocade Communications 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.netconf.sal.rest.doc.impl;
10 import static org.opendaylight.netconf.sal.rest.doc.impl.ApiDocServiceImpl.DEFAULT_PAGESIZE;
11 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.TOP;
12 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildDelete;
13 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildGet;
14 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPost;
15 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPostOperation;
16 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPut;
17 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.getTypeParentNode;
18 import static org.opendaylight.netconf.sal.rest.doc.util.JsonUtil.addFields;
19 import static org.opendaylight.netconf.sal.rest.doc.util.RestDocgenUtil.resolvePathArgumentsName;
21 import com.fasterxml.jackson.databind.JsonNode;
22 import com.fasterxml.jackson.databind.ObjectMapper;
23 import com.fasterxml.jackson.databind.SerializationFeature;
24 import com.fasterxml.jackson.databind.node.ArrayNode;
25 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
26 import com.fasterxml.jackson.databind.node.ObjectNode;
27 import com.google.common.base.Preconditions;
28 import com.google.common.collect.ImmutableList;
29 import com.google.common.collect.Range;
30 import java.io.IOException;
32 import java.time.format.DateTimeParseException;
33 import java.util.ArrayList;
34 import java.util.Arrays;
35 import java.util.Collection;
36 import java.util.Collections;
37 import java.util.HashMap;
38 import java.util.Iterator;
39 import java.util.List;
41 import java.util.Map.Entry;
42 import java.util.Optional;
44 import java.util.SortedSet;
45 import java.util.TreeSet;
46 import javax.ws.rs.core.UriInfo;
47 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
48 import org.opendaylight.netconf.sal.rest.doc.impl.ApiDocServiceImpl.OAversion;
49 import org.opendaylight.netconf.sal.rest.doc.swagger.CommonApiObject;
50 import org.opendaylight.netconf.sal.rest.doc.swagger.Components;
51 import org.opendaylight.netconf.sal.rest.doc.swagger.Info;
52 import org.opendaylight.netconf.sal.rest.doc.swagger.OpenApiObject;
53 import org.opendaylight.netconf.sal.rest.doc.swagger.Resource;
54 import org.opendaylight.netconf.sal.rest.doc.swagger.ResourceList;
55 import org.opendaylight.netconf.sal.rest.doc.swagger.Server;
56 import org.opendaylight.netconf.sal.rest.doc.swagger.SwaggerObject;
57 import org.opendaylight.netconf.sal.rest.doc.util.JsonUtil;
58 import org.opendaylight.yangtools.yang.common.QName;
59 import org.opendaylight.yangtools.yang.common.Revision;
60 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
61 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
62 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
63 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
64 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
65 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
66 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
67 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
68 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
69 import org.opendaylight.yangtools.yang.model.api.Module;
70 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
71 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
75 public abstract class BaseYangSwaggerGenerator {
77 private static final Logger LOG = LoggerFactory.getLogger(BaseYangSwaggerGenerator.class);
79 private static final String API_VERSION = "1.0.0";
80 private static final String SWAGGER_VERSION = "2.0";
81 private static final String OPEN_API_VERSION = "3.0.3";
83 private final DefinitionGenerator jsonConverter = new DefinitionGenerator();
85 private final ObjectMapper mapper = new ObjectMapper();
86 private final DOMSchemaService schemaService;
88 public static final String BASE_PATH = "/";
89 public static final String MODULE_NAME_SUFFIX = "_module";
91 protected BaseYangSwaggerGenerator(final Optional<DOMSchemaService> schemaService) {
92 this.schemaService = schemaService.orElse(null);
93 mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
96 public DOMSchemaService getSchemaService() {
100 public ResourceList getResourceListing(final UriInfo uriInfo, final EffectiveModelContext schemaContext,
101 final String context, final OAversion oaversion) {
102 return getResourceListing(uriInfo, schemaContext, context, 0, true, oaversion);
106 * Return list of modules converted to swagger compliant resource list.
108 public ResourceList getResourceListing(final UriInfo uriInfo, final EffectiveModelContext schemaContext,
109 final String context, final int pageNum, final boolean all,
110 final OAversion oaversion) {
111 final ResourceList resourceList = createResourceList();
113 final Set<Module> modules = getSortedModules(schemaContext);
115 final List<Resource> resources = new ArrayList<>(DEFAULT_PAGESIZE);
117 LOG.info("Modules found [{}]", modules.size());
118 final int start = DEFAULT_PAGESIZE * pageNum;
119 final int end = start + DEFAULT_PAGESIZE;
121 for (final Module module : modules) {
122 final String revisionString = module.getQNameModule().getRevision().map(Revision::toString).orElse(null);
124 LOG.debug("Working on [{},{}]...", module.getName(), revisionString);
125 final SwaggerObject doc = getApiDeclaration(module.getName(), revisionString, uriInfo, schemaContext,
129 if (count >= start && count < end || all) {
130 final Resource resource = new Resource();
131 resource.setPath(generatePath(uriInfo, module.getName(), revisionString));
132 resources.add(resource);
135 if (count >= end && !all) {
139 LOG.warn("Could not generate doc for {},{}", module.getName(), revisionString);
143 resourceList.setApis(resources);
148 public SwaggerObject getAllModulesDoc(final UriInfo uriInfo, final DefinitionNames definitionNames,
149 final OAversion oaversion) {
150 final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
151 Preconditions.checkState(schemaContext != null);
152 return getAllModulesDoc(uriInfo, Optional.empty(), schemaContext, Optional.empty(), "", definitionNames,
156 public SwaggerObject getAllModulesDoc(final UriInfo uriInfo, final Optional<Range<Integer>> range,
157 final EffectiveModelContext schemaContext, final Optional<String> deviceName,
158 final String context, final DefinitionNames definitionNames,
159 final OAversion oaversion) {
160 final String schema = createSchemaFromUriInfo(uriInfo);
161 final String host = createHostFromUriInfo(uriInfo);
162 String name = "Controller";
163 if (deviceName.isPresent()) {
164 name = deviceName.get();
167 final String title = name + " modules of RESTCONF";
168 final SwaggerObject doc = createSwaggerObject(schema, host, BASE_PATH, title);
169 doc.setDefinitions(JsonNodeFactory.instance.objectNode());
170 doc.setPaths(JsonNodeFactory.instance.objectNode());
172 fillDoc(doc, range, schemaContext, context, deviceName, oaversion, definitionNames);
177 public void fillDoc(final SwaggerObject doc, final Optional<Range<Integer>> range,
178 final EffectiveModelContext schemaContext, final String context,
179 final Optional<String> deviceName, final OAversion oaversion,
180 final DefinitionNames definitionNames) {
181 final SortedSet<Module> modules = getSortedModules(schemaContext);
182 final Set<Module> filteredModules;
183 if (range.isPresent()) {
184 filteredModules = filterByRange(modules, range.get());
186 filteredModules = modules;
189 for (final Module module : filteredModules) {
190 final String revisionString = module.getQNameModule().getRevision().map(Revision::toString).orElse(null);
192 LOG.debug("Working on [{},{}]...", module.getName(), revisionString);
194 getSwaggerDocSpec(module, context, deviceName, schemaContext, oaversion, definitionNames, doc, false);
198 private static Set<Module> filterByRange(final SortedSet<Module> modules, final Range<Integer> range) {
199 final int begin = range.lowerEndpoint();
200 final int end = range.upperEndpoint();
202 Module firstModule = null;
204 final Iterator<Module> iterator = modules.iterator();
206 while (iterator.hasNext() && counter < end) {
207 final Module module = iterator.next();
208 if (containsListOrContainer(module.getChildNodes()) || !module.getRpcs().isEmpty()) {
209 if (counter == begin) {
210 firstModule = module;
216 if (iterator.hasNext()) {
217 return modules.subSet(firstModule, iterator.next());
219 return modules.tailSet(firstModule);
223 public ResourceList createResourceList() {
224 final ResourceList resourceList = new ResourceList();
225 resourceList.setApiVersion(API_VERSION);
226 resourceList.setSwaggerVersion(SWAGGER_VERSION);
230 public String generatePath(final UriInfo uriInfo, final String name, final String revision) {
231 final URI uri = uriInfo.getRequestUriBuilder().replaceQuery("").path(generateCacheKey(name, revision)).build();
232 return uri.toASCIIString();
235 public CommonApiObject getApiDeclaration(final String module, final String revision, final UriInfo uriInfo,
236 final OAversion oaversion) {
237 final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
238 Preconditions.checkState(schemaContext != null);
239 final SwaggerObject doc = getApiDeclaration(module, revision, uriInfo, schemaContext, "", oaversion);
240 return getAppropriateDoc(doc, oaversion);
243 public SwaggerObject getApiDeclaration(final String moduleName, final String revision, final UriInfo uriInfo,
244 final EffectiveModelContext schemaContext, final String context,
245 final OAversion oaversion) {
246 final Optional<Revision> rev;
249 rev = Revision.ofNullable(revision);
250 } catch (final DateTimeParseException e) {
251 throw new IllegalArgumentException(e);
254 final Module module = schemaContext.findModule(moduleName, rev).orElse(null);
255 Preconditions.checkArgument(module != null,
256 "Could not find module by name,revision: " + moduleName + "," + revision);
258 return getApiDeclaration(module, uriInfo, context, schemaContext, oaversion);
261 public SwaggerObject getApiDeclaration(final Module module, final UriInfo uriInfo, final String context,
262 final EffectiveModelContext schemaContext, final OAversion oaversion) {
263 final String schema = createSchemaFromUriInfo(uriInfo);
264 final String host = createHostFromUriInfo(uriInfo);
266 return getSwaggerDocSpec(module, schema, host, BASE_PATH, context, schemaContext, oaversion);
269 public String createHostFromUriInfo(final UriInfo uriInfo) {
270 String portPart = "";
271 final int port = uriInfo.getBaseUri().getPort();
273 portPart = ":" + port;
275 return uriInfo.getBaseUri().getHost() + portPart;
278 public String createSchemaFromUriInfo(final UriInfo uriInfo) {
279 return uriInfo.getBaseUri().getScheme();
282 public SwaggerObject getSwaggerDocSpec(final Module module, final String schema, final String host,
283 final String basePath, final String context,
284 final EffectiveModelContext schemaContext, final OAversion oaversion) {
285 final SwaggerObject doc = createSwaggerObject(schema, host, basePath, module.getName());
286 final DefinitionNames definitionNames = new DefinitionNames();
287 return getSwaggerDocSpec(module, context, Optional.empty(), schemaContext, oaversion, definitionNames, doc,
292 public SwaggerObject getSwaggerDocSpec(final Module module, final String context, final Optional<String> deviceName,
293 final EffectiveModelContext schemaContext, final OAversion oaversion,
294 final DefinitionNames definitionNames, final SwaggerObject doc,
295 final boolean isForSingleModule) {
296 final ObjectNode definitions;
299 if (isForSingleModule) {
300 definitions = jsonConverter.convertToJsonSchema(module, schemaContext, definitionNames, oaversion,
302 doc.setDefinitions(definitions);
304 definitions = jsonConverter.convertToJsonSchema(module, schemaContext, definitionNames, oaversion,
306 addFields(doc.getDefinitions(), definitions.fields());
308 if (LOG.isDebugEnabled()) {
309 LOG.debug("Document: {}", mapper.writeValueAsString(doc));
311 } catch (final IOException e) {
312 LOG.error("Exception occured in DefinitionGenerator", e);
315 final ObjectNode paths = JsonNodeFactory.instance.objectNode();
316 final String moduleName = module.getName();
318 boolean hasAddRootPostLink = false;
320 final Collection<? extends DataSchemaNode> dataSchemaNodes = module.getChildNodes();
321 LOG.debug("child nodes size [{}]", dataSchemaNodes.size());
322 for (final DataSchemaNode node : dataSchemaNodes) {
323 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
324 LOG.debug("Is Configuration node [{}] [{}]", node.isConfiguration(), node.getQName().getLocalName());
326 final String localName = module.getName() + ":" + node.getQName().getLocalName();
327 ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
330 if (node.isConfiguration()) { // This node's config statement is
332 resourcePath = getResourcePath("config", context);
335 * When there are two or more top container or list nodes
336 * whose config statement is true in module, make sure that
337 * only one root post link is added for this module.
339 if (isForSingleModule && !hasAddRootPostLink) {
340 LOG.debug("Has added root post link for module {}", module.getName());
341 addRootPostLink(module, deviceName, pathParams, resourcePath, paths, oaversion);
343 hasAddRootPostLink = true;
346 final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, oaversion, localName);
347 addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, true, module.getName(),
348 definitionNames, oaversion, resolvedPath);
350 pathParams = JsonNodeFactory.instance.arrayNode();
351 resourcePath = getResourcePath("operational", context);
353 if (!node.isConfiguration()) {
354 final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, oaversion, localName);
355 addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, false, moduleName,
356 definitionNames, oaversion, resolvedPath);
361 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
362 final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
363 + rpcDefinition.getQName().getLocalName();
364 addOperations(rpcDefinition, moduleName, deviceName, paths, module.getName(), definitionNames, oaversion,
368 LOG.debug("Number of Paths found [{}]", paths.size());
370 if (isForSingleModule) {
373 addFields(doc.getPaths(), paths.fields());
379 private static void addRootPostLink(final Module module, final Optional<String> deviceName,
380 final ArrayNode pathParams, final String resourcePath, final ObjectNode paths, final OAversion oaversion) {
381 if (containsListOrContainer(module.getChildNodes())) {
382 final ObjectNode post = JsonNodeFactory.instance.objectNode();
383 final String moduleName = module.getName();
384 final String name = moduleName + MODULE_NAME_SUFFIX;
385 post.set("post", buildPost("", name, "", moduleName, deviceName,
386 module.getDescription().orElse(""), pathParams, oaversion));
387 paths.set(resourcePath, post);
391 public SwaggerObject createSwaggerObject(final String schema, final String host, final String basePath,
392 final String title) {
393 final SwaggerObject doc = new SwaggerObject();
394 doc.setSwagger(SWAGGER_VERSION);
395 final Info info = new Info();
396 info.setTitle(title);
397 info.setVersion(API_VERSION);
399 doc.setSchemes(ImmutableList.of(schema));
401 doc.setBasePath(basePath);
402 doc.setProduces(Arrays.asList("application/xml", "application/json"));
406 public static CommonApiObject getAppropriateDoc(final SwaggerObject swaggerObject, final OAversion oaversion) {
407 if (oaversion.equals(OAversion.V3_0)) {
408 return convertToOpenApi(swaggerObject);
410 return swaggerObject;
413 private static OpenApiObject convertToOpenApi(final SwaggerObject swaggerObject) {
414 final OpenApiObject doc = new OpenApiObject();
415 doc.setOpenapi(OPEN_API_VERSION);
416 doc.setInfo(swaggerObject.getInfo());
417 doc.setServers(convertToServers(swaggerObject.getSchemes(), swaggerObject.getHost(),
418 swaggerObject.getBasePath()));
419 doc.setPaths(swaggerObject.getPaths());
420 doc.setComponents(new Components(swaggerObject.getDefinitions()));
425 private static List<Server> convertToServers(final List<String> schemes, final String host, final String basePath) {
426 return ImmutableList.of(new Server(schemes.get(0) + "://" + host + basePath));
429 protected abstract String getPathVersion();
431 public abstract String getResourcePath(String resourceType, String context);
433 public abstract String getResourcePathPart(String resourceType);
435 private static String generateCacheKey(final String module, final String revision) {
436 return module + "(" + revision + ")";
439 private void addPaths(final DataSchemaNode node, final Optional<String> deviceName, final String moduleName,
440 final ObjectNode paths, final ArrayNode parentPathParams,
441 final EffectiveModelContext schemaContext, final boolean isConfig, final String parentName,
442 final DefinitionNames definitionNames, final OAversion oaversion, final String resourcePath) {
443 LOG.debug("Adding path: [{}]", resourcePath);
445 final ArrayNode pathParams = JsonUtil.copy(parentPathParams);
446 Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
447 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
448 final DataNodeContainer dataNodeContainer = (DataNodeContainer) node;
449 childSchemaNodes = dataNodeContainer.getChildNodes();
452 final ObjectNode path = JsonNodeFactory.instance.objectNode();
453 path.setAll(operations(node, moduleName, deviceName, pathParams, isConfig, parentName, definitionNames,
455 paths.set(resourcePath, path);
457 if (node instanceof ActionNodeContainer) {
458 ((ActionNodeContainer) node).getActions().forEach(actionDef -> {
459 final String resolvedPath = "rests/operations" + resourcePath.substring(11)
460 + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
461 addOperations(actionDef, moduleName, deviceName, paths, parentName, definitionNames, oaversion,
466 for (final DataSchemaNode childNode : childSchemaNodes) {
467 if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
468 final String newParent = parentName + "_" + node.getQName().getLocalName();
469 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
470 final String newResourcePath = resourcePath + "/" + createPath(childNode, pathParams, oaversion,
472 final boolean newIsConfig = isConfig && childNode.isConfiguration();
473 addPaths(childNode, deviceName, moduleName, paths, pathParams, schemaContext,
474 newIsConfig, newParent, definitionNames, oaversion, newResourcePath);
479 private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
480 for (final DataSchemaNode child : nodes) {
481 if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
488 private static Map<String, ObjectNode> operations(final DataSchemaNode node, final String moduleName,
489 final Optional<String> deviceName, final ArrayNode pathParams,
490 final boolean isConfig, final String parentName,
491 final DefinitionNames definitionNames,
492 final OAversion oaversion) {
493 final Map<String, ObjectNode> operations = new HashMap<>();
494 final String discriminator = definitionNames.getDiscriminator(node);
496 final String nodeName = node.getQName().getLocalName();
498 final String defName = parentName + "_" + nodeName + TOP + discriminator;
499 final ObjectNode get = buildGet(node, moduleName, deviceName, pathParams, defName, isConfig, oaversion);
500 operations.put("get", get);
504 final ObjectNode put = buildPut(parentName, nodeName, discriminator, moduleName, deviceName,
505 node.getDescription().orElse(""), pathParams, oaversion);
506 operations.put("put", put);
508 final ObjectNode delete = buildDelete(node, moduleName, deviceName, pathParams, oaversion);
509 operations.put("delete", delete);
511 operations.put("post", buildPost(parentName, nodeName, discriminator, moduleName, deviceName,
512 node.getDescription().orElse(""), pathParams, oaversion));
517 protected abstract ListPathBuilder newListPathBuilder();
519 private String createPath(final DataSchemaNode schemaNode, final ArrayNode pathParams,
520 final OAversion oaversion, final String localName) {
521 final StringBuilder path = new StringBuilder();
522 path.append(localName);
524 if (schemaNode instanceof ListSchemaNode) {
525 final ListPathBuilder keyBuilder = newListPathBuilder();
526 for (final QName listKey : ((ListSchemaNode) schemaNode).getKeyDefinition()) {
527 final String paramName = createUniquePathParamName(listKey.getLocalName(), pathParams);
528 final String pathParamIdentifier = keyBuilder.nextParamIdentifier(paramName);
530 path.append(pathParamIdentifier);
532 final ObjectNode pathParam = JsonNodeFactory.instance.objectNode();
533 pathParam.put("name", paramName);
535 ((DataNodeContainer) schemaNode).findDataChildByName(listKey).flatMap(DataSchemaNode::getDescription)
536 .ifPresent(desc -> pathParam.put("description", desc));
538 final ObjectNode typeParent = getTypeParentNode(pathParam, oaversion);
540 typeParent.put("type", "string");
541 pathParam.put("in", "path");
542 pathParam.put("required", true);
544 pathParams.add(pathParam);
547 return path.toString();
550 private String createUniquePathParamName(final String clearName, final ArrayNode pathParams) {
551 for (final JsonNode pathParam : pathParams) {
552 if (isNamePicked(clearName, pathParam)) {
553 return createUniquePathParamName(clearName, pathParams, 1);
559 private String createUniquePathParamName(final String clearName, final ArrayNode pathParams,
560 final int discriminator) {
561 final String newName = clearName + discriminator;
562 for (final JsonNode pathParam : pathParams) {
563 if (isNamePicked(newName, pathParam)) {
564 return createUniquePathParamName(clearName, pathParams, discriminator + 1);
570 private static boolean isNamePicked(final String name, final JsonNode pathParam) {
571 return name.equals(pathParam.get("name").asText());
574 public SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
575 if (schemaContext == null) {
576 return Collections.emptySortedSet();
579 final SortedSet<Module> sortedModules = new TreeSet<>((module1, module2) -> {
580 int result = module1.getName().compareTo(module2.getName());
582 result = Revision.compare(module1.getRevision(), module2.getRevision());
585 result = module1.getNamespace().compareTo(module2.getNamespace());
589 for (final Module m : schemaContext.getModules()) {
591 sortedModules.add(m);
594 return sortedModules;
597 private static void addOperations(final OperationDefinition operDef, final String moduleName,
598 final Optional<String> deviceName, final ObjectNode paths, final String parentName,
599 final DefinitionNames definitionNames, final OAversion oaversion, final String resourcePath) {
600 final ObjectNode operations = JsonNodeFactory.instance.objectNode();
601 operations.set("post", buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames,
603 paths.set(resourcePath, operations);
606 protected abstract void appendPathKeyValue(StringBuilder builder, Object value);
608 public String generateUrlPrefixFromInstanceID(final YangInstanceIdentifier key, final String moduleName) {
609 final StringBuilder builder = new StringBuilder();
611 if (moduleName != null) {
612 builder.append(moduleName).append(':');
614 for (final PathArgument arg : key.getPathArguments()) {
615 final String name = arg.getNodeType().getLocalName();
616 if (arg instanceof NodeIdentifierWithPredicates) {
617 final NodeIdentifierWithPredicates nodeId = (NodeIdentifierWithPredicates) arg;
618 for (final Entry<QName, Object> entry : nodeId.entrySet()) {
619 appendPathKeyValue(builder, entry.getValue());
622 builder.append(name).append('/');
625 return builder.toString();
628 protected interface ListPathBuilder {
629 String nextParamIdentifier(String key);