2 * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others. All rights reserved.
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
8 package org.opendaylight.netconf.sal.rest.doc.impl;
10 import static org.opendaylight.netconf.sal.rest.doc.impl.BaseYangOpenApiGenerator.MODULE_NAME_SUFFIX;
11 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.COMPONENTS_PREFIX;
12 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.CONFIG;
13 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.NAME_KEY;
14 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.POST_SUFFIX;
15 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.TOP;
16 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.XML_KEY;
17 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.XML_SUFFIX;
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;
33 import java.util.Optional;
35 import java.util.regex.Pattern;
36 import java.util.stream.Collectors;
37 import org.opendaylight.yangtools.yang.common.Decimal64;
38 import org.opendaylight.yangtools.yang.common.QName;
39 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
40 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
41 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
46 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
47 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
48 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
50 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraint;
51 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
55 import org.opendaylight.yangtools.yang.model.api.MandatoryAware;
56 import org.opendaylight.yangtools.yang.model.api.Module;
57 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
58 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
59 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
60 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
61 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
62 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
63 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition.Bit;
64 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
65 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
66 import org.opendaylight.yangtools.yang.model.api.type.EmptyTypeDefinition;
67 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
68 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition.EnumPair;
69 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
70 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
71 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
72 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
73 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
74 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
75 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
76 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
77 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
78 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
79 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
80 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
81 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
82 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
83 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
84 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
85 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
86 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
87 import org.slf4j.Logger;
88 import org.slf4j.LoggerFactory;
91 * Generates JSON Schema for data defined in YANG. This class is not thread-safe.
93 public class DefinitionGenerator {
95 private static final Logger LOG = LoggerFactory.getLogger(DefinitionGenerator.class);
97 private static final String UNIQUE_ITEMS_KEY = "uniqueItems";
98 private static final String MAX_ITEMS = "maxItems";
99 private static final String MIN_ITEMS = "minItems";
100 private static final String MAX_LENGTH_KEY = "maxLength";
101 private static final String MIN_LENGTH_KEY = "minLength";
102 private static final String REQUIRED_KEY = "required";
103 private static final String REF_KEY = "$ref";
104 private static final String ITEMS_KEY = "items";
105 private static final String TYPE_KEY = "type";
106 private static final String PROPERTIES_KEY = "properties";
107 private static final String DESCRIPTION_KEY = "description";
108 private static final String ARRAY_TYPE = "array";
109 private static final String ENUM_KEY = "enum";
110 private static final String TITLE_KEY = "title";
111 private static final String DEFAULT_KEY = "default";
112 private static final String FORMAT_KEY = "format";
113 private static final String NAMESPACE_KEY = "namespace";
114 public static final String INPUT = "input";
115 public static final String INPUT_SUFFIX = "_input";
116 public static final String OUTPUT = "output";
117 public static final String OUTPUT_SUFFIX = "_output";
118 private static final String STRING_TYPE = "string";
119 private static final String OBJECT_TYPE = "object";
120 private static final String NUMBER_TYPE = "number";
121 private static final String INTEGER_TYPE = "integer";
122 private static final String INT32_FORMAT = "int32";
123 private static final String INT64_FORMAT = "int64";
124 private static final String BOOLEAN_TYPE = "boolean";
125 // Special characters used in automaton inside Generex.
126 // See https://www.brics.dk/automaton/doc/dk/brics/automaton/RegExp.html
127 private static final Pattern AUTOMATON_SPECIAL_CHARACTERS = Pattern.compile("[@&\"<>#~]");
129 private Module topLevelModule;
131 public DefinitionGenerator() {
135 * Creates Json definitions from provided module according to swagger spec.
137 * @param module - Yang module to be converted
138 * @param schemaContext - SchemaContext of all Yang files used by Api Doc
139 * @param definitionNames - Store for definition names
140 * @return ObjectNode containing data used for creating examples and definitions in Api Doc
141 * @throws IOException if I/O operation fails
145 public ObjectNode convertToJsonSchema(final Module module, final EffectiveModelContext schemaContext,
146 final ObjectNode definitions, final DefinitionNames definitionNames, final boolean isForSingleModule)
148 topLevelModule = module;
150 processIdentities(module, definitions, definitionNames, schemaContext);
151 processContainersAndLists(module, definitions, definitionNames, schemaContext);
152 processRPCs(module, definitions, definitionNames, schemaContext);
154 if (isForSingleModule) {
155 processModule(module, definitions, definitionNames, schemaContext);
161 public ObjectNode convertToJsonSchema(final Module module, final EffectiveModelContext schemaContext,
162 final DefinitionNames definitionNames, final boolean isForSingleModule)
164 final ObjectNode definitions = JsonNodeFactory.instance.objectNode();
165 if (isForSingleModule) {
166 definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
168 return convertToJsonSchema(module, schemaContext, definitions, definitionNames, isForSingleModule);
171 private void processModule(final Module module, final ObjectNode definitions, final DefinitionNames definitionNames,
172 final EffectiveModelContext schemaContext) {
173 final ObjectNode definition = JsonNodeFactory.instance.objectNode();
174 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
175 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
176 final String moduleName = module.getName();
177 final String definitionName = moduleName + MODULE_NAME_SUFFIX;
178 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
179 for (final DataSchemaNode node : module.getChildNodes()) {
180 stack.enterSchemaTree(node.getQName());
181 final String localName = node.getQName().getLocalName();
182 if (node.isConfiguration()) {
183 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
184 for (final DataSchemaNode childNode : ((DataNodeContainer) node).getChildNodes()) {
185 final ObjectNode childNodeProperties = JsonNodeFactory.instance.objectNode();
187 final String ref = COMPONENTS_PREFIX
188 + moduleName + CONFIG
190 + definitionNames.getDiscriminator(node);
192 if (node instanceof ListSchemaNode) {
193 childNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
194 final ObjectNode items = JsonNodeFactory.instance.objectNode();
195 items.put(REF_KEY, ref);
196 childNodeProperties.set(ITEMS_KEY, items);
197 childNodeProperties.put(DESCRIPTION_KEY, childNode.getDescription().orElse(""));
198 childNodeProperties.put(TITLE_KEY, localName + CONFIG);
201 Description can't be added, because nothing allowed alongside $ref.
202 allOf is not an option, because ServiceNow can't parse it.
204 childNodeProperties.put(REF_KEY, ref);
206 //add module name prefix to property name, when ServiceNow can process colons
207 properties.set(localName, childNodeProperties);
209 } else if (node instanceof LeafSchemaNode) {
211 Add module name prefix to property name, when ServiceNow can process colons(second parameter
214 processLeafNode((LeafSchemaNode) node, localName, properties, required, stack,
215 definitions, definitionNames);
220 definition.put(TITLE_KEY, definitionName);
221 definition.put(TYPE_KEY, OBJECT_TYPE);
222 definition.set(PROPERTIES_KEY, properties);
223 definition.put(DESCRIPTION_KEY, module.getDescription().orElse(""));
224 setRequiredIfNotEmpty(definition, required);
226 definitions.set(definitionName, definition);
229 private void processContainersAndLists(final Module module, final ObjectNode definitions,
230 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
231 final String moduleName = module.getName();
232 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
233 for (final DataSchemaNode childNode : module.getChildNodes()) {
234 stack.enterSchemaTree(childNode.getQName());
235 // For every container and list in the module
236 if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
237 if (childNode.isConfiguration()) {
238 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
241 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
243 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack);
249 private void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
250 final ObjectNode definitions, final DefinitionNames definitionNames, final SchemaInferenceStack stack)
252 for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
253 stack.enterSchemaTree(actionDef.getQName());
254 processOperations(actionDef, moduleName, definitions, definitionNames, stack);
259 private void processRPCs(final Module module, final ObjectNode definitions, final DefinitionNames definitionNames,
260 final EffectiveModelContext schemaContext) throws IOException {
261 final String moduleName = module.getName();
262 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
263 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
264 stack.enterSchemaTree(rpcDefinition.getQName());
265 processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack);
270 private void processOperations(final OperationDefinition operationDef, final String parentName,
271 final ObjectNode definitions, final DefinitionNames definitionNames, final SchemaInferenceStack stack)
273 final String operationName = operationDef.getQName().getLocalName();
274 processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
275 definitionNames, stack);
276 processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
277 definitionNames, stack);
280 private void processOperationInputOutput(final ContainerLike container, final String operationName,
281 final String parentName, final boolean isInput, final ObjectNode definitions,
282 final DefinitionNames definitionNames, final SchemaInferenceStack stack)
284 stack.enterSchemaTree(container.getQName());
285 if (!container.getChildNodes().isEmpty()) {
286 final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
287 final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
288 processChildren(childSchema, container.getChildNodes(), parentName, definitions, definitionNames,
291 childSchema.put(TYPE_KEY, OBJECT_TYPE);
292 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
293 xml.put(NAME_KEY, isInput ? INPUT : OUTPUT);
294 childSchema.set(XML_KEY, xml);
295 childSchema.put(TITLE_KEY, filename);
296 final String discriminator =
297 definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
298 definitions.set(filename + discriminator, childSchema);
300 processTopData(filename, discriminator, definitions, container);
305 private static ObjectNode processTopData(final String filename, final String discriminator,
306 final ObjectNode definitions, final SchemaNode schemaNode) {
307 final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
308 final String name = filename + discriminator;
309 final String ref = COMPONENTS_PREFIX + name;
310 final String topName = filename + TOP;
312 if (schemaNode instanceof ListSchemaNode) {
313 dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
314 final ObjectNode items = JsonNodeFactory.instance.objectNode();
315 items.put(REF_KEY, ref);
316 dataNodeProperties.set(ITEMS_KEY, items);
317 dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
320 Description can't be added, because nothing allowed alongside $ref.
321 allOf is not an option, because ServiceNow can't parse it.
323 dataNodeProperties.put(REF_KEY, ref);
326 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
328 Add module name prefix to property name, when needed, when ServiceNow can process colons,
329 use RestDocGenUtil#resolveNodesName for creating property name
331 properties.set(schemaNode.getQName().getLocalName(), dataNodeProperties);
332 final ObjectNode finalChildSchema = JsonNodeFactory.instance.objectNode();
333 finalChildSchema.put(TYPE_KEY, OBJECT_TYPE);
334 finalChildSchema.set(PROPERTIES_KEY, properties);
335 finalChildSchema.put(TITLE_KEY, topName);
338 definitions.set(topName + discriminator, finalChildSchema);
340 return dataNodeProperties;
344 * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
345 * @param module The module from which the identity stmt will be processed
346 * @param definitions The ObjectNode in which the parsed identity will be put as a 'model' obj
347 * @param definitionNames Store for definition names
349 private static void processIdentities(final Module module, final ObjectNode definitions,
350 final DefinitionNames definitionNames, final EffectiveModelContext context) {
351 final String moduleName = module.getName();
352 final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
353 LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
355 for (final IdentitySchemaNode idNode : idNodes) {
356 final ObjectNode identityObj = buildIdentityObject(idNode, context);
357 final String idName = idNode.getQName().getLocalName();
358 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
359 final String name = idName + discriminator;
360 definitions.set(name, identityObj);
364 private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
365 final ArrayNode enumPayload, final EffectiveModelContext context) {
366 for (final IdentitySchemaNode derivedId : derivedIds) {
367 enumPayload.add(derivedId.getQName().getLocalName());
368 populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
372 private ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
373 final ObjectNode definitions, final DefinitionNames definitionNames, final boolean isConfig,
374 final SchemaInferenceStack stack) throws IOException {
375 if (dataNode instanceof ListSchemaNode || dataNode instanceof ContainerSchemaNode) {
376 final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
377 final SchemaNode schemaNode = (SchemaNode) dataNode;
378 final String localName = schemaNode.getQName().getLocalName();
379 final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
380 final String nameAsParent = parentName + "_" + localName;
381 final ObjectNode properties =
382 processChildren(childSchema, containerChildren, parentName + "_" + localName, definitions,
383 definitionNames, isConfig, stack);
385 final String nodeName = parentName + (isConfig ? CONFIG : "") + "_" + localName;
386 final String postNodeName = parentName + CONFIG + "_" + localName + POST_SUFFIX;
387 final String postXmlNodeName = postNodeName + XML_SUFFIX;
388 final String parentNameConfigLocalName = parentName + CONFIG + "_" + localName;
390 final String description = schemaNode.getDescription().orElse("");
391 final String discriminator;
393 if (!definitionNames.isListedNode(schemaNode)) {
394 final List<String> names = List.of(parentNameConfigLocalName,
395 parentNameConfigLocalName + TOP,
400 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
402 discriminator = definitionNames.getDiscriminator(schemaNode);
406 final ObjectNode postSchema = createPostJsonSchema(schemaNode, properties, postNodeName, description);
407 String truePostNodeName = postNodeName + discriminator;
408 definitions.set(truePostNodeName, postSchema);
410 final ObjectNode postXmlSchema = JsonNodeFactory.instance.objectNode();
411 postXmlSchema.put(REF_KEY, COMPONENTS_PREFIX + truePostNodeName);
412 definitions.set(postXmlNodeName + discriminator, postXmlSchema);
415 childSchema.put(TYPE_KEY, OBJECT_TYPE);
416 childSchema.set(PROPERTIES_KEY, properties);
417 childSchema.put(TITLE_KEY, nodeName);
418 childSchema.put(DESCRIPTION_KEY, description);
420 final String defName = nodeName + discriminator;
421 childSchema.set(XML_KEY, buildXmlParameter(schemaNode));
422 definitions.set(defName, childSchema);
424 return processTopData(nodeName, discriminator, definitions, schemaNode);
429 private static ObjectNode createPostJsonSchema(final SchemaNode dataNode, final ObjectNode properties,
430 final String postNodeName, final String description) {
431 final ObjectNode postSchema = JsonNodeFactory.instance.objectNode();
432 final ObjectNode postItemProperties;
433 if (dataNode instanceof ListSchemaNode) {
434 postItemProperties = createListItemProperties(properties, (ListSchemaNode) dataNode);
436 postItemProperties = properties.deepCopy();
438 postSchema.put(TYPE_KEY, OBJECT_TYPE);
439 postSchema.set(PROPERTIES_KEY, postItemProperties);
440 postSchema.put(TITLE_KEY, postNodeName);
441 postSchema.put(DESCRIPTION_KEY, description);
442 postSchema.set(XML_KEY, buildXmlParameter(dataNode));
446 private static ObjectNode createListItemProperties(final ObjectNode properties, final ListSchemaNode listNode) {
447 final ObjectNode postListItemProperties = JsonNodeFactory.instance.objectNode();
448 final List<QName> keyDefinition = listNode.getKeyDefinition();
449 final Set<String> keys = listNode.getChildNodes().stream()
450 .filter(node -> keyDefinition.contains(node.getQName()))
451 .map(node -> node.getQName().getLocalName())
452 .collect(Collectors.toSet());
454 Iterator<Map.Entry<String, JsonNode>> it = properties.fields();
455 while (it.hasNext()) {
456 Map.Entry<String, JsonNode> property = it.next();
457 if (!keys.contains(property.getKey())) {
458 postListItemProperties.set(property.getKey(), property.getValue());
462 return postListItemProperties;
466 * Processes the nodes.
468 private ObjectNode processChildren(final ObjectNode parentNode, final Collection<? extends DataSchemaNode> nodes,
469 final String parentName, final ObjectNode definitions, final DefinitionNames definitionNames,
470 final boolean isConfig, final SchemaInferenceStack stack) throws IOException {
471 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
472 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
473 for (final DataSchemaNode node : nodes) {
474 if (!isConfig || node.isConfiguration()) {
475 processChildNode(node, parentName, definitions, definitionNames, isConfig, stack, properties);
478 parentNode.set(PROPERTIES_KEY, properties);
479 setRequiredIfNotEmpty(parentNode, required);
483 private void processChildNode(final DataSchemaNode node, final String parentName, final ObjectNode definitions,
484 final DefinitionNames definitionNames, final boolean isConfig, final SchemaInferenceStack stack,
485 final ObjectNode properties) throws IOException {
487 stack.enterSchemaTree(node.getQName());
490 Add module name prefix to property name, when needed, when ServiceNow can process colons,
491 use RestDocGenUtil#resolveNodesName for creating property name
493 final String name = node.getQName().getLocalName();
495 if (node instanceof LeafSchemaNode leaf) {
496 processLeafNode(leaf, name, properties, JsonNodeFactory.instance.arrayNode(), stack, definitions,
499 } else if (node instanceof AnyxmlSchemaNode anyxml) {
500 processAnyXMLNode(anyxml, name, properties, JsonNodeFactory.instance.arrayNode());
502 } else if (node instanceof AnydataSchemaNode anydata) {
503 processAnydataNode(anydata, name, properties, JsonNodeFactory.instance.arrayNode());
507 final ObjectNode property;
508 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
509 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions,
510 definitionNames, isConfig, stack);
512 processActionNodeContainer(node, parentName, definitions, definitionNames, stack);
514 } else if (node instanceof LeafListSchemaNode leafList) {
515 property = processLeafListNode(leafList, stack, definitions, definitionNames);
517 } else if (node instanceof ChoiceSchemaNode choice) {
518 if (!choice.getCases().isEmpty()) {
519 CaseSchemaNode caseSchemaNode = choice.getDefaultCase()
520 .orElse(choice.getCases().stream()
521 .findFirst().orElseThrow());
522 stack.enterSchemaTree(caseSchemaNode.getQName());
523 for (final DataSchemaNode childNode : caseSchemaNode.getChildNodes()) {
524 processChildNode(childNode, parentName, definitions, definitionNames, isConfig, stack,
532 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
534 if (property != null) {
535 properties.set(name, property);
542 private ObjectNode processLeafListNode(final LeafListSchemaNode listNode, final SchemaInferenceStack stack,
543 final ObjectNode definitions, final DefinitionNames definitionNames) {
544 final ObjectNode props = JsonNodeFactory.instance.objectNode();
545 props.put(TYPE_KEY, ARRAY_TYPE);
547 final ObjectNode itemsVal = JsonNodeFactory.instance.objectNode();
548 final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
549 processElementCount(optConstraint, props);
551 processTypeDef(listNode.getType(), listNode, itemsVal, stack, definitions, definitionNames);
552 props.set(ITEMS_KEY, itemsVal);
554 props.put(DESCRIPTION_KEY, listNode.getDescription().orElse(""));
559 private static void processElementCount(final Optional<ElementCountConstraint> constraint, final ObjectNode props) {
560 if (constraint.isPresent()) {
561 final ElementCountConstraint constr = constraint.orElseThrow();
562 final Integer minElements = constr.getMinElements();
563 if (minElements != null) {
564 props.put(MIN_ITEMS, minElements);
566 final Integer maxElements = constr.getMaxElements();
567 if (maxElements != null) {
568 props.put(MAX_ITEMS, maxElements);
573 private static void processMandatory(final MandatoryAware node, final String nodeName, final ArrayNode required) {
574 if (node.isMandatory()) {
575 required.add(nodeName);
579 private ObjectNode processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
580 final ObjectNode properties, final ArrayNode required, final SchemaInferenceStack stack,
581 final ObjectNode definitions, final DefinitionNames definitionNames) {
582 final ObjectNode property = JsonNodeFactory.instance.objectNode();
584 final String leafDescription = leafNode.getDescription().orElse("");
586 Description can't be added, because nothing allowed alongside $ref.
587 allOf is not an option, because ServiceNow can't parse it.
589 if (!(leafNode.getType() instanceof IdentityrefTypeDefinition)) {
590 property.put(DESCRIPTION_KEY, leafDescription);
593 processTypeDef(leafNode.getType(), leafNode, property, stack, definitions, definitionNames);
594 properties.set(jsonLeafName, property);
595 property.set(XML_KEY, buildXmlParameter(leafNode));
596 processMandatory(leafNode, jsonLeafName, required);
601 private static ObjectNode processAnydataNode(final AnydataSchemaNode leafNode, final String name,
602 final ObjectNode properties, final ArrayNode required) {
603 final ObjectNode property = JsonNodeFactory.instance.objectNode();
605 final String leafDescription = leafNode.getDescription().orElse("");
606 property.put(DESCRIPTION_KEY, leafDescription);
608 final String localName = leafNode.getQName().getLocalName();
609 setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
610 property.put(TYPE_KEY, STRING_TYPE);
611 property.set(XML_KEY, buildXmlParameter(leafNode));
612 processMandatory(leafNode, name, required);
613 properties.set(name, property);
618 private static ObjectNode processAnyXMLNode(final AnyxmlSchemaNode leafNode, final String name,
619 final ObjectNode properties, final ArrayNode required) {
620 final ObjectNode property = JsonNodeFactory.instance.objectNode();
622 final String leafDescription = leafNode.getDescription().orElse("");
623 property.put(DESCRIPTION_KEY, leafDescription);
625 final String localName = leafNode.getQName().getLocalName();
626 setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
627 property.put(TYPE_KEY, STRING_TYPE);
628 property.set(XML_KEY, buildXmlParameter(leafNode));
629 processMandatory(leafNode, name, required);
630 properties.set(name, property);
635 private String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
636 final ObjectNode property, final SchemaInferenceStack stack, final ObjectNode definitions,
637 final DefinitionNames definitionNames) {
638 final String jsonType;
639 if (leafTypeDef instanceof BinaryTypeDefinition) {
640 jsonType = processBinaryType(property);
642 } else if (leafTypeDef instanceof BitsTypeDefinition) {
643 jsonType = processBitsType((BitsTypeDefinition) leafTypeDef, property);
645 } else if (leafTypeDef instanceof EnumTypeDefinition) {
646 jsonType = processEnumType((EnumTypeDefinition) leafTypeDef, property);
648 } else if (leafTypeDef instanceof IdentityrefTypeDefinition) {
649 jsonType = processIdentityRefType((IdentityrefTypeDefinition) leafTypeDef, property, definitions,
650 definitionNames, stack.getEffectiveModelContext());
652 } else if (leafTypeDef instanceof StringTypeDefinition) {
653 jsonType = processStringType(leafTypeDef, property, node.getQName().getLocalName());
655 } else if (leafTypeDef instanceof UnionTypeDefinition) {
656 jsonType = processUnionType((UnionTypeDefinition) leafTypeDef);
658 } else if (leafTypeDef instanceof EmptyTypeDefinition) {
659 jsonType = OBJECT_TYPE;
660 } else if (leafTypeDef instanceof LeafrefTypeDefinition) {
661 return processTypeDef(stack.resolveLeafref((LeafrefTypeDefinition) leafTypeDef), node, property,
662 stack, definitions, definitionNames);
663 } else if (leafTypeDef instanceof BooleanTypeDefinition) {
664 jsonType = BOOLEAN_TYPE;
665 setDefaultValue(property, true);
666 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
667 jsonType = processNumberType((RangeRestrictedTypeDefinition<?, ?>) leafTypeDef, property);
668 } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
669 jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
671 jsonType = STRING_TYPE;
673 if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
674 putIfNonNull(property, TYPE_KEY, jsonType);
675 if (leafTypeDef.getDefaultValue().isPresent()) {
676 final Object defaultValue = leafTypeDef.getDefaultValue().orElseThrow();
677 if (defaultValue instanceof String stringDefaultValue) {
678 if (leafTypeDef instanceof BooleanTypeDefinition) {
679 setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
680 } else if (leafTypeDef instanceof DecimalTypeDefinition
681 || leafTypeDef instanceof Uint64TypeDefinition) {
682 setDefaultValue(property, new BigDecimal(stringDefaultValue));
683 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
684 //uint8,16,32 int8,16,32,64
685 if (isHexadecimalOrOctal((RangeRestrictedTypeDefinition<?, ?>)leafTypeDef)) {
686 setDefaultValue(property, stringDefaultValue);
688 setDefaultValue(property, Long.valueOf(stringDefaultValue));
691 setDefaultValue(property, stringDefaultValue);
694 //we should never get here. getDefaultValue always gives us string
695 setDefaultValue(property, defaultValue.toString());
702 private static String processBinaryType(final ObjectNode property) {
703 property.put(FORMAT_KEY, "byte");
707 private static String processEnumType(final EnumTypeDefinition enumLeafType, final ObjectNode property) {
708 final List<EnumPair> enumPairs = enumLeafType.getValues();
709 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
710 for (final EnumPair enumPair : enumPairs) {
711 enumNames.add(new TextNode(enumPair.getName()));
714 property.set(ENUM_KEY, enumNames);
715 setDefaultValue(property, enumLeafType.getValues().iterator().next().getName());
719 private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
720 final ObjectNode definitions, final DefinitionNames definitionNames,
721 final EffectiveModelContext schemaContext) {
722 final String definitionName;
723 if (isImported(leafTypeDef)) {
724 definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
726 final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
727 definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
729 property.put(REF_KEY, COMPONENTS_PREFIX + definitionName);
733 private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
734 final ObjectNode definitions, final DefinitionNames definitionNames, final EffectiveModelContext context) {
735 final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
736 final String identityName = idNode.getQName().getLocalName();
737 if (!definitionNames.isListedNode(idNode)) {
738 final ObjectNode identityObj = buildIdentityObject(idNode, context);
739 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
740 final String name = identityName + discriminator;
741 definitions.set(name, identityObj);
744 return identityName + definitionNames.getDiscriminator(idNode);
748 private static ObjectNode buildIdentityObject(final IdentitySchemaNode idNode,
749 final EffectiveModelContext context) {
750 final ObjectNode identityObj = JsonNodeFactory.instance.objectNode();
751 final String identityName = idNode.getQName().getLocalName();
752 LOG.debug("Processing Identity: {}", identityName);
754 identityObj.put(TITLE_KEY, identityName);
755 identityObj.put(DESCRIPTION_KEY, idNode.getDescription().orElse(""));
757 final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
759 final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
760 enumPayload.add(identityName);
761 populateEnumWithDerived(derivedIds, enumPayload, context);
762 identityObj.set(ENUM_KEY, enumPayload);
763 identityObj.put(TYPE_KEY, STRING_TYPE);
767 private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
768 return !leafTypeDef.getQName().getModule().equals(topLevelModule.getQNameModule());
771 private static String processBitsType(final BitsTypeDefinition bitsType, final ObjectNode property) {
772 property.put(MIN_ITEMS, 0);
773 property.put(UNIQUE_ITEMS_KEY, true);
774 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
775 final Collection<? extends Bit> bits = bitsType.getBits();
776 for (final Bit bit : bits) {
777 enumNames.add(new TextNode(bit.getName()));
779 property.set(ENUM_KEY, enumNames);
780 property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
784 private static String processStringType(final TypeDefinition<?> stringType, final ObjectNode property,
785 final String nodeName) {
786 StringTypeDefinition type = (StringTypeDefinition) stringType;
787 Optional<LengthConstraint> lengthConstraints = ((StringTypeDefinition) stringType).getLengthConstraint();
788 while (lengthConstraints.isEmpty() && type.getBaseType() != null) {
789 type = type.getBaseType();
790 lengthConstraints = type.getLengthConstraint();
793 if (lengthConstraints.isPresent()) {
794 final Range<Integer> range = lengthConstraints.orElseThrow().getAllowedRanges().span();
795 putIfNonNull(property, MIN_LENGTH_KEY, range.lowerEndpoint());
796 putIfNonNull(property, MAX_LENGTH_KEY, range.upperEndpoint());
799 if (type.getPatternConstraints().iterator().hasNext()) {
800 final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
801 String regex = pattern.getJavaPatternString();
802 regex = regex.substring(1, regex.length() - 1);
803 // Escape special characters to prevent issues inside Generex.
804 regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
805 String defaultValue = "";
807 final Generex generex = new Generex(regex);
808 defaultValue = generex.random();
809 } catch (IllegalArgumentException ex) {
810 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
812 setDefaultValue(property, defaultValue);
814 setDefaultValue(property, "Some " + nodeName);
819 private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
820 final ObjectNode property) {
821 final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
822 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
824 if (isHexadecimalOrOctal(leafTypeDef)) {
828 if (leafTypeDef instanceof DecimalTypeDefinition) {
829 maybeLower.ifPresent(number -> setDefaultValue(property, ((Decimal64) number).decimalValue()));
832 if (leafTypeDef instanceof Uint8TypeDefinition
833 || leafTypeDef instanceof Uint16TypeDefinition
834 || leafTypeDef instanceof Int8TypeDefinition
835 || leafTypeDef instanceof Int16TypeDefinition
836 || leafTypeDef instanceof Int32TypeDefinition) {
838 property.put(FORMAT_KEY, INT32_FORMAT);
839 maybeLower.ifPresent(number -> setDefaultValue(property, Integer.valueOf(number.toString())));
840 } else if (leafTypeDef instanceof Uint32TypeDefinition
841 || leafTypeDef instanceof Int64TypeDefinition) {
843 property.put(FORMAT_KEY, INT64_FORMAT);
844 maybeLower.ifPresent(number -> setDefaultValue(property, Long.valueOf(number.toString())));
847 setDefaultValue(property, 0);
852 private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
853 final Optional<?> optDefaultValue = typeDef.getDefaultValue();
854 if (optDefaultValue.isPresent()) {
855 final String defaultValue = (String) optDefaultValue.orElseThrow();
856 return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
861 private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
862 final EffectiveModelContext schemaContext) {
863 // create example instance-identifier to the first container of node's module if exists or leave it empty
864 final var module = schemaContext.findModule(node.getQName().getModule());
865 if (module.isPresent()) {
866 final var container = module.orElseThrow().getChildNodes().stream()
867 .filter(n -> n instanceof ContainerSchemaNode)
869 container.ifPresent(c -> setDefaultValue(property, String.format("/%s:%s", module.orElseThrow().getPrefix(),
870 c.getQName().getLocalName())));
876 private static String processUnionType(final UnionTypeDefinition unionType) {
877 boolean isStringTakePlace = false;
878 boolean isNumberTakePlace = false;
879 boolean isBooleanTakePlace = false;
880 for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
881 if (!isStringTakePlace) {
882 if (typeDef instanceof StringTypeDefinition
883 || typeDef instanceof BitsTypeDefinition
884 || typeDef instanceof BinaryTypeDefinition
885 || typeDef instanceof IdentityrefTypeDefinition
886 || typeDef instanceof EnumTypeDefinition
887 || typeDef instanceof LeafrefTypeDefinition
888 || typeDef instanceof UnionTypeDefinition) {
889 isStringTakePlace = true;
890 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
891 isNumberTakePlace = true;
892 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
893 isBooleanTakePlace = true;
897 if (isStringTakePlace) {
900 if (isBooleanTakePlace) {
901 if (isNumberTakePlace) {
909 private static ObjectNode buildXmlParameter(final SchemaNode node) {
910 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
911 final QName qName = node.getQName();
912 xml.put(NAME_KEY, qName.getLocalName());
913 xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
917 private static void putIfNonNull(final ObjectNode property, final String key, final Number number) {
918 if (key != null && number != null) {
919 if (number instanceof Double) {
920 property.put(key, (Double) number);
921 } else if (number instanceof Float) {
922 property.put(key, (Float) number);
923 } else if (number instanceof Integer) {
924 property.put(key, (Integer) number);
925 } else if (number instanceof Short) {
926 property.put(key, (Short) number);
927 } else if (number instanceof Long) {
928 property.put(key, (Long) number);
933 private static void putIfNonNull(final ObjectNode property, final String key, final String value) {
934 if (key != null && value != null) {
935 property.put(key, value);
939 private static void setRequiredIfNotEmpty(final ObjectNode node, final ArrayNode required) {
940 if (required.size() > 0) {
941 node.set(REQUIRED_KEY, required);
945 private static void setDefaultValue(final ObjectNode property, final String value) {
946 property.put(DEFAULT_KEY, value);
949 private static void setDefaultValue(final ObjectNode property, final Integer value) {
950 property.put(DEFAULT_KEY, value);
953 private static void setDefaultValue(final ObjectNode property, final Long value) {
954 property.put(DEFAULT_KEY, value);
957 private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
958 property.put(DEFAULT_KEY, value);
961 private static void setDefaultValue(final ObjectNode property, final Boolean value) {
962 property.put(DEFAULT_KEY, value);