Set default for boolean type
[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.CONFIG;
13 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.NAME_KEY;
14 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.TOP;
15 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.XML_KEY;
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 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 Module topLevelModule;
130
131     public DefinitionGenerator() {
132     }
133
134     /**
135      * Creates Json definitions from provided module according to openapi 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 {@link Map} containing data used for creating examples and definitions in OpenAPI documentation
141      * @throws IOException if I/O operation fails
142      */
143     public Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
144             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
145             final boolean isForSingleModule) throws IOException {
146         topLevelModule = module;
147
148         processIdentities(module, definitions, definitionNames, schemaContext);
149         processContainersAndLists(module, definitions, definitionNames, schemaContext);
150         processRPCs(module, definitions, definitionNames, schemaContext);
151
152         if (isForSingleModule) {
153             processModule(module, definitions, definitionNames, schemaContext);
154         }
155
156         return definitions;
157     }
158
159     public Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
160             final DefinitionNames definitionNames, final boolean isForSingleModule)
161             throws IOException {
162         final Map<String, Schema> definitions = new HashMap<>();
163         if (isForSingleModule) {
164             definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
165         }
166         return convertToSchemas(module, schemaContext, definitions, definitionNames, isForSingleModule);
167     }
168
169     private void processModule(final Module module, final Map<String, Schema> definitions,
170             final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) {
171         final ObjectNode properties = JsonNodeFactory.instance.objectNode();
172         final ArrayNode required = JsonNodeFactory.instance.arrayNode();
173         final String moduleName = module.getName();
174         final String definitionName = moduleName + MODULE_NAME_SUFFIX;
175         final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
176         for (final DataSchemaNode node : module.getChildNodes()) {
177             stack.enterSchemaTree(node.getQName());
178             final String localName = node.getQName().getLocalName();
179             if (node.isConfiguration()) {
180                 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
181                     if (isSchemaNodeMandatory(node)) {
182                         required.add(localName);
183                     }
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                     final ObjectNode leafNode = processLeafNode((LeafSchemaNode) node, localName, required, stack,
215                             definitions, definitionNames, module.getNamespace());
216                     properties.set(localName, leafNode);
217                 }
218             }
219             stack.exit();
220         }
221         final Schema.Builder definitionBuilder = new Schema.Builder()
222             .title(definitionName)
223             .type(OBJECT_TYPE)
224             .properties(properties)
225             .description(module.getDescription().orElse(""))
226             .required(required.size() > 0 ? required : null);
227
228         definitions.put(definitionName, definitionBuilder.build());
229     }
230
231     private static boolean isSchemaNodeMandatory(final DataSchemaNode node) {
232         //    https://www.rfc-editor.org/rfc/rfc7950#page-14
233         //    mandatory node: A mandatory node is one of:
234         if (node instanceof ContainerSchemaNode containerNode) {
235             //  A container node without a "presence" statement and that has at least one mandatory node as a child.
236             if (containerNode.isPresenceContainer()) {
237                 return false;
238             }
239             for (final DataSchemaNode childNode : containerNode.getChildNodes()) {
240                 if (childNode instanceof MandatoryAware mandatoryAware && mandatoryAware.isMandatory()) {
241                     return true;
242                 }
243             }
244         }
245         //  A list or leaf-list node with a "min-elements" statement with a value greater than zero.
246         return node instanceof ElementCountConstraintAware constraintAware
247                 && constraintAware.getElementCountConstraint()
248                 .map(ElementCountConstraint::getMinElements)
249                 .orElse(0)
250                 > 0;
251     }
252
253     private void processContainersAndLists(final Module module, final Map<String, Schema> definitions,
254             final DefinitionNames definitionNames, final EffectiveModelContext schemaContext)  throws IOException {
255         final String moduleName = module.getName();
256         final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
257         for (final DataSchemaNode childNode : module.getChildNodes()) {
258             stack.enterSchemaTree(childNode.getQName());
259             // For every container and list in the module
260             if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
261                 if (childNode.isConfiguration()) {
262                     processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
263                         stack);
264                 }
265                 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack);
266             }
267             stack.exit();
268         }
269     }
270
271     private void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
272             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
273             final SchemaInferenceStack stack) throws IOException {
274         for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
275             stack.enterSchemaTree(actionDef.getQName());
276             processOperations(actionDef, moduleName, definitions, definitionNames, stack);
277             stack.exit();
278         }
279     }
280
281     private void processRPCs(final Module module, final Map<String, Schema> definitions,
282             final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
283         final String moduleName = module.getName();
284         final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
285         for (final RpcDefinition rpcDefinition : module.getRpcs()) {
286             stack.enterSchemaTree(rpcDefinition.getQName());
287             processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack);
288             stack.exit();
289         }
290     }
291
292     private void processOperations(final OperationDefinition operationDef, final String parentName,
293             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
294             final SchemaInferenceStack stack) throws IOException {
295         final String operationName = operationDef.getQName().getLocalName();
296         processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
297                 definitionNames, stack);
298         processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
299                 definitionNames, stack);
300     }
301
302     private void processOperationInputOutput(final ContainerLike container, final String operationName,
303             final String parentName, final boolean isInput, final Map<String, Schema> definitions,
304             final DefinitionNames definitionNames, final SchemaInferenceStack stack)
305             throws IOException {
306         stack.enterSchemaTree(container.getQName());
307         if (!container.getChildNodes().isEmpty()) {
308             final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
309             final Schema.Builder childSchemaBuilder = new Schema.Builder()
310                 .title(filename)
311                 .type(OBJECT_TYPE)
312                 .xml(JsonNodeFactory.instance.objectNode().put(NAME_KEY, isInput ? INPUT : OUTPUT));
313             processChildren(childSchemaBuilder, container.getChildNodes(), parentName, definitions, definitionNames,
314                 stack);
315             final String discriminator =
316                 definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
317             definitions.put(filename + discriminator, childSchemaBuilder.build());
318             processTopData(filename, discriminator, definitions, container);
319         }
320         stack.exit();
321     }
322
323     private static ObjectNode processTopData(final String filename, final String discriminator,
324             final Map<String, Schema> definitions, final SchemaNode schemaNode) {
325         final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
326         final String name = filename + discriminator;
327         final String ref = COMPONENTS_PREFIX + name;
328         final String topName = filename + TOP;
329
330         if (schemaNode instanceof ListSchemaNode) {
331             dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
332             final ObjectNode items = JsonNodeFactory.instance.objectNode();
333             items.put(REF_KEY, ref);
334             dataNodeProperties.set(ITEMS_KEY, items);
335             dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
336         } else {
337              /*
338                 Description can't be added, because nothing allowed alongside $ref.
339                 allOf is not an option, because ServiceNow can't parse it.
340               */
341             dataNodeProperties.put(REF_KEY, ref);
342         }
343
344         final ObjectNode properties = JsonNodeFactory.instance.objectNode();
345         /*
346             Add module name prefix to property name, when needed, when ServiceNow can process colons,
347             use RestDocGenUtil#resolveNodesName for creating property name
348          */
349         properties.set(schemaNode.getQName().getLocalName(), dataNodeProperties);
350         final var schema = new Schema.Builder()
351             .type(OBJECT_TYPE)
352             .properties(properties)
353             .title(topName)
354             .build();
355
356         definitions.put(topName + discriminator, schema);
357
358         return dataNodeProperties;
359     }
360
361     /**
362      * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
363      * @param module          The module from which the identity stmt will be processed
364      * @param definitions     The ObjectNode in which the parsed identity will be put as a 'model' obj
365      * @param definitionNames Store for definition names
366      */
367     private static void processIdentities(final Module module, final Map<String, Schema> definitions,
368             final DefinitionNames definitionNames, final EffectiveModelContext context) {
369         final String moduleName = module.getName();
370         final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
371         LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
372
373         for (final IdentitySchemaNode idNode : idNodes) {
374             final Schema identityObj = buildIdentityObject(idNode, context);
375             final String idName = idNode.getQName().getLocalName();
376             final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
377             final String name = idName + discriminator;
378             definitions.put(name, identityObj);
379         }
380     }
381
382     private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
383             final ArrayNode enumPayload, final EffectiveModelContext context) {
384         for (final IdentitySchemaNode derivedId : derivedIds) {
385             enumPayload.add(derivedId.getQName().getLocalName());
386             populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
387         }
388     }
389
390     private ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
391             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
392             final SchemaInferenceStack stack) throws IOException {
393         final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
394         final SchemaNode schemaNode = (SchemaNode) dataNode;
395         final String localName = schemaNode.getQName().getLocalName();
396         final String nodeName = parentName + CONFIG + "_" + localName;
397         final Schema.Builder childSchemaBuilder = new Schema.Builder()
398             .type(OBJECT_TYPE)
399             .title(nodeName)
400             .description(schemaNode.getDescription().orElse(""));
401
402         childSchemaBuilder.properties(processChildren(childSchemaBuilder, containerChildren,
403             parentName + "_" + localName, definitions, definitionNames, stack));
404
405         final String discriminator;
406         if (!definitionNames.isListedNode(schemaNode)) {
407             final String parentNameConfigLocalName = parentName + CONFIG + "_" + localName;
408             final String nameAsParent = parentName + "_" + localName;
409             final List<String> names = List.of(parentNameConfigLocalName, parentNameConfigLocalName + TOP,
410                 nameAsParent, nameAsParent + TOP);
411             discriminator = definitionNames.pickDiscriminator(schemaNode, names);
412         } else {
413             discriminator = definitionNames.getDiscriminator(schemaNode);
414         }
415
416         final String defName = nodeName + discriminator;
417         childSchemaBuilder.xml(buildXmlParameter(schemaNode));
418         definitions.put(defName, childSchemaBuilder.build());
419
420         return processTopData(nodeName, discriminator, definitions, schemaNode);
421     }
422
423     /**
424      * Processes the nodes.
425      */
426     private ObjectNode processChildren(final Schema.Builder parentNodeBuilder,
427             final Collection<? extends DataSchemaNode> nodes, final String parentName,
428             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
429             final SchemaInferenceStack stack) throws IOException {
430         final ObjectNode properties = JsonNodeFactory.instance.objectNode();
431         final ArrayNode required = JsonNodeFactory.instance.arrayNode();
432         for (final DataSchemaNode node : nodes) {
433             if (node.isConfiguration()) {
434                 if (node instanceof ChoiceSchemaNode choice) {
435                     stack.enterSchemaTree(node.getQName());
436                     final Map<String, ObjectNode> choiceProperties = processChoiceNodeRecursively(parentName,
437                         definitions, definitionNames, stack, required, choice);
438                     choiceProperties.forEach(properties::set);
439                     stack.exit();
440                 } else {
441                     final ObjectNode property = processChildNode(node, parentName, definitions, definitionNames,
442                         stack, required);
443                     properties.set(node.getQName().getLocalName(), property);
444                 }
445             }
446         }
447         parentNodeBuilder.properties(properties).required(required.size() > 0 ? required : null);
448         return properties;
449     }
450
451     private Map<String, ObjectNode> processChoiceNodeRecursively(final String parentName,
452             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
453             final SchemaInferenceStack stack, final ArrayNode required, final ChoiceSchemaNode choice)
454             throws IOException {
455         if (!choice.getCases().isEmpty()) {
456             final var properties = new HashMap<String, ObjectNode>();
457             final var caseSchemaNode = choice.getDefaultCase().orElse(choice.getCases().stream()
458                 .findFirst().orElseThrow());
459             stack.enterSchemaTree(caseSchemaNode.getQName());
460             for (final var childNode : caseSchemaNode.getChildNodes()) {
461                 if (childNode instanceof ChoiceSchemaNode childChoice) {
462                     stack.enterSchemaTree(childNode.getQName());
463                     final var childProperties = processChoiceNodeRecursively(parentName, definitions, definitionNames,
464                         stack, required, childChoice);
465                     properties.putAll(childProperties);
466                     stack.exit();
467                 } else {
468                     final var property = processChildNode(childNode, parentName, definitions, definitionNames, stack,
469                         required);
470                     properties.put(childNode.getQName().getLocalName(), property);
471                 }
472             }
473             stack.exit();
474             return properties;
475         }
476         return Map.of();
477     }
478
479     private ObjectNode processChildNode(final DataSchemaNode node, final String parentName,
480             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
481             final SchemaInferenceStack stack, final ArrayNode required) throws IOException {
482         final XMLNamespace parentNamespace = stack.toSchemaNodeIdentifier().lastNodeIdentifier().getNamespace();
483         stack.enterSchemaTree(node.getQName());
484         /*
485             Add module name prefix to property name, when needed, when ServiceNow can process colons,
486             use RestDocGenUtil#resolveNodesName for creating property name
487          */
488         final String name = node.getQName().getLocalName();
489         final ObjectNode property;
490         if (node instanceof LeafSchemaNode leaf) {
491             property = processLeafNode(leaf, name, required, stack, definitions, definitionNames, parentNamespace);
492         } else if (node instanceof AnyxmlSchemaNode anyxml) {
493             property = processAnyXMLNode(anyxml, name, required, parentNamespace);
494         } else if (node instanceof AnydataSchemaNode anydata) {
495             property = processAnydataNode(anydata, name, required, parentNamespace);
496         } else if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
497             if (isSchemaNodeMandatory(node)) {
498                 required.add(name);
499             }
500             property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions, definitionNames,
501                 stack);
502             processActionNodeContainer(node, parentName, definitions, definitionNames, stack);
503         } else if (node instanceof LeafListSchemaNode leafList) {
504             if (isSchemaNodeMandatory(node)) {
505                 required.add(name);
506             }
507             property = processLeafListNode(leafList, stack, definitions, definitionNames);
508         } else {
509             throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
510         }
511         stack.exit();
512         return property;
513     }
514
515     private ObjectNode processLeafListNode(final LeafListSchemaNode listNode, final SchemaInferenceStack stack,
516             final Map<String, Schema> definitions, final DefinitionNames definitionNames) {
517         final ObjectNode props = JsonNodeFactory.instance.objectNode();
518         props.put(TYPE_KEY, ARRAY_TYPE);
519
520         final ObjectNode itemsVal = JsonNodeFactory.instance.objectNode();
521         final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
522         optConstraint.ifPresent(elementCountConstraint -> processElementCount(elementCountConstraint, props));
523
524         processTypeDef(listNode.getType(), listNode, itemsVal, stack, definitions, definitionNames);
525         props.set(ITEMS_KEY, itemsVal);
526
527         props.put(DESCRIPTION_KEY, listNode.getDescription().orElse(""));
528
529         return props;
530     }
531
532     private static void processElementCount(final ElementCountConstraint constraint, final ObjectNode props) {
533         final Integer minElements = constraint.getMinElements();
534         if (minElements != null) {
535             props.put(MIN_ITEMS, minElements);
536         }
537         final Integer maxElements = constraint.getMaxElements();
538         if (maxElements != null) {
539             props.put(MAX_ITEMS, maxElements);
540         }
541     }
542
543     private static void processMandatory(final MandatoryAware node, final String nodeName, final ArrayNode required) {
544         if (node.isMandatory()) {
545             required.add(nodeName);
546         }
547     }
548
549     private ObjectNode processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
550             final ArrayNode required, final SchemaInferenceStack stack, final Map<String, Schema> definitions,
551             final DefinitionNames definitionNames, final XMLNamespace parentNamespace) {
552         final ObjectNode property = JsonNodeFactory.instance.objectNode();
553
554         final String leafDescription = leafNode.getDescription().orElse("");
555         /*
556             Description can't be added, because nothing allowed alongside $ref.
557             allOf is not an option, because ServiceNow can't parse it.
558         */
559         if (!(leafNode.getType() instanceof IdentityrefTypeDefinition)) {
560             property.put(DESCRIPTION_KEY, leafDescription);
561         }
562
563         processTypeDef(leafNode.getType(), leafNode, property, stack, definitions, definitionNames);
564         if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
565             // If the parent is not from the same model, define the child XML namespace.
566             property.set(XML_KEY, buildXmlParameter(leafNode));
567         }
568         processMandatory(leafNode, jsonLeafName, required);
569         return property;
570     }
571
572     private static ObjectNode processAnydataNode(final AnydataSchemaNode leafNode, final String name,
573             final ArrayNode required, final XMLNamespace parentNamespace) {
574         final ObjectNode property = JsonNodeFactory.instance.objectNode();
575
576         final String leafDescription = leafNode.getDescription().orElse("");
577         property.put(DESCRIPTION_KEY, leafDescription);
578
579         final String localName = leafNode.getQName().getLocalName();
580         setExampleValue(property, String.format("<%s> ... </%s>", localName, localName));
581         property.put(TYPE_KEY, STRING_TYPE);
582         if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
583             // If the parent is not from the same model, define the child XML namespace.
584             property.set(XML_KEY, buildXmlParameter(leafNode));
585         }
586         processMandatory(leafNode, name, required);
587         return property;
588     }
589
590     private static ObjectNode processAnyXMLNode(final AnyxmlSchemaNode leafNode, final String name,
591             final ArrayNode required, final XMLNamespace parentNamespace) {
592         final ObjectNode property = JsonNodeFactory.instance.objectNode();
593
594         final String leafDescription = leafNode.getDescription().orElse("");
595         property.put(DESCRIPTION_KEY, leafDescription);
596
597         final String localName = leafNode.getQName().getLocalName();
598         setExampleValue(property, String.format("<%s> ... </%s>", localName, localName));
599         property.put(TYPE_KEY, STRING_TYPE);
600         if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
601             // If the parent is not from the same model, define the child XML namespace.
602             property.set(XML_KEY, buildXmlParameter(leafNode));
603         }
604         processMandatory(leafNode, name, required);
605         return property;
606     }
607
608     private String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
609             final ObjectNode property, final SchemaInferenceStack stack, final Map<String, Schema> definitions,
610             final DefinitionNames definitionNames) {
611         final String jsonType;
612         if (leafTypeDef instanceof BinaryTypeDefinition binaryType) {
613             jsonType = processBinaryType(binaryType, property);
614         } else if (leafTypeDef instanceof BitsTypeDefinition bitsType) {
615             jsonType = processBitsType(bitsType, property);
616         } else if (leafTypeDef instanceof EnumTypeDefinition enumType) {
617             jsonType = processEnumType(enumType, property);
618         } else if (leafTypeDef instanceof IdentityrefTypeDefinition identityrefType) {
619             jsonType = processIdentityRefType(identityrefType, property, definitions,
620                     definitionNames, stack.getEffectiveModelContext());
621         } else if (leafTypeDef instanceof StringTypeDefinition stringType) {
622             jsonType = processStringType(stringType, property, node.getQName().getLocalName());
623         } else if (leafTypeDef instanceof UnionTypeDefinition unionType) {
624             jsonType = processUnionType(unionType, property, node.getQName().getLocalName());
625         } else if (leafTypeDef instanceof EmptyTypeDefinition) {
626             jsonType = OBJECT_TYPE;
627         } else if (leafTypeDef instanceof LeafrefTypeDefinition leafrefType) {
628             return processTypeDef(stack.resolveLeafref(leafrefType), node, property,
629                 stack, definitions, definitionNames);
630         } else if (leafTypeDef instanceof BooleanTypeDefinition) {
631             jsonType = BOOLEAN_TYPE;
632             leafTypeDef.getDefaultValue().ifPresent(v -> setDefaultValue(property, Boolean.valueOf((String) v)));
633             setExampleValue(property, true);
634         } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
635             jsonType = processNumberType(rangeRestrictedType, property);
636         } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
637             jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
638         } else {
639             jsonType = STRING_TYPE;
640         }
641         if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
642             if (TYPE_KEY != null && jsonType != null) {
643                 property.put(TYPE_KEY, jsonType);
644             }
645             if (leafTypeDef.getDefaultValue().isPresent()) {
646                 final Object defaultValue = leafTypeDef.getDefaultValue().orElseThrow();
647                 if (defaultValue instanceof String stringDefaultValue) {
648                     if (leafTypeDef instanceof BooleanTypeDefinition) {
649                         setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
650                     } else if (leafTypeDef instanceof DecimalTypeDefinition
651                             || leafTypeDef instanceof Uint64TypeDefinition) {
652                         setDefaultValue(property, new BigDecimal(stringDefaultValue));
653                     } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
654                         //uint8,16,32 int8,16,32,64
655                         if (isHexadecimalOrOctal(rangeRestrictedType)) {
656                             setDefaultValue(property, stringDefaultValue);
657                         } else {
658                             setDefaultValue(property, Long.valueOf(stringDefaultValue));
659                         }
660                     } else {
661                         setDefaultValue(property, stringDefaultValue);
662                     }
663                 } else {
664                     //we should never get here. getDefaultValue always gives us string
665                     setDefaultValue(property, defaultValue.toString());
666                 }
667             }
668         }
669         return jsonType;
670     }
671
672     private static String processBinaryType(final BinaryTypeDefinition definition, final ObjectNode property) {
673         definition.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
674         property.put(FORMAT_KEY, "byte");
675         return STRING_TYPE;
676     }
677
678     private static String processEnumType(final EnumTypeDefinition enumLeafType, final ObjectNode property) {
679         final List<EnumPair> enumPairs = enumLeafType.getValues();
680         final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
681         for (final EnumPair enumPair : enumPairs) {
682             enumNames.add(new TextNode(enumPair.getName()));
683         }
684
685         property.set(ENUM_KEY, enumNames);
686         enumLeafType.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
687         setExampleValue(property, enumLeafType.getValues().iterator().next().getName());
688         return STRING_TYPE;
689     }
690
691     private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
692             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
693             final EffectiveModelContext schemaContext) {
694         final String definitionName;
695         if (isImported(leafTypeDef)) {
696             definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
697         } else {
698             final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
699             definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
700         }
701         property.put(REF_KEY, COMPONENTS_PREFIX + definitionName);
702         return STRING_TYPE;
703     }
704
705     private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
706             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
707             final EffectiveModelContext context) {
708         final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
709         final String identityName = idNode.getQName().getLocalName();
710         if (!definitionNames.isListedNode(idNode)) {
711             final Schema identityObj = buildIdentityObject(idNode, context);
712             final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
713             final String name = identityName + discriminator;
714             definitions.put(name, identityObj);
715             return name;
716         } else {
717             return identityName + definitionNames.getDiscriminator(idNode);
718         }
719     }
720
721     private static Schema buildIdentityObject(final IdentitySchemaNode idNode, final EffectiveModelContext context) {
722         final String identityName = idNode.getQName().getLocalName();
723         LOG.debug("Processing Identity: {}", identityName);
724
725         final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
726         final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
727         enumPayload.add(identityName);
728         populateEnumWithDerived(derivedIds, enumPayload, context);
729
730         return new Schema.Builder()
731             .title(identityName)
732             .description(idNode.getDescription().orElse(""))
733             .schemaEnum(enumPayload)
734             .type(STRING_TYPE)
735             .build();
736     }
737
738     private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
739         return !leafTypeDef.getQName().getModule().equals(topLevelModule.getQNameModule());
740     }
741
742     private static String processBitsType(final BitsTypeDefinition bitsType, final ObjectNode property) {
743         property.put(MIN_ITEMS, 0);
744         property.put(UNIQUE_ITEMS_KEY, true);
745         final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
746         final Collection<? extends Bit> bits = bitsType.getBits();
747         for (final Bit bit : bits) {
748             enumNames.add(new TextNode(bit.getName()));
749         }
750         property.set(ENUM_KEY, enumNames);
751         property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
752         return STRING_TYPE;
753     }
754
755     private static String processStringType(final StringTypeDefinition stringType, final ObjectNode property,
756             final String nodeName) {
757         var type = stringType;
758         while (type.getLengthConstraint().isEmpty() && type.getBaseType() != null) {
759             type = type.getBaseType();
760         }
761
762         type.getLengthConstraint().ifPresent(constraint -> {
763             final Range<Integer> range = constraint.getAllowedRanges().span();
764             putIfNonNull(property, MIN_LENGTH_KEY, range.lowerEndpoint());
765             putIfNonNull(property, MAX_LENGTH_KEY, range.upperEndpoint());
766         });
767
768         if (type.getPatternConstraints().iterator().hasNext()) {
769             final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
770             String regex = pattern.getRegularExpressionString();
771             // Escape special characters to prevent issues inside Automaton.
772             regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
773             for (final var charClass : PREDEFINED_CHARACTER_CLASSES.entrySet()) {
774                 regex = regex.replaceAll(charClass.getKey(), charClass.getValue());
775             }
776             String defaultValue = "";
777             try {
778                 final RegExp regExp = new RegExp(regex);
779                 defaultValue = regExp.toAutomaton().getShortestExample(true);
780             } catch (IllegalArgumentException ex) {
781                 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
782             }
783             setExampleValue(property, defaultValue);
784         } else {
785             setExampleValue(property, "Some " + nodeName);
786         }
787         return STRING_TYPE;
788     }
789
790     private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
791             final ObjectNode property) {
792         final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
793                 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
794
795         if (isHexadecimalOrOctal(leafTypeDef)) {
796             return STRING_TYPE;
797         }
798
799         if (leafTypeDef instanceof DecimalTypeDefinition) {
800             maybeLower.ifPresent(number -> setExampleValue(property, ((Decimal64) number).decimalValue()));
801             return NUMBER_TYPE;
802         }
803         if (leafTypeDef instanceof Uint8TypeDefinition
804                 || leafTypeDef instanceof Uint16TypeDefinition
805                 || leafTypeDef instanceof Int8TypeDefinition
806                 || leafTypeDef instanceof Int16TypeDefinition
807                 || leafTypeDef instanceof Int32TypeDefinition) {
808
809             property.put(FORMAT_KEY, INT32_FORMAT);
810             maybeLower.ifPresent(number -> setExampleValue(property, Integer.valueOf(number.toString())));
811         } else if (leafTypeDef instanceof Uint32TypeDefinition
812                 || leafTypeDef instanceof Int64TypeDefinition) {
813
814             property.put(FORMAT_KEY, INT64_FORMAT);
815             maybeLower.ifPresent(number -> setExampleValue(property, Long.valueOf(number.toString())));
816         } else {
817             //uint64
818             setExampleValue(property, 0);
819         }
820         return INTEGER_TYPE;
821     }
822
823     private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
824         final Optional<?> optDefaultValue = typeDef.getDefaultValue();
825         if (optDefaultValue.isPresent()) {
826             final String defaultValue = (String) optDefaultValue.orElseThrow();
827             return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
828         }
829         return false;
830     }
831
832     private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
833             final EffectiveModelContext schemaContext) {
834         // create example instance-identifier to the first container of node's module if exists or leave it empty
835         final var module = schemaContext.findModule(node.getQName().getModule());
836         if (module.isPresent()) {
837             final var container = module.orElseThrow().getChildNodes().stream()
838                     .filter(n -> n instanceof ContainerSchemaNode)
839                     .findFirst();
840             container.ifPresent(c -> setExampleValue(property, String.format("/%s:%s", module.orElseThrow().getPrefix(),
841                     c.getQName().getLocalName())));
842         }
843
844         return STRING_TYPE;
845     }
846
847     private static String processUnionType(final UnionTypeDefinition unionType, final ObjectNode property,
848             final String nodeName) {
849         boolean isStringTakePlace = false;
850         boolean isNumberTakePlace = false;
851         boolean isBooleanTakePlace = false;
852         for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
853             if (!isStringTakePlace) {
854                 if (typeDef instanceof StringTypeDefinition
855                         || typeDef instanceof BitsTypeDefinition
856                         || typeDef instanceof BinaryTypeDefinition
857                         || typeDef instanceof IdentityrefTypeDefinition
858                         || typeDef instanceof EnumTypeDefinition
859                         || typeDef instanceof LeafrefTypeDefinition
860                         || typeDef instanceof UnionTypeDefinition) {
861                     isStringTakePlace = true;
862                 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
863                     isNumberTakePlace = true;
864                 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
865                     isBooleanTakePlace = true;
866                 }
867             }
868         }
869         if (isStringTakePlace) {
870             unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, (String) v));
871             setExampleValue(property, "Some " + nodeName);
872             return STRING_TYPE;
873         }
874         if (isBooleanTakePlace) {
875             if (isNumberTakePlace) {
876                 // FIXME deal with other number formats
877                 unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Long.valueOf((String) v)));
878                 setExampleValue(property, 0);
879                 return NUMBER_TYPE;
880             }
881             unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Boolean.valueOf((String) v)));
882             setExampleValue(property, true);
883             return BOOLEAN_TYPE;
884         }
885         // FIXME deal with other number formats
886         unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Long.valueOf((String) v)));
887         setExampleValue(property, 0);
888         return NUMBER_TYPE;
889     }
890
891     private static ObjectNode buildXmlParameter(final SchemaNode node) {
892         final ObjectNode xml = JsonNodeFactory.instance.objectNode();
893         final QName qName = node.getQName();
894         xml.put(NAME_KEY, qName.getLocalName());
895         xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
896         return xml;
897     }
898
899     private static void putIfNonNull(final ObjectNode property, final String key, final Number number) {
900         if (key != null && number != null) {
901             if (number instanceof Double) {
902                 property.put(key, (Double) number);
903             } else if (number instanceof Float) {
904                 property.put(key, (Float) number);
905             } else if (number instanceof Integer) {
906                 property.put(key, (Integer) number);
907             } else if (number instanceof Short) {
908                 property.put(key, (Short) number);
909             } else if (number instanceof Long) {
910                 property.put(key, (Long) number);
911             }
912         }
913     }
914
915     private static void setExampleValue(final ObjectNode property, final String value) {
916         property.put(EXAMPLE_KEY, value);
917     }
918
919     private static void setExampleValue(final ObjectNode property, final Integer value) {
920         property.put(EXAMPLE_KEY, value);
921     }
922
923     private static void setExampleValue(final ObjectNode property, final Long value) {
924         property.put(EXAMPLE_KEY, value);
925     }
926
927     private static void setExampleValue(final ObjectNode property, final BigDecimal value) {
928         property.put(EXAMPLE_KEY, value);
929     }
930
931     private static void setExampleValue(final ObjectNode property, final Boolean value) {
932         property.put(EXAMPLE_KEY, value);
933     }
934
935     private static void setDefaultValue(final ObjectNode property, final String value) {
936         property.put(DEFAULT_KEY, value);
937     }
938
939     private static void setDefaultValue(final ObjectNode property, final Long value) {
940         property.put(DEFAULT_KEY, value);
941     }
942
943     private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
944         property.put(DEFAULT_KEY, value);
945     }
946
947     private static void setDefaultValue(final ObjectNode property, final Boolean value) {
948         property.put(DEFAULT_KEY, value);
949     }
950
951 }