e49c7cf1f2d57f1e536c70ead3e8fa75fa8ae440
[netconf.git] / restconf / sal-rest-docgen / src / main / java / org / opendaylight / netconf / sal / rest / doc / impl / DefinitionGenerator.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. 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.impl.BaseYangSwaggerGenerator.MODULE_NAME_SUFFIX;
11 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.CONFIG;
12 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.NAME_KEY;
13 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.POST_SUFFIX;
14 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.TOP;
15 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.XML_KEY;
16 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.XML_SUFFIX;
17 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.getAppropriateModelPrefix;
18
19 import com.fasterxml.jackson.databind.JsonNode;
20 import com.fasterxml.jackson.databind.node.ArrayNode;
21 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
22 import com.fasterxml.jackson.databind.node.ObjectNode;
23 import com.fasterxml.jackson.databind.node.TextNode;
24 import com.google.common.collect.Range;
25 import com.google.common.collect.RangeSet;
26 import com.mifmif.common.regex.Generex;
27 import java.io.IOException;
28 import java.math.BigDecimal;
29 import java.util.Collection;
30 import java.util.Iterator;
31 import java.util.List;
32 import java.util.Map;
33 import java.util.Optional;
34 import java.util.Set;
35 import java.util.regex.Pattern;
36 import java.util.stream.Collectors;
37 import org.opendaylight.netconf.sal.rest.doc.impl.ApiDocServiceImpl.OAversion;
38 import org.opendaylight.yangtools.yang.common.Decimal64;
39 import org.opendaylight.yangtools.yang.common.QName;
40 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
41 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
42 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
47 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
49 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
51 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraint;
52 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
55 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.MandatoryAware;
57 import org.opendaylight.yangtools.yang.model.api.Module;
58 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
59 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
60 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
61 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
62 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
63 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
64 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition.Bit;
65 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
66 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
67 import org.opendaylight.yangtools.yang.model.api.type.EmptyTypeDefinition;
68 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
69 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition.EnumPair;
70 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
71 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
72 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
73 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
74 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
75 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
76 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
77 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
78 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
79 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
80 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
81 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
82 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
83 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
84 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
85 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
86 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
87 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
88 import org.slf4j.Logger;
89 import org.slf4j.LoggerFactory;
90
91 /**
92  * Generates JSON Schema for data defined in YANG. This class is not thread-safe.
93  */
94 public class DefinitionGenerator {
95
96     private static final Logger LOG = LoggerFactory.getLogger(DefinitionGenerator.class);
97
98     private static final String UNIQUE_ITEMS_KEY = "uniqueItems";
99     private static final String MAX_ITEMS = "maxItems";
100     private static final String MIN_ITEMS = "minItems";
101     private static final String MAX_LENGTH_KEY = "maxLength";
102     private static final String MIN_LENGTH_KEY = "minLength";
103     private static final String REQUIRED_KEY = "required";
104     private static final String REF_KEY = "$ref";
105     private static final String ITEMS_KEY = "items";
106     private static final String TYPE_KEY = "type";
107     private static final String PROPERTIES_KEY = "properties";
108     private static final String DESCRIPTION_KEY = "description";
109     private static final String ARRAY_TYPE = "array";
110     private static final String ENUM_KEY = "enum";
111     private static final String TITLE_KEY = "title";
112     private static final String DEFAULT_KEY = "default";
113     private static final String FORMAT_KEY = "format";
114     private static final String NAMESPACE_KEY = "namespace";
115     public static final String INPUT = "input";
116     public static final String INPUT_SUFFIX = "_input";
117     public static final String OUTPUT = "output";
118     public static final String OUTPUT_SUFFIX = "_output";
119     private static final String STRING_TYPE = "string";
120     private static final String OBJECT_TYPE = "object";
121     private static final String NUMBER_TYPE = "number";
122     private static final String INTEGER_TYPE = "integer";
123     private static final String INT32_FORMAT = "int32";
124     private static final String INT64_FORMAT = "int64";
125     private static final String BOOLEAN_TYPE = "boolean";
126     // Special characters used in automaton inside Generex.
127     // See https://www.brics.dk/automaton/doc/dk/brics/automaton/RegExp.html
128     private static final Pattern AUTOMATON_SPECIAL_CHARACTERS = Pattern.compile("[@&\"<>#~]");
129
130     private Module topLevelModule;
131
132     public DefinitionGenerator() {
133     }
134
135     /**
136      * Creates Json definitions from provided module according to swagger spec.
137      *
138      * @param module          - Yang module to be converted
139      * @param schemaContext   - SchemaContext of all Yang files used by Api Doc
140      * @param definitionNames - Store for definition names
141      * @return ObjectNode containing data used for creating examples and definitions in Api Doc
142      * @throws IOException if I/O operation fails
143      */
144
145
146     public ObjectNode convertToJsonSchema(final Module module, final EffectiveModelContext schemaContext,
147                                           final ObjectNode definitions, final DefinitionNames definitionNames,
148                                           final OAversion oaversion, final boolean isForSingleModule)
149             throws IOException {
150         topLevelModule = module;
151
152         processIdentities(module, definitions, definitionNames, schemaContext);
153         processContainersAndLists(module, definitions, definitionNames, schemaContext, oaversion);
154         processRPCs(module, definitions, definitionNames, schemaContext, oaversion);
155
156         if (isForSingleModule) {
157             processModule(module, definitions, definitionNames, schemaContext, oaversion);
158         }
159
160         return definitions;
161     }
162
163     public ObjectNode convertToJsonSchema(final Module module, final EffectiveModelContext schemaContext,
164                                           final DefinitionNames definitionNames, final OAversion oaversion,
165                                           final boolean isForSingleModule)
166             throws IOException {
167         final ObjectNode definitions = JsonNodeFactory.instance.objectNode();
168         if (isForSingleModule) {
169             definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
170         }
171         return convertToJsonSchema(module, schemaContext, definitions, definitionNames, oaversion, isForSingleModule);
172     }
173
174     private void processModule(final Module module, final ObjectNode definitions, final DefinitionNames definitionNames,
175                                final EffectiveModelContext schemaContext, final OAversion oaversion) {
176         final ObjectNode definition = JsonNodeFactory.instance.objectNode();
177         final ObjectNode properties = JsonNodeFactory.instance.objectNode();
178         final ArrayNode required = JsonNodeFactory.instance.arrayNode();
179         final String moduleName = module.getName();
180         final String definitionName = moduleName + MODULE_NAME_SUFFIX;
181         final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
182         for (final DataSchemaNode node : module.getChildNodes()) {
183             stack.enterSchemaTree(node.getQName());
184             final String localName = node.getQName().getLocalName();
185             if (node.isConfiguration()) {
186                 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
187                     for (final DataSchemaNode childNode : ((DataNodeContainer) node).getChildNodes()) {
188                         final ObjectNode childNodeProperties = JsonNodeFactory.instance.objectNode();
189
190                         final String ref = getAppropriateModelPrefix(oaversion)
191                                 + moduleName + CONFIG
192                                 + "_" + localName
193                                 + definitionNames.getDiscriminator(node);
194
195                         if (node instanceof ListSchemaNode) {
196                             childNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
197                             final ObjectNode items = JsonNodeFactory.instance.objectNode();
198                             items.put(REF_KEY, ref);
199                             childNodeProperties.set(ITEMS_KEY, items);
200                             childNodeProperties.put(DESCRIPTION_KEY, childNode.getDescription().orElse(""));
201                             childNodeProperties.put(TITLE_KEY, localName + CONFIG);
202                         } else {
203                          /*
204                             Description can't be added, because nothing allowed alongside $ref.
205                             allOf is not an option, because ServiceNow can't parse it.
206                           */
207                             childNodeProperties.put(REF_KEY, ref);
208                         }
209                         //add module name prefix to property name, when ServiceNow can process colons
210                         properties.set(localName, childNodeProperties);
211                     }
212                 } else if (node instanceof LeafSchemaNode) {
213                     /*
214                         Add module name prefix to property name, when ServiceNow can process colons(second parameter
215                         of processLeafNode).
216                      */
217                     processLeafNode((LeafSchemaNode) node, localName, properties, required, stack,
218                             definitions, definitionNames, oaversion);
219                 }
220             }
221             stack.exit();
222         }
223         definition.put(TITLE_KEY, definitionName);
224         definition.put(TYPE_KEY, OBJECT_TYPE);
225         definition.set(PROPERTIES_KEY, properties);
226         definition.put(DESCRIPTION_KEY, module.getDescription().orElse(""));
227         setRequiredIfNotEmpty(definition, required);
228
229         definitions.set(definitionName, definition);
230     }
231
232     private void processContainersAndLists(final Module module, final ObjectNode definitions,
233             final DefinitionNames definitionNames, final EffectiveModelContext schemaContext, final OAversion oaversion)
234                 throws IOException {
235         final String moduleName = module.getName();
236         final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
237         for (final DataSchemaNode childNode : module.getChildNodes()) {
238             stack.enterSchemaTree(childNode.getQName());
239             // For every container and list in the module
240             if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
241                 if (childNode.isConfiguration()) {
242                     processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
243                             true, stack, oaversion);
244                 }
245                 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
246                         false, stack, oaversion);
247                 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack, oaversion);
248             }
249             stack.exit();
250         }
251     }
252
253     private void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
254                                             final ObjectNode definitions, final DefinitionNames definitionNames,
255                                             final SchemaInferenceStack stack, final OAversion oaversion)
256             throws IOException {
257         for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
258             stack.enterSchemaTree(actionDef.getQName());
259             processOperations(actionDef, moduleName, definitions, definitionNames, stack, oaversion);
260             stack.exit();
261         }
262     }
263
264     private void processRPCs(final Module module, final ObjectNode definitions, final DefinitionNames definitionNames,
265                              final EffectiveModelContext schemaContext, final OAversion oaversion) throws IOException {
266         final String moduleName = module.getName();
267         final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
268         for (final RpcDefinition rpcDefinition : module.getRpcs()) {
269             stack.enterSchemaTree(rpcDefinition.getQName());
270             processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack, oaversion);
271             stack.exit();
272         }
273     }
274
275     private void processOperations(final OperationDefinition operationDef, final String parentName,
276             final ObjectNode definitions, final DefinitionNames definitionNames,
277             final SchemaInferenceStack stack, final OAversion oaversion)
278                 throws IOException {
279         final String operationName = operationDef.getQName().getLocalName();
280         processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
281                 definitionNames, stack, oaversion);
282         processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
283                 definitionNames, stack, oaversion);
284     }
285
286     private void processOperationInputOutput(final ContainerLike container, final String operationName,
287                                              final String parentName, final boolean isInput,
288                                              final ObjectNode definitions, final DefinitionNames definitionNames,
289                                              final SchemaInferenceStack stack, final OAversion oaversion)
290             throws IOException {
291         stack.enterSchemaTree(container.getQName());
292         if (!container.getChildNodes().isEmpty()) {
293             final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
294             final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
295             processChildren(childSchema, container.getChildNodes(), parentName, definitions, definitionNames,
296                     false, stack, oaversion);
297
298             childSchema.put(TYPE_KEY, OBJECT_TYPE);
299             final ObjectNode xml = JsonNodeFactory.instance.objectNode();
300             xml.put(NAME_KEY, isInput ? INPUT : OUTPUT);
301             childSchema.set(XML_KEY, xml);
302             childSchema.put(TITLE_KEY, filename);
303             final String discriminator =
304                     definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
305             definitions.set(filename + discriminator, childSchema);
306
307             processTopData(filename, discriminator, definitions, container, oaversion);
308         }
309         stack.exit();
310     }
311
312     private static ObjectNode processTopData(final String filename, final String discriminator,
313             final ObjectNode definitions, final SchemaNode schemaNode, final OAversion oaversion) {
314         final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
315         final String name = filename + discriminator;
316         final String ref = getAppropriateModelPrefix(oaversion) + name;
317         final String topName = filename + TOP;
318
319         if (schemaNode instanceof ListSchemaNode) {
320             dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
321             final ObjectNode items = JsonNodeFactory.instance.objectNode();
322             items.put(REF_KEY, ref);
323             dataNodeProperties.set(ITEMS_KEY, items);
324             dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
325         } else {
326              /*
327                 Description can't be added, because nothing allowed alongside $ref.
328                 allOf is not an option, because ServiceNow can't parse it.
329               */
330             dataNodeProperties.put(REF_KEY, ref);
331         }
332
333         final ObjectNode properties = JsonNodeFactory.instance.objectNode();
334         /*
335             Add module name prefix to property name, when needed, when ServiceNow can process colons,
336             use RestDocGenUtil#resolveNodesName for creating property name
337          */
338         properties.set(schemaNode.getQName().getLocalName(), dataNodeProperties);
339         final ObjectNode finalChildSchema = JsonNodeFactory.instance.objectNode();
340         finalChildSchema.put(TYPE_KEY, OBJECT_TYPE);
341         finalChildSchema.set(PROPERTIES_KEY, properties);
342         finalChildSchema.put(TITLE_KEY, topName);
343
344
345         definitions.set(topName + discriminator, finalChildSchema);
346
347         return dataNodeProperties;
348     }
349
350     /**
351      * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
352      * @param module          The module from which the identity stmt will be processed
353      * @param definitions     The ObjectNode in which the parsed identity will be put as a 'model' obj
354      * @param definitionNames Store for definition names
355      */
356     private static void processIdentities(final Module module, final ObjectNode definitions,
357                                           final DefinitionNames definitionNames, final EffectiveModelContext context) {
358         final String moduleName = module.getName();
359         final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
360         LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
361
362         for (final IdentitySchemaNode idNode : idNodes) {
363             final ObjectNode identityObj = buildIdentityObject(idNode, context);
364             final String idName = idNode.getQName().getLocalName();
365             final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
366             final String name = idName + discriminator;
367             definitions.set(name, identityObj);
368         }
369     }
370
371     private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
372                                                 final ArrayNode enumPayload, final EffectiveModelContext context) {
373         for (final IdentitySchemaNode derivedId : derivedIds) {
374             enumPayload.add(derivedId.getQName().getLocalName());
375             populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
376         }
377     }
378
379     private ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
380                                                 final ObjectNode definitions, final DefinitionNames definitionNames,
381                                                 final boolean isConfig, final SchemaInferenceStack stack,
382                                                 final OAversion oaversion) throws IOException {
383         if (dataNode instanceof ListSchemaNode || dataNode instanceof ContainerSchemaNode) {
384             final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
385             final SchemaNode schemaNode = (SchemaNode) dataNode;
386             final String localName = schemaNode.getQName().getLocalName();
387             final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
388             final String nameAsParent = parentName + "_" + localName;
389             final ObjectNode properties =
390                     processChildren(childSchema, containerChildren, parentName + "_" + localName, definitions,
391                             definitionNames, isConfig, stack, oaversion);
392
393             final String nodeName = parentName + (isConfig ? CONFIG : "") + "_" + localName;
394             final String postNodeName = parentName + CONFIG + "_" + localName + POST_SUFFIX;
395             final String postXmlNodeName = postNodeName + XML_SUFFIX;
396             final String parentNameConfigLocalName = parentName + CONFIG + "_" + localName;
397
398             final String description = schemaNode.getDescription().orElse("");
399             final String discriminator;
400
401             if (!definitionNames.isListedNode(schemaNode)) {
402                 final List<String> names = List.of(parentNameConfigLocalName,
403                         parentNameConfigLocalName + TOP,
404                         nameAsParent,
405                         nameAsParent + TOP,
406                         postNodeName,
407                         postXmlNodeName);
408                 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
409             } else {
410                 discriminator = definitionNames.getDiscriminator(schemaNode);
411             }
412
413             if (isConfig) {
414                 final ObjectNode postSchema = createPostJsonSchema(schemaNode, properties, postNodeName, description);
415                 String truePostNodeName = postNodeName + discriminator;
416                 definitions.set(truePostNodeName, postSchema);
417
418                 final ObjectNode postXmlSchema = JsonNodeFactory.instance.objectNode();
419                 postXmlSchema.put(REF_KEY, getAppropriateModelPrefix(oaversion) + truePostNodeName);
420                 definitions.set(postXmlNodeName + discriminator, postXmlSchema);
421             }
422
423             childSchema.put(TYPE_KEY, OBJECT_TYPE);
424             childSchema.set(PROPERTIES_KEY, properties);
425             childSchema.put(TITLE_KEY, nodeName);
426             childSchema.put(DESCRIPTION_KEY, description);
427
428             final String defName = nodeName + discriminator;
429             childSchema.set(XML_KEY, buildXmlParameter(schemaNode));
430             definitions.set(defName, childSchema);
431
432             return processTopData(nodeName, discriminator, definitions, schemaNode, oaversion);
433         }
434         return null;
435     }
436
437     private static ObjectNode createPostJsonSchema(final SchemaNode dataNode, final ObjectNode properties,
438             final String postNodeName, final String description) {
439         final ObjectNode postSchema = JsonNodeFactory.instance.objectNode();
440         final ObjectNode postItemProperties;
441         if (dataNode instanceof ListSchemaNode) {
442             postItemProperties = createListItemProperties(properties, (ListSchemaNode) dataNode);
443         } else {
444             postItemProperties = properties.deepCopy();
445         }
446         postSchema.put(TYPE_KEY, OBJECT_TYPE);
447         postSchema.set(PROPERTIES_KEY, postItemProperties);
448         postSchema.put(TITLE_KEY, postNodeName);
449         postSchema.put(DESCRIPTION_KEY, description);
450         postSchema.set(XML_KEY, buildXmlParameter(dataNode));
451         return postSchema;
452     }
453
454     private static ObjectNode createListItemProperties(final ObjectNode properties, final ListSchemaNode listNode) {
455         final ObjectNode postListItemProperties = JsonNodeFactory.instance.objectNode();
456         final List<QName> keyDefinition = listNode.getKeyDefinition();
457         final Set<String> keys = listNode.getChildNodes().stream()
458                 .filter(node -> keyDefinition.contains(node.getQName()))
459                 .map(node -> node.getQName().getLocalName())
460                 .collect(Collectors.toSet());
461
462         Iterator<Map.Entry<String, JsonNode>> it = properties.fields();
463         while (it.hasNext()) {
464             Map.Entry<String, JsonNode> property = it.next();
465             if (!keys.contains(property.getKey())) {
466                 postListItemProperties.set(property.getKey(), property.getValue());
467             }
468         }
469
470         return postListItemProperties;
471     }
472
473     /**
474      * Processes the nodes.
475      */
476     private ObjectNode processChildren(
477             final ObjectNode parentNode, final Collection<? extends DataSchemaNode> nodes, final String parentName,
478             final ObjectNode definitions, final DefinitionNames definitionNames, final boolean isConfig,
479             final SchemaInferenceStack stack, final OAversion oaversion) throws IOException {
480         final ObjectNode properties = JsonNodeFactory.instance.objectNode();
481         final ArrayNode required = JsonNodeFactory.instance.arrayNode();
482         for (final DataSchemaNode node : nodes) {
483             if (!isConfig || node.isConfiguration()) {
484                 processChildNode(node, parentName, definitions, definitionNames, isConfig, stack, properties,
485                         oaversion);
486             }
487         }
488         parentNode.set(PROPERTIES_KEY, properties);
489         setRequiredIfNotEmpty(parentNode, required);
490         return properties;
491     }
492
493     private void processChildNode(
494             final DataSchemaNode node, final String parentName, final ObjectNode definitions,
495             final DefinitionNames definitionNames, final boolean isConfig,
496             final SchemaInferenceStack stack, final ObjectNode properties, final OAversion oaversion)
497             throws IOException {
498
499         stack.enterSchemaTree(node.getQName());
500
501         /*
502             Add module name prefix to property name, when needed, when ServiceNow can process colons,
503             use RestDocGenUtil#resolveNodesName for creating property name
504          */
505         final String name = node.getQName().getLocalName();
506
507         if (node instanceof LeafSchemaNode leaf) {
508             processLeafNode(leaf, name, properties, JsonNodeFactory.instance.arrayNode(), stack, definitions,
509                     definitionNames, oaversion);
510
511         } else if (node instanceof AnyxmlSchemaNode anyxml) {
512             processAnyXMLNode(anyxml, name, properties, JsonNodeFactory.instance.arrayNode());
513
514         } else if (node instanceof AnydataSchemaNode anydata) {
515             processAnydataNode(anydata, name, properties, JsonNodeFactory.instance.arrayNode());
516
517         } else {
518
519             final ObjectNode property;
520             if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
521                 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions,
522                         definitionNames, isConfig, stack, oaversion);
523                 if (!isConfig) {
524                     processActionNodeContainer(node, parentName, definitions, definitionNames, stack, oaversion);
525                 }
526             } else if (node instanceof LeafListSchemaNode leafList) {
527                 property = processLeafListNode(leafList, stack, definitions, definitionNames, oaversion);
528
529             } else if (node instanceof ChoiceSchemaNode choice) {
530                 for (final CaseSchemaNode variant : choice.getCases()) {
531                     stack.enterSchemaTree(variant.getQName());
532                     for (final DataSchemaNode childNode : variant.getChildNodes()) {
533                         processChildNode(childNode, parentName, definitions, definitionNames, isConfig, stack,
534                                 properties, oaversion);
535                     }
536                     stack.exit();
537                 }
538                 property = null;
539
540             } else {
541                 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
542             }
543             if (property != null) {
544                 properties.set(name, property);
545             }
546         }
547
548         stack.exit();
549     }
550
551     private ObjectNode processLeafListNode(final LeafListSchemaNode listNode, final SchemaInferenceStack stack,
552                                            final ObjectNode definitions, final DefinitionNames definitionNames,
553                                            final OAversion oaversion) {
554         final ObjectNode props = JsonNodeFactory.instance.objectNode();
555         props.put(TYPE_KEY, ARRAY_TYPE);
556
557         final ObjectNode itemsVal = JsonNodeFactory.instance.objectNode();
558         final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
559         processElementCount(optConstraint, props);
560
561         processTypeDef(listNode.getType(), listNode, itemsVal, stack, definitions, definitionNames, oaversion);
562         props.set(ITEMS_KEY, itemsVal);
563
564         props.put(DESCRIPTION_KEY, listNode.getDescription().orElse(""));
565
566         return props;
567     }
568
569     private static void processElementCount(final Optional<ElementCountConstraint> constraint, final ObjectNode props) {
570         if (constraint.isPresent()) {
571             final ElementCountConstraint constr = constraint.get();
572             final Integer minElements = constr.getMinElements();
573             if (minElements != null) {
574                 props.put(MIN_ITEMS, minElements);
575             }
576             final Integer maxElements = constr.getMaxElements();
577             if (maxElements != null) {
578                 props.put(MAX_ITEMS, maxElements);
579             }
580         }
581     }
582
583     private static void processMandatory(final MandatoryAware node, final String nodeName, final ArrayNode required) {
584         if (node.isMandatory()) {
585             required.add(nodeName);
586         }
587     }
588
589     private ObjectNode processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
590                                        final ObjectNode properties, final ArrayNode required,
591                                        final SchemaInferenceStack stack, final ObjectNode definitions,
592                                        final DefinitionNames definitionNames, final OAversion oaversion) {
593         final ObjectNode property = JsonNodeFactory.instance.objectNode();
594
595         final String leafDescription = leafNode.getDescription().orElse("");
596         /*
597             Description can't be added, because nothing allowed alongside $ref.
598             allOf is not an option, because ServiceNow can't parse it.
599         */
600         if (!(leafNode.getType() instanceof IdentityrefTypeDefinition)) {
601             property.put(DESCRIPTION_KEY, leafDescription);
602         }
603
604         processTypeDef(leafNode.getType(), leafNode, property, stack, definitions, definitionNames, oaversion);
605         properties.set(jsonLeafName, property);
606         property.set(XML_KEY, buildXmlParameter(leafNode));
607         processMandatory(leafNode, jsonLeafName, required);
608
609         return property;
610     }
611
612     private static ObjectNode processAnydataNode(final AnydataSchemaNode leafNode, final String name,
613                                                  final ObjectNode properties, final ArrayNode required) {
614         final ObjectNode property = JsonNodeFactory.instance.objectNode();
615
616         final String leafDescription = leafNode.getDescription().orElse("");
617         property.put(DESCRIPTION_KEY, leafDescription);
618
619         final String localName = leafNode.getQName().getLocalName();
620         setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
621         property.put(TYPE_KEY, STRING_TYPE);
622         property.set(XML_KEY, buildXmlParameter(leafNode));
623         processMandatory(leafNode, name, required);
624         properties.set(name, property);
625
626         return property;
627     }
628
629     private static ObjectNode processAnyXMLNode(final AnyxmlSchemaNode leafNode, final String name,
630                                                 final ObjectNode properties, final ArrayNode required) {
631         final ObjectNode property = JsonNodeFactory.instance.objectNode();
632
633         final String leafDescription = leafNode.getDescription().orElse("");
634         property.put(DESCRIPTION_KEY, leafDescription);
635
636         final String localName = leafNode.getQName().getLocalName();
637         setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
638         property.put(TYPE_KEY, STRING_TYPE);
639         property.set(XML_KEY, buildXmlParameter(leafNode));
640         processMandatory(leafNode, name, required);
641         properties.set(name, property);
642
643         return property;
644     }
645
646     private String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
647                                   final ObjectNode property, final SchemaInferenceStack stack,
648                                   final ObjectNode definitions, final DefinitionNames definitionNames,
649                                   final OAversion oaversion) {
650         final String jsonType;
651         if (leafTypeDef instanceof BinaryTypeDefinition) {
652             jsonType = processBinaryType(property);
653
654         } else if (leafTypeDef instanceof BitsTypeDefinition) {
655             jsonType = processBitsType((BitsTypeDefinition) leafTypeDef, property);
656
657         } else if (leafTypeDef instanceof EnumTypeDefinition) {
658             jsonType = processEnumType((EnumTypeDefinition) leafTypeDef, property);
659
660         } else if (leafTypeDef instanceof IdentityrefTypeDefinition) {
661             jsonType = processIdentityRefType((IdentityrefTypeDefinition) leafTypeDef, property, definitions,
662                     definitionNames, oaversion, stack.getEffectiveModelContext());
663
664         } else if (leafTypeDef instanceof StringTypeDefinition) {
665             jsonType = processStringType(leafTypeDef, property, node.getQName().getLocalName());
666
667         } else if (leafTypeDef instanceof UnionTypeDefinition) {
668             jsonType = processUnionType((UnionTypeDefinition) leafTypeDef);
669
670         } else if (leafTypeDef instanceof EmptyTypeDefinition) {
671             jsonType = OBJECT_TYPE;
672         } else if (leafTypeDef instanceof LeafrefTypeDefinition) {
673             return processTypeDef(stack.resolveLeafref((LeafrefTypeDefinition) leafTypeDef), node, property,
674                 stack, definitions, definitionNames, oaversion);
675         } else if (leafTypeDef instanceof BooleanTypeDefinition) {
676             jsonType = BOOLEAN_TYPE;
677             setDefaultValue(property, true);
678         } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
679             jsonType = processNumberType((RangeRestrictedTypeDefinition<?, ?>) leafTypeDef, property);
680         } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
681             jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
682         } else {
683             jsonType = STRING_TYPE;
684         }
685         if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
686             putIfNonNull(property, TYPE_KEY, jsonType);
687             if (leafTypeDef.getDefaultValue().isPresent()) {
688                 final Object defaultValue = leafTypeDef.getDefaultValue().get();
689                 if (defaultValue instanceof String stringDefaultValue) {
690                     if (leafTypeDef instanceof BooleanTypeDefinition) {
691                         setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
692                     } else if (leafTypeDef instanceof DecimalTypeDefinition
693                             || leafTypeDef instanceof Uint64TypeDefinition) {
694                         setDefaultValue(property, new BigDecimal(stringDefaultValue));
695                     } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
696                         //uint8,16,32 int8,16,32,64
697                         if (isHexadecimalOrOctal((RangeRestrictedTypeDefinition<?, ?>)leafTypeDef)) {
698                             setDefaultValue(property, stringDefaultValue);
699                         } else {
700                             setDefaultValue(property, Long.valueOf(stringDefaultValue));
701                         }
702                     } else {
703                         setDefaultValue(property, stringDefaultValue);
704                     }
705                 } else {
706                     //we should never get here. getDefaultValue always gives us string
707                     setDefaultValue(property, defaultValue.toString());
708                 }
709             }
710         }
711         return jsonType;
712     }
713
714     private static String processBinaryType(final ObjectNode property) {
715         property.put(FORMAT_KEY, "byte");
716         return STRING_TYPE;
717     }
718
719     private static String processEnumType(final EnumTypeDefinition enumLeafType,
720                                           final ObjectNode property) {
721         final List<EnumPair> enumPairs = enumLeafType.getValues();
722         final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
723         for (final EnumPair enumPair : enumPairs) {
724             enumNames.add(new TextNode(enumPair.getName()));
725         }
726
727         property.set(ENUM_KEY, enumNames);
728         setDefaultValue(property, enumLeafType.getValues().iterator().next().getName());
729         return STRING_TYPE;
730     }
731
732     private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
733                                           final ObjectNode definitions, final DefinitionNames definitionNames,
734                                           final OAversion oaversion, final EffectiveModelContext schemaContext) {
735         final String definitionName;
736         if (isImported(leafTypeDef)) {
737             definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
738         } else {
739             final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
740             definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
741         }
742         property.put(REF_KEY, getAppropriateModelPrefix(oaversion) + definitionName);
743         return STRING_TYPE;
744     }
745
746     private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
747                                               final ObjectNode definitions, final DefinitionNames definitionNames,
748                                               final EffectiveModelContext context) {
749         final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
750         final String identityName = idNode.getQName().getLocalName();
751         if (!definitionNames.isListedNode(idNode)) {
752             final ObjectNode identityObj = buildIdentityObject(idNode, context);
753             final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
754             final String name = identityName + discriminator;
755             definitions.set(name, identityObj);
756             return name;
757         } else {
758             return identityName + definitionNames.getDiscriminator(idNode);
759         }
760     }
761
762     private static ObjectNode buildIdentityObject(final IdentitySchemaNode idNode,
763             final EffectiveModelContext context) {
764         final ObjectNode identityObj = JsonNodeFactory.instance.objectNode();
765         final String identityName = idNode.getQName().getLocalName();
766         LOG.debug("Processing Identity: {}", identityName);
767
768         identityObj.put(TITLE_KEY, identityName);
769         identityObj.put(DESCRIPTION_KEY, idNode.getDescription().orElse(""));
770
771         final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
772
773         final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
774         enumPayload.add(identityName);
775         populateEnumWithDerived(derivedIds, enumPayload, context);
776         identityObj.set(ENUM_KEY, enumPayload);
777         identityObj.put(TYPE_KEY, STRING_TYPE);
778         return identityObj;
779     }
780
781     private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
782         return !leafTypeDef.getQName().getModule().equals(topLevelModule.getQNameModule());
783     }
784
785     private static String processBitsType(final BitsTypeDefinition bitsType,
786                                           final ObjectNode property) {
787         property.put(MIN_ITEMS, 0);
788         property.put(UNIQUE_ITEMS_KEY, true);
789         final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
790         final Collection<? extends Bit> bits = bitsType.getBits();
791         for (final Bit bit : bits) {
792             enumNames.add(new TextNode(bit.getName()));
793         }
794         property.set(ENUM_KEY, enumNames);
795         property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
796         return STRING_TYPE;
797     }
798
799     private static String processStringType(final TypeDefinition<?> stringType, final ObjectNode property,
800                                             final String nodeName) {
801         StringTypeDefinition type = (StringTypeDefinition) stringType;
802         Optional<LengthConstraint> lengthConstraints = ((StringTypeDefinition) stringType).getLengthConstraint();
803         while (lengthConstraints.isEmpty() && type.getBaseType() != null) {
804             type = type.getBaseType();
805             lengthConstraints = type.getLengthConstraint();
806         }
807
808         if (lengthConstraints.isPresent()) {
809             final Range<Integer> range = lengthConstraints.get().getAllowedRanges().span();
810             putIfNonNull(property, MIN_LENGTH_KEY, range.lowerEndpoint());
811             putIfNonNull(property, MAX_LENGTH_KEY, range.upperEndpoint());
812         }
813
814         if (type.getPatternConstraints().iterator().hasNext()) {
815             final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
816             String regex = pattern.getJavaPatternString();
817             regex = regex.substring(1, regex.length() - 1);
818             // Escape special characters to prevent issues inside Generex.
819             regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
820             String defaultValue = "";
821             try {
822                 final Generex generex = new Generex(regex);
823                 defaultValue = generex.random();
824             } catch (IllegalArgumentException ex) {
825                 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
826             }
827             setDefaultValue(property, defaultValue);
828         } else {
829             setDefaultValue(property, "Some " + nodeName);
830         }
831         return STRING_TYPE;
832     }
833
834     private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
835             final ObjectNode property) {
836         final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
837                 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
838
839         if (isHexadecimalOrOctal(leafTypeDef)) {
840             return STRING_TYPE;
841         }
842
843         if (leafTypeDef instanceof DecimalTypeDefinition) {
844             maybeLower.ifPresent(number -> setDefaultValue(property, ((Decimal64) number).decimalValue()));
845             return NUMBER_TYPE;
846         }
847         if (leafTypeDef instanceof Uint8TypeDefinition
848                 || leafTypeDef instanceof Uint16TypeDefinition
849                 || leafTypeDef instanceof Int8TypeDefinition
850                 || leafTypeDef instanceof Int16TypeDefinition
851                 || leafTypeDef instanceof Int32TypeDefinition) {
852
853             property.put(FORMAT_KEY, INT32_FORMAT);
854             maybeLower.ifPresent(number -> setDefaultValue(property, Integer.valueOf(number.toString())));
855         } else if (leafTypeDef instanceof Uint32TypeDefinition
856                 || leafTypeDef instanceof Int64TypeDefinition) {
857
858             property.put(FORMAT_KEY, INT64_FORMAT);
859             maybeLower.ifPresent(number -> setDefaultValue(property, Long.valueOf(number.toString())));
860         } else {
861             //uint64
862             setDefaultValue(property, 0);
863         }
864         return INTEGER_TYPE;
865     }
866
867     private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
868         final Optional<?> optDefaultValue = typeDef.getDefaultValue();
869         if (optDefaultValue.isPresent()) {
870             final String defaultValue = (String) optDefaultValue.get();
871             return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
872         }
873         return false;
874     }
875
876     private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
877                                                         final EffectiveModelContext schemaContext) {
878         // create example instance-identifier to the first container of node's module if exists or leave it empty
879         final var module = schemaContext.findModule(node.getQName().getModule());
880         if (module.isPresent()) {
881             final var container = module.get().getChildNodes().stream()
882                     .filter(n -> n instanceof ContainerSchemaNode)
883                     .findFirst();
884             container.ifPresent(c -> setDefaultValue(property, String.format("/%s:%s", module.get().getPrefix(),
885                     c.getQName().getLocalName())));
886         }
887
888         return STRING_TYPE;
889     }
890
891     private static String processUnionType(final UnionTypeDefinition unionType) {
892         boolean isStringTakePlace = false;
893         boolean isNumberTakePlace = false;
894         boolean isBooleanTakePlace = false;
895         for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
896             if (!isStringTakePlace) {
897                 if (typeDef instanceof StringTypeDefinition
898                         || typeDef instanceof BitsTypeDefinition
899                         || typeDef instanceof BinaryTypeDefinition
900                         || typeDef instanceof IdentityrefTypeDefinition
901                         || typeDef instanceof EnumTypeDefinition
902                         || typeDef instanceof LeafrefTypeDefinition
903                         || typeDef instanceof UnionTypeDefinition) {
904                     isStringTakePlace = true;
905                 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
906                     isNumberTakePlace = true;
907                 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
908                     isBooleanTakePlace = true;
909                 }
910             }
911         }
912         if (isStringTakePlace) {
913             return STRING_TYPE;
914         }
915         if (isBooleanTakePlace) {
916             if (isNumberTakePlace) {
917                 return STRING_TYPE;
918             }
919             return BOOLEAN_TYPE;
920         }
921         return NUMBER_TYPE;
922     }
923
924     private static ObjectNode buildXmlParameter(final SchemaNode node) {
925         final ObjectNode xml = JsonNodeFactory.instance.objectNode();
926         final QName qName = node.getQName();
927         xml.put(NAME_KEY, qName.getLocalName());
928         xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
929         return xml;
930     }
931
932     private static void putIfNonNull(final ObjectNode property, final String key, final Number number) {
933         if (key != null && number != null) {
934             if (number instanceof Double) {
935                 property.put(key, (Double) number);
936             } else if (number instanceof Float) {
937                 property.put(key, (Float) number);
938             } else if (number instanceof Integer) {
939                 property.put(key, (Integer) number);
940             } else if (number instanceof Short) {
941                 property.put(key, (Short) number);
942             } else if (number instanceof Long) {
943                 property.put(key, (Long) number);
944             }
945         }
946     }
947
948     private static void putIfNonNull(final ObjectNode property, final String key, final String value) {
949         if (key != null && value != null) {
950             property.put(key, value);
951         }
952     }
953
954     private static void setRequiredIfNotEmpty(final ObjectNode node, final ArrayNode required) {
955         if (required.size() > 0) {
956             node.set(REQUIRED_KEY, required);
957         }
958     }
959
960     private static void setDefaultValue(final ObjectNode property, final String value) {
961         property.put(DEFAULT_KEY, value);
962     }
963
964     private static void setDefaultValue(final ObjectNode property, final Integer value) {
965         property.put(DEFAULT_KEY, value);
966     }
967
968     private static void setDefaultValue(final ObjectNode property, final Long value) {
969         property.put(DEFAULT_KEY, value);
970     }
971
972     private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
973         property.put(DEFAULT_KEY, value);
974     }
975
976     private static void setDefaultValue(final ObjectNode property, final Boolean value) {
977         property.put(DEFAULT_KEY, value);
978     }
979
980 }