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