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