Update testtool documentation
[netconf.git] / restconf / sal-rest-docgen / src / main / java / org / opendaylight / netconf / sal / rest / doc / impl / BaseYangSwaggerGenerator.java
1 /*
2  * Copyright (c) 2014 Brocade Communications Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.netconf.sal.rest.doc.impl;
9
10 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.TOP;
11 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildDelete;
12 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildGet;
13 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPost;
14 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPostOperation;
15 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.buildPut;
16 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.getTypeParentNode;
17 import static org.opendaylight.netconf.sal.rest.doc.util.JsonUtil.addFields;
18 import static org.opendaylight.netconf.sal.rest.doc.util.RestDocgenUtil.resolvePathArgumentsName;
19
20 import com.fasterxml.jackson.databind.JsonNode;
21 import com.fasterxml.jackson.databind.ObjectMapper;
22 import com.fasterxml.jackson.databind.SerializationFeature;
23 import com.fasterxml.jackson.databind.node.ArrayNode;
24 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
25 import com.fasterxml.jackson.databind.node.ObjectNode;
26 import com.google.common.base.Preconditions;
27 import com.google.common.collect.ImmutableList;
28 import com.google.common.collect.Range;
29 import java.io.IOException;
30 import java.time.format.DateTimeParseException;
31 import java.util.Arrays;
32 import java.util.Collection;
33 import java.util.Collections;
34 import java.util.HashMap;
35 import java.util.Iterator;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Map.Entry;
39 import java.util.Optional;
40 import java.util.Set;
41 import java.util.SortedSet;
42 import java.util.TreeSet;
43 import javax.ws.rs.core.UriInfo;
44 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
45 import org.opendaylight.netconf.sal.rest.doc.impl.ApiDocServiceImpl.OAversion;
46 import org.opendaylight.netconf.sal.rest.doc.swagger.CommonApiObject;
47 import org.opendaylight.netconf.sal.rest.doc.swagger.Components;
48 import org.opendaylight.netconf.sal.rest.doc.swagger.Info;
49 import org.opendaylight.netconf.sal.rest.doc.swagger.OpenApiObject;
50 import org.opendaylight.netconf.sal.rest.doc.swagger.Server;
51 import org.opendaylight.netconf.sal.rest.doc.swagger.SwaggerObject;
52 import org.opendaylight.netconf.sal.rest.doc.util.JsonUtil;
53 import org.opendaylight.yangtools.yang.common.QName;
54 import org.opendaylight.yangtools.yang.common.Revision;
55 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
56 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
57 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
58 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
59 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
60 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
61 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
62 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
63 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
64 import org.opendaylight.yangtools.yang.model.api.Module;
65 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
66 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
69
70 public abstract class BaseYangSwaggerGenerator {
71
72     private static final Logger LOG = LoggerFactory.getLogger(BaseYangSwaggerGenerator.class);
73
74     private static final String API_VERSION = "1.0.0";
75     private static final String SWAGGER_VERSION = "2.0";
76     private static final String OPEN_API_VERSION = "3.0.3";
77     private static final ObjectMapper MAPPER = new ObjectMapper();
78
79     private final DefinitionGenerator jsonConverter = new DefinitionGenerator();
80     private final DOMSchemaService schemaService;
81
82     public static final String BASE_PATH = "/";
83     public static final String MODULE_NAME_SUFFIX = "_module";
84
85     static {
86         MAPPER.configure(SerializationFeature.INDENT_OUTPUT, true);
87     }
88
89     protected BaseYangSwaggerGenerator(final Optional<DOMSchemaService> schemaService) {
90         this.schemaService = schemaService.orElse(null);
91     }
92
93     public DOMSchemaService getSchemaService() {
94         return schemaService;
95     }
96
97     public SwaggerObject getAllModulesDoc(final UriInfo uriInfo, final DefinitionNames definitionNames,
98                                           final OAversion oaversion) {
99         final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
100         Preconditions.checkState(schemaContext != null);
101         return getAllModulesDoc(uriInfo, Optional.empty(), schemaContext, Optional.empty(), "", definitionNames,
102                 oaversion);
103     }
104
105     public SwaggerObject getAllModulesDoc(final UriInfo uriInfo, final Optional<Range<Integer>> range,
106                                           final EffectiveModelContext schemaContext, final Optional<String> deviceName,
107                                           final String context, final DefinitionNames definitionNames,
108                                           final OAversion oaversion) {
109         final String schema = createSchemaFromUriInfo(uriInfo);
110         final String host = createHostFromUriInfo(uriInfo);
111         String name = "Controller";
112         if (deviceName.isPresent()) {
113             name = deviceName.get();
114         }
115
116         final String title = name + " modules of RESTCONF";
117         final SwaggerObject doc = createSwaggerObject(schema, host, BASE_PATH, title);
118         doc.setDefinitions(JsonNodeFactory.instance.objectNode());
119         doc.setPaths(JsonNodeFactory.instance.objectNode());
120
121         fillDoc(doc, range, schemaContext, context, deviceName, oaversion, definitionNames);
122
123         return doc;
124     }
125
126     public void fillDoc(final SwaggerObject doc, final Optional<Range<Integer>> range,
127                         final EffectiveModelContext schemaContext, final String context,
128                         final Optional<String> deviceName, final OAversion oaversion,
129                         final DefinitionNames definitionNames) {
130         final SortedSet<Module> modules = getSortedModules(schemaContext);
131         final Set<Module> filteredModules;
132         if (range.isPresent()) {
133             filteredModules = filterByRange(modules, range.get());
134         } else {
135             filteredModules = modules;
136         }
137
138         for (final Module module : filteredModules) {
139             final String revisionString = module.getQNameModule().getRevision().map(Revision::toString).orElse(null);
140
141             LOG.debug("Working on [{},{}]...", module.getName(), revisionString);
142
143             getSwaggerDocSpec(module, context, deviceName, schemaContext, oaversion, definitionNames, doc, false);
144         }
145     }
146
147     private static Set<Module> filterByRange(final SortedSet<Module> modules, final Range<Integer> range) {
148         final int begin = range.lowerEndpoint();
149         final int end = range.upperEndpoint();
150
151         Module firstModule = null;
152
153         final Iterator<Module> iterator = modules.iterator();
154         int counter = 0;
155         while (iterator.hasNext() && counter < end) {
156             final Module module = iterator.next();
157             if (containsListOrContainer(module.getChildNodes()) || !module.getRpcs().isEmpty()) {
158                 if (counter == begin) {
159                     firstModule = module;
160                 }
161                 counter++;
162             }
163         }
164
165         if (iterator.hasNext()) {
166             return modules.subSet(firstModule, iterator.next());
167         } else {
168             return modules.tailSet(firstModule);
169         }
170     }
171
172     public CommonApiObject getApiDeclaration(final String module, final String revision, final UriInfo uriInfo,
173                                              final OAversion oaversion) {
174         final EffectiveModelContext schemaContext = schemaService.getGlobalContext();
175         Preconditions.checkState(schemaContext != null);
176         final SwaggerObject doc = getApiDeclaration(module, revision, uriInfo, schemaContext, "", oaversion);
177         return getAppropriateDoc(doc, oaversion);
178     }
179
180     public SwaggerObject getApiDeclaration(final String moduleName, final String revision, final UriInfo uriInfo,
181                                            final EffectiveModelContext schemaContext, final String context,
182                                            final OAversion oaversion) {
183         final Optional<Revision> rev;
184
185         try {
186             rev = Revision.ofNullable(revision);
187         } catch (final DateTimeParseException e) {
188             throw new IllegalArgumentException(e);
189         }
190
191         final Module module = schemaContext.findModule(moduleName, rev).orElse(null);
192         Preconditions.checkArgument(module != null,
193                 "Could not find module by name,revision: " + moduleName + "," + revision);
194
195         return getApiDeclaration(module, uriInfo, context, schemaContext, oaversion);
196     }
197
198     public SwaggerObject getApiDeclaration(final Module module, final UriInfo uriInfo, final String context,
199                                            final EffectiveModelContext schemaContext, final OAversion oaversion) {
200         final String schema = createSchemaFromUriInfo(uriInfo);
201         final String host = createHostFromUriInfo(uriInfo);
202
203         return getSwaggerDocSpec(module, schema, host, BASE_PATH, context, schemaContext, oaversion);
204     }
205
206     public String createHostFromUriInfo(final UriInfo uriInfo) {
207         String portPart = "";
208         final int port = uriInfo.getBaseUri().getPort();
209         if (port != -1) {
210             portPart = ":" + port;
211         }
212         return uriInfo.getBaseUri().getHost() + portPart;
213     }
214
215     public String createSchemaFromUriInfo(final UriInfo uriInfo) {
216         return uriInfo.getBaseUri().getScheme();
217     }
218
219     public SwaggerObject getSwaggerDocSpec(final Module module, final String schema, final String host,
220                                            final String basePath, final String context,
221                                            final EffectiveModelContext schemaContext, final OAversion oaversion) {
222         final SwaggerObject doc = createSwaggerObject(schema, host, basePath, module.getName());
223         final DefinitionNames definitionNames = new DefinitionNames();
224         return getSwaggerDocSpec(module, context, Optional.empty(), schemaContext, oaversion, definitionNames, doc,
225             true);
226     }
227
228     public SwaggerObject getSwaggerDocSpec(final Module module, final String context, final Optional<String> deviceName,
229                                            final EffectiveModelContext schemaContext, final OAversion oaversion,
230                                            final DefinitionNames definitionNames, final SwaggerObject doc,
231                                            final boolean isForSingleModule) {
232         final ObjectNode definitions;
233
234         try {
235             if (isForSingleModule) {
236                 definitions = jsonConverter.convertToJsonSchema(module, schemaContext, definitionNames, oaversion,
237                         true);
238                 doc.setDefinitions(definitions);
239             } else {
240                 definitions = jsonConverter.convertToJsonSchema(module, schemaContext, definitionNames, oaversion,
241                         false);
242                 addFields(doc.getDefinitions(), definitions.fields());
243             }
244             if (LOG.isDebugEnabled()) {
245                 LOG.debug("Document: {}", MAPPER.writeValueAsString(doc));
246             }
247         } catch (final IOException e) {
248             LOG.error("Exception occured in DefinitionGenerator", e);
249         }
250
251         final ObjectNode paths = JsonNodeFactory.instance.objectNode();
252         final String moduleName = module.getName();
253
254         boolean hasAddRootPostLink = false;
255
256         final Collection<? extends DataSchemaNode> dataSchemaNodes = module.getChildNodes();
257         LOG.debug("child nodes size [{}]", dataSchemaNodes.size());
258         for (final DataSchemaNode node : dataSchemaNodes) {
259             if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
260                 LOG.debug("Is Configuration node [{}] [{}]", node.isConfiguration(), node.getQName().getLocalName());
261
262                 final String localName = module.getName() + ":" + node.getQName().getLocalName();
263                 ArrayNode pathParams = JsonNodeFactory.instance.arrayNode();
264                 String resourcePath;
265
266                 if (node.isConfiguration()) { // This node's config statement is
267                     // true.
268                     resourcePath = getResourcePath("config", context);
269
270                     /*
271                      * When there are two or more top container or list nodes
272                      * whose config statement is true in module, make sure that
273                      * only one root post link is added for this module.
274                      */
275                     if (isForSingleModule && !hasAddRootPostLink) {
276                         LOG.debug("Has added root post link for module {}", module.getName());
277                         addRootPostLink(module, deviceName, pathParams, resourcePath, paths, oaversion);
278
279                         hasAddRootPostLink = true;
280                     }
281
282                     final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, oaversion, localName);
283                     addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, true, module.getName(),
284                         definitionNames, oaversion, resolvedPath);
285                 }
286                 pathParams = JsonNodeFactory.instance.arrayNode();
287                 resourcePath = getResourcePath("operational", context);
288
289                 if (!node.isConfiguration()) {
290                     final String resolvedPath = resourcePath + "/" + createPath(node, pathParams, oaversion, localName);
291                     addPaths(node, deviceName, moduleName, paths, pathParams, schemaContext, false, moduleName,
292                         definitionNames, oaversion, resolvedPath);
293                 }
294             }
295         }
296
297         for (final RpcDefinition rpcDefinition : module.getRpcs()) {
298             final String resolvedPath = getResourcePath("operations", context) + "/" + moduleName + ":"
299                     + rpcDefinition.getQName().getLocalName();
300             addOperations(rpcDefinition, moduleName, deviceName, paths, module.getName(), definitionNames, oaversion,
301                     resolvedPath);
302         }
303
304         LOG.debug("Number of Paths found [{}]", paths.size());
305
306         if (isForSingleModule) {
307             doc.setPaths(paths);
308         } else {
309             addFields(doc.getPaths(), paths.fields());
310         }
311
312         return doc;
313     }
314
315     private static void addRootPostLink(final Module module, final Optional<String> deviceName,
316             final ArrayNode pathParams, final String resourcePath, final ObjectNode paths, final OAversion oaversion) {
317         if (containsListOrContainer(module.getChildNodes())) {
318             final ObjectNode post = JsonNodeFactory.instance.objectNode();
319             final String moduleName = module.getName();
320             final String name = moduleName + MODULE_NAME_SUFFIX;
321             post.set("post", buildPost("", name, "", moduleName, deviceName,
322                     module.getDescription().orElse(""), pathParams, oaversion));
323             paths.set(resourcePath, post);
324         }
325     }
326
327     public SwaggerObject createSwaggerObject(final String schema, final String host, final String basePath,
328                                              final String title) {
329         final SwaggerObject doc = new SwaggerObject();
330         doc.setSwagger(SWAGGER_VERSION);
331         final Info info = new Info();
332         info.setTitle(title);
333         info.setVersion(API_VERSION);
334         doc.setInfo(info);
335         doc.setSchemes(ImmutableList.of(schema));
336         doc.setHost(host);
337         doc.setBasePath(basePath);
338         doc.setProduces(Arrays.asList("application/xml", "application/json"));
339         return doc;
340     }
341
342     public static CommonApiObject getAppropriateDoc(final SwaggerObject swaggerObject, final OAversion oaversion) {
343         if (oaversion.equals(OAversion.V3_0)) {
344             return convertToOpenApi(swaggerObject);
345         }
346         return swaggerObject;
347     }
348
349     private static OpenApiObject convertToOpenApi(final SwaggerObject swaggerObject) {
350         final OpenApiObject doc = new OpenApiObject();
351         doc.setOpenapi(OPEN_API_VERSION);
352         doc.setInfo(swaggerObject.getInfo());
353         doc.setServers(convertToServers(swaggerObject.getSchemes(), swaggerObject.getHost(),
354                 swaggerObject.getBasePath()));
355         doc.setPaths(swaggerObject.getPaths());
356         doc.setComponents(new Components(swaggerObject.getDefinitions()));
357         return doc;
358     }
359
360
361     private static List<Server> convertToServers(final List<String> schemes, final String host, final String basePath) {
362         return ImmutableList.of(new Server(schemes.get(0) + "://" + host + basePath));
363     }
364
365     protected abstract String getPathVersion();
366
367     public abstract String getResourcePath(String resourceType, String context);
368
369     public abstract String getResourcePathPart(String resourceType);
370
371     private void addPaths(final DataSchemaNode node, final Optional<String> deviceName, final String moduleName,
372                           final ObjectNode paths, final ArrayNode parentPathParams,
373                           final EffectiveModelContext schemaContext, final boolean isConfig, final String parentName,
374                           final DefinitionNames definitionNames, final OAversion oaversion, final String resourcePath) {
375         LOG.debug("Adding path: [{}]", resourcePath);
376
377         final ArrayNode pathParams = JsonUtil.copy(parentPathParams);
378         Iterable<? extends DataSchemaNode> childSchemaNodes = Collections.emptySet();
379         if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
380             final DataNodeContainer dataNodeContainer = (DataNodeContainer) node;
381             childSchemaNodes = dataNodeContainer.getChildNodes();
382         }
383
384         final ObjectNode path = JsonNodeFactory.instance.objectNode();
385         path.setAll(operations(node, moduleName, deviceName, pathParams, isConfig, parentName, definitionNames,
386                 oaversion));
387         paths.set(resourcePath, path);
388
389         if (node instanceof ActionNodeContainer) {
390             ((ActionNodeContainer) node).getActions().forEach(actionDef -> {
391                 final String resolvedPath = "rests/operations" + resourcePath.substring(11)
392                         + "/" + resolvePathArgumentsName(actionDef.getQName(), node.getQName(), schemaContext);
393                 addOperations(actionDef, moduleName, deviceName, paths, parentName, definitionNames, oaversion,
394                         resolvedPath);
395             });
396         }
397
398         for (final DataSchemaNode childNode : childSchemaNodes) {
399             if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
400                 final String newParent = parentName + "_" + node.getQName().getLocalName();
401                 final String localName = resolvePathArgumentsName(childNode.getQName(), node.getQName(), schemaContext);
402                 final String newResourcePath = resourcePath + "/" + createPath(childNode, pathParams, oaversion,
403                         localName);
404                 final boolean newIsConfig = isConfig && childNode.isConfiguration();
405                 addPaths(childNode, deviceName, moduleName, paths, pathParams, schemaContext,
406                     newIsConfig, newParent, definitionNames, oaversion, newResourcePath);
407             }
408         }
409     }
410
411     private static boolean containsListOrContainer(final Iterable<? extends DataSchemaNode> nodes) {
412         for (final DataSchemaNode child : nodes) {
413             if (child instanceof ListSchemaNode || child instanceof ContainerSchemaNode) {
414                 return true;
415             }
416         }
417         return false;
418     }
419
420     private static Map<String, ObjectNode> operations(final DataSchemaNode node, final String moduleName,
421                                                       final Optional<String> deviceName, final ArrayNode pathParams,
422                                                       final boolean isConfig, final String parentName,
423                                                       final DefinitionNames definitionNames,
424                                                       final OAversion oaversion) {
425         final Map<String, ObjectNode> operations = new HashMap<>();
426         final String discriminator = definitionNames.getDiscriminator(node);
427
428         final String nodeName = node.getQName().getLocalName();
429
430         final String defName = parentName + "_" + nodeName + TOP + discriminator;
431         final ObjectNode get = buildGet(node, moduleName, deviceName, pathParams, defName, isConfig, oaversion);
432         operations.put("get", get);
433
434
435         if (isConfig) {
436             final ObjectNode put = buildPut(parentName, nodeName, discriminator, moduleName, deviceName,
437                     node.getDescription().orElse(""), pathParams, oaversion);
438             operations.put("put", put);
439
440             final ObjectNode delete = buildDelete(node, moduleName, deviceName, pathParams, oaversion);
441             operations.put("delete", delete);
442
443             operations.put("post", buildPost(parentName, nodeName, discriminator, moduleName, deviceName,
444                     node.getDescription().orElse(""), pathParams, oaversion));
445         }
446         return operations;
447     }
448
449     protected abstract ListPathBuilder newListPathBuilder();
450
451     private String createPath(final DataSchemaNode schemaNode, final ArrayNode pathParams,
452                               final OAversion oaversion, final String localName) {
453         final StringBuilder path = new StringBuilder();
454         path.append(localName);
455
456         if (schemaNode instanceof ListSchemaNode) {
457             final ListPathBuilder keyBuilder = newListPathBuilder();
458             for (final QName listKey : ((ListSchemaNode) schemaNode).getKeyDefinition()) {
459                 final String paramName = createUniquePathParamName(listKey.getLocalName(), pathParams);
460                 final String pathParamIdentifier = keyBuilder.nextParamIdentifier(paramName);
461
462                 path.append(pathParamIdentifier);
463
464                 final ObjectNode pathParam = JsonNodeFactory.instance.objectNode();
465                 pathParam.put("name", paramName);
466
467                 ((DataNodeContainer) schemaNode).findDataChildByName(listKey).flatMap(DataSchemaNode::getDescription)
468                         .ifPresent(desc -> pathParam.put("description", desc));
469
470                 final ObjectNode typeParent = getTypeParentNode(pathParam, oaversion);
471
472                 typeParent.put("type", "string");
473                 pathParam.put("in", "path");
474                 pathParam.put("required", true);
475
476                 pathParams.add(pathParam);
477             }
478         }
479         return path.toString();
480     }
481
482     private String createUniquePathParamName(final String clearName, final ArrayNode pathParams) {
483         for (final JsonNode pathParam : pathParams) {
484             if (isNamePicked(clearName, pathParam)) {
485                 return createUniquePathParamName(clearName, pathParams, 1);
486             }
487         }
488         return clearName;
489     }
490
491     private String createUniquePathParamName(final String clearName, final ArrayNode pathParams,
492                                              final int discriminator) {
493         final String newName = clearName + discriminator;
494         for (final JsonNode pathParam : pathParams) {
495             if (isNamePicked(newName, pathParam)) {
496                 return createUniquePathParamName(clearName, pathParams, discriminator + 1);
497             }
498         }
499         return newName;
500     }
501
502     private static boolean isNamePicked(final String name, final JsonNode pathParam) {
503         return name.equals(pathParam.get("name").asText());
504     }
505
506     public SortedSet<Module> getSortedModules(final EffectiveModelContext schemaContext) {
507         if (schemaContext == null) {
508             return Collections.emptySortedSet();
509         }
510
511         final SortedSet<Module> sortedModules = new TreeSet<>((module1, module2) -> {
512             int result = module1.getName().compareTo(module2.getName());
513             if (result == 0) {
514                 result = Revision.compare(module1.getRevision(), module2.getRevision());
515             }
516             if (result == 0) {
517                 result = module1.getNamespace().compareTo(module2.getNamespace());
518             }
519             return result;
520         });
521         for (final Module m : schemaContext.getModules()) {
522             if (m != null) {
523                 sortedModules.add(m);
524             }
525         }
526         return sortedModules;
527     }
528
529     private static void addOperations(final OperationDefinition operDef, final String moduleName,
530             final Optional<String> deviceName, final ObjectNode paths, final String parentName,
531             final DefinitionNames definitionNames, final OAversion oaversion, final String resourcePath) {
532         final ObjectNode operations = JsonNodeFactory.instance.objectNode();
533         operations.set("post", buildPostOperation(operDef, moduleName, deviceName, parentName, definitionNames,
534                 oaversion));
535         paths.set(resourcePath, operations);
536     }
537
538     protected abstract void appendPathKeyValue(StringBuilder builder, Object value);
539
540     public String generateUrlPrefixFromInstanceID(final YangInstanceIdentifier key, final String moduleName) {
541         final StringBuilder builder = new StringBuilder();
542         builder.append("/");
543         if (moduleName != null) {
544             builder.append(moduleName).append(':');
545         }
546         for (final PathArgument arg : key.getPathArguments()) {
547             final String name = arg.getNodeType().getLocalName();
548             if (arg instanceof NodeIdentifierWithPredicates) {
549                 final NodeIdentifierWithPredicates nodeId = (NodeIdentifierWithPredicates) arg;
550                 for (final Entry<QName, Object> entry : nodeId.entrySet()) {
551                     appendPathKeyValue(builder, entry.getValue());
552                 }
553             } else {
554                 builder.append(name).append('/');
555             }
556         }
557         return builder.toString();
558     }
559
560     protected interface ListPathBuilder {
561         String nextParamIdentifier(String key);
562     }
563 }