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