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