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