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