Set default for union 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             setExampleValue(property, true);
633         } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
634             jsonType = processNumberType(rangeRestrictedType, property);
635         } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
636             jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
637         } else {
638             jsonType = STRING_TYPE;
639         }
640         if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
641             if (TYPE_KEY != null && jsonType != null) {
642                 property.put(TYPE_KEY, jsonType);
643             }
644             if (leafTypeDef.getDefaultValue().isPresent()) {
645                 final Object defaultValue = leafTypeDef.getDefaultValue().orElseThrow();
646                 if (defaultValue instanceof String stringDefaultValue) {
647                     if (leafTypeDef instanceof BooleanTypeDefinition) {
648                         setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
649                     } else if (leafTypeDef instanceof DecimalTypeDefinition
650                             || leafTypeDef instanceof Uint64TypeDefinition) {
651                         setDefaultValue(property, new BigDecimal(stringDefaultValue));
652                     } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
653                         //uint8,16,32 int8,16,32,64
654                         if (isHexadecimalOrOctal(rangeRestrictedType)) {
655                             setDefaultValue(property, stringDefaultValue);
656                         } else {
657                             setDefaultValue(property, Long.valueOf(stringDefaultValue));
658                         }
659                     } else {
660                         setDefaultValue(property, stringDefaultValue);
661                     }
662                 } else {
663                     //we should never get here. getDefaultValue always gives us string
664                     setDefaultValue(property, defaultValue.toString());
665                 }
666             }
667         }
668         return jsonType;
669     }
670
671     private static String processBinaryType(final BinaryTypeDefinition definition, final ObjectNode property) {
672         definition.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
673         property.put(FORMAT_KEY, "byte");
674         return STRING_TYPE;
675     }
676
677     private static String processEnumType(final EnumTypeDefinition enumLeafType, final ObjectNode property) {
678         final List<EnumPair> enumPairs = enumLeafType.getValues();
679         final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
680         for (final EnumPair enumPair : enumPairs) {
681             enumNames.add(new TextNode(enumPair.getName()));
682         }
683
684         property.set(ENUM_KEY, enumNames);
685         enumLeafType.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
686         setExampleValue(property, enumLeafType.getValues().iterator().next().getName());
687         return STRING_TYPE;
688     }
689
690     private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
691             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
692             final EffectiveModelContext schemaContext) {
693         final String definitionName;
694         if (isImported(leafTypeDef)) {
695             definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
696         } else {
697             final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
698             definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
699         }
700         property.put(REF_KEY, COMPONENTS_PREFIX + definitionName);
701         return STRING_TYPE;
702     }
703
704     private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
705             final Map<String, Schema> definitions, final DefinitionNames definitionNames,
706             final EffectiveModelContext context) {
707         final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
708         final String identityName = idNode.getQName().getLocalName();
709         if (!definitionNames.isListedNode(idNode)) {
710             final Schema identityObj = buildIdentityObject(idNode, context);
711             final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
712             final String name = identityName + discriminator;
713             definitions.put(name, identityObj);
714             return name;
715         } else {
716             return identityName + definitionNames.getDiscriminator(idNode);
717         }
718     }
719
720     private static Schema buildIdentityObject(final IdentitySchemaNode idNode, final EffectiveModelContext context) {
721         final String identityName = idNode.getQName().getLocalName();
722         LOG.debug("Processing Identity: {}", identityName);
723
724         final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
725         final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
726         enumPayload.add(identityName);
727         populateEnumWithDerived(derivedIds, enumPayload, context);
728
729         return new Schema.Builder()
730             .title(identityName)
731             .description(idNode.getDescription().orElse(""))
732             .schemaEnum(enumPayload)
733             .type(STRING_TYPE)
734             .build();
735     }
736
737     private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
738         return !leafTypeDef.getQName().getModule().equals(topLevelModule.getQNameModule());
739     }
740
741     private static String processBitsType(final BitsTypeDefinition bitsType, final ObjectNode property) {
742         property.put(MIN_ITEMS, 0);
743         property.put(UNIQUE_ITEMS_KEY, true);
744         final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
745         final Collection<? extends Bit> bits = bitsType.getBits();
746         for (final Bit bit : bits) {
747             enumNames.add(new TextNode(bit.getName()));
748         }
749         property.set(ENUM_KEY, enumNames);
750         property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
751         return STRING_TYPE;
752     }
753
754     private static String processStringType(final StringTypeDefinition stringType, final ObjectNode property,
755             final String nodeName) {
756         var type = stringType;
757         while (type.getLengthConstraint().isEmpty() && type.getBaseType() != null) {
758             type = type.getBaseType();
759         }
760
761         type.getLengthConstraint().ifPresent(constraint -> {
762             final Range<Integer> range = constraint.getAllowedRanges().span();
763             putIfNonNull(property, MIN_LENGTH_KEY, range.lowerEndpoint());
764             putIfNonNull(property, MAX_LENGTH_KEY, range.upperEndpoint());
765         });
766
767         if (type.getPatternConstraints().iterator().hasNext()) {
768             final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
769             String regex = pattern.getRegularExpressionString();
770             // Escape special characters to prevent issues inside Automaton.
771             regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
772             for (final var charClass : PREDEFINED_CHARACTER_CLASSES.entrySet()) {
773                 regex = regex.replaceAll(charClass.getKey(), charClass.getValue());
774             }
775             String defaultValue = "";
776             try {
777                 final RegExp regExp = new RegExp(regex);
778                 defaultValue = regExp.toAutomaton().getShortestExample(true);
779             } catch (IllegalArgumentException ex) {
780                 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
781             }
782             setExampleValue(property, defaultValue);
783         } else {
784             setExampleValue(property, "Some " + nodeName);
785         }
786         return STRING_TYPE;
787     }
788
789     private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
790             final ObjectNode property) {
791         final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
792                 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
793
794         if (isHexadecimalOrOctal(leafTypeDef)) {
795             return STRING_TYPE;
796         }
797
798         if (leafTypeDef instanceof DecimalTypeDefinition) {
799             maybeLower.ifPresent(number -> setExampleValue(property, ((Decimal64) number).decimalValue()));
800             return NUMBER_TYPE;
801         }
802         if (leafTypeDef instanceof Uint8TypeDefinition
803                 || leafTypeDef instanceof Uint16TypeDefinition
804                 || leafTypeDef instanceof Int8TypeDefinition
805                 || leafTypeDef instanceof Int16TypeDefinition
806                 || leafTypeDef instanceof Int32TypeDefinition) {
807
808             property.put(FORMAT_KEY, INT32_FORMAT);
809             maybeLower.ifPresent(number -> setExampleValue(property, Integer.valueOf(number.toString())));
810         } else if (leafTypeDef instanceof Uint32TypeDefinition
811                 || leafTypeDef instanceof Int64TypeDefinition) {
812
813             property.put(FORMAT_KEY, INT64_FORMAT);
814             maybeLower.ifPresent(number -> setExampleValue(property, Long.valueOf(number.toString())));
815         } else {
816             //uint64
817             setExampleValue(property, 0);
818         }
819         return INTEGER_TYPE;
820     }
821
822     private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
823         final Optional<?> optDefaultValue = typeDef.getDefaultValue();
824         if (optDefaultValue.isPresent()) {
825             final String defaultValue = (String) optDefaultValue.orElseThrow();
826             return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
827         }
828         return false;
829     }
830
831     private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
832             final EffectiveModelContext schemaContext) {
833         // create example instance-identifier to the first container of node's module if exists or leave it empty
834         final var module = schemaContext.findModule(node.getQName().getModule());
835         if (module.isPresent()) {
836             final var container = module.orElseThrow().getChildNodes().stream()
837                     .filter(n -> n instanceof ContainerSchemaNode)
838                     .findFirst();
839             container.ifPresent(c -> setExampleValue(property, String.format("/%s:%s", module.orElseThrow().getPrefix(),
840                     c.getQName().getLocalName())));
841         }
842
843         return STRING_TYPE;
844     }
845
846     private static String processUnionType(final UnionTypeDefinition unionType, final ObjectNode property,
847             final String nodeName) {
848         boolean isStringTakePlace = false;
849         boolean isNumberTakePlace = false;
850         boolean isBooleanTakePlace = false;
851         for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
852             if (!isStringTakePlace) {
853                 if (typeDef instanceof StringTypeDefinition
854                         || typeDef instanceof BitsTypeDefinition
855                         || typeDef instanceof BinaryTypeDefinition
856                         || typeDef instanceof IdentityrefTypeDefinition
857                         || typeDef instanceof EnumTypeDefinition
858                         || typeDef instanceof LeafrefTypeDefinition
859                         || typeDef instanceof UnionTypeDefinition) {
860                     isStringTakePlace = true;
861                 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
862                     isNumberTakePlace = true;
863                 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
864                     isBooleanTakePlace = true;
865                 }
866             }
867         }
868         if (isStringTakePlace) {
869             unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, (String) v));
870             setExampleValue(property, "Some " + nodeName);
871             return STRING_TYPE;
872         }
873         if (isBooleanTakePlace) {
874             if (isNumberTakePlace) {
875                 // FIXME deal with other number formats
876                 unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Long.valueOf((String) v)));
877                 setExampleValue(property, 0);
878                 return NUMBER_TYPE;
879             }
880             unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Boolean.valueOf((String) v)));
881             setExampleValue(property, true);
882             return BOOLEAN_TYPE;
883         }
884         // FIXME deal with other number formats
885         unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Long.valueOf((String) v)));
886         setExampleValue(property, 0);
887         return NUMBER_TYPE;
888     }
889
890     private static ObjectNode buildXmlParameter(final SchemaNode node) {
891         final ObjectNode xml = JsonNodeFactory.instance.objectNode();
892         final QName qName = node.getQName();
893         xml.put(NAME_KEY, qName.getLocalName());
894         xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
895         return xml;
896     }
897
898     private static void putIfNonNull(final ObjectNode property, final String key, final Number number) {
899         if (key != null && number != null) {
900             if (number instanceof Double) {
901                 property.put(key, (Double) number);
902             } else if (number instanceof Float) {
903                 property.put(key, (Float) number);
904             } else if (number instanceof Integer) {
905                 property.put(key, (Integer) number);
906             } else if (number instanceof Short) {
907                 property.put(key, (Short) number);
908             } else if (number instanceof Long) {
909                 property.put(key, (Long) number);
910             }
911         }
912     }
913
914     private static void setExampleValue(final ObjectNode property, final String value) {
915         property.put(EXAMPLE_KEY, value);
916     }
917
918     private static void setExampleValue(final ObjectNode property, final Integer value) {
919         property.put(EXAMPLE_KEY, value);
920     }
921
922     private static void setExampleValue(final ObjectNode property, final Long value) {
923         property.put(EXAMPLE_KEY, value);
924     }
925
926     private static void setExampleValue(final ObjectNode property, final BigDecimal value) {
927         property.put(EXAMPLE_KEY, value);
928     }
929
930     private static void setExampleValue(final ObjectNode property, final Boolean value) {
931         property.put(EXAMPLE_KEY, value);
932     }
933
934     private static void setDefaultValue(final ObjectNode property, final String value) {
935         property.put(DEFAULT_KEY, value);
936     }
937
938     private static void setDefaultValue(final ObjectNode property, final Long value) {
939         property.put(DEFAULT_KEY, value);
940     }
941
942     private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
943         property.put(DEFAULT_KEY, value);
944     }
945
946     private static void setDefaultValue(final ObjectNode property, final Boolean value) {
947         property.put(DEFAULT_KEY, value);
948     }
949
950 }