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.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;
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.netconf.sal.rest.doc.impl.ApiDocServiceImpl.OAversion;
38 import org.opendaylight.yangtools.yang.common.Decimal64;
39 import org.opendaylight.yangtools.yang.common.QName;
40 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
41 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
42 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
47 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
49 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
51 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraint;
52 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
55 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.MandatoryAware;
57 import org.opendaylight.yangtools.yang.model.api.Module;
58 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
59 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
60 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
61 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
62 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
63 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
64 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition.Bit;
65 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
66 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
67 import org.opendaylight.yangtools.yang.model.api.type.EmptyTypeDefinition;
68 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
69 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition.EnumPair;
70 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
71 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
72 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
73 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
74 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
75 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
76 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
77 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
78 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
79 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
80 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
81 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
82 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
83 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
84 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
85 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
86 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
87 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
88 import org.slf4j.Logger;
89 import org.slf4j.LoggerFactory;
92 * Generates JSON Schema for data defined in YANG. This class is not thread-safe.
94 public class DefinitionGenerator {
96 private static final Logger LOG = LoggerFactory.getLogger(DefinitionGenerator.class);
98 private static final String UNIQUE_ITEMS_KEY = "uniqueItems";
99 private static final String MAX_ITEMS = "maxItems";
100 private static final String MIN_ITEMS = "minItems";
101 private static final String MAX_LENGTH_KEY = "maxLength";
102 private static final String MIN_LENGTH_KEY = "minLength";
103 private static final String REQUIRED_KEY = "required";
104 private static final String REF_KEY = "$ref";
105 private static final String ITEMS_KEY = "items";
106 private static final String TYPE_KEY = "type";
107 private static final String PROPERTIES_KEY = "properties";
108 private static final String DESCRIPTION_KEY = "description";
109 private static final String ARRAY_TYPE = "array";
110 private static final String ENUM_KEY = "enum";
111 private static final String TITLE_KEY = "title";
112 private static final String DEFAULT_KEY = "default";
113 private static final String FORMAT_KEY = "format";
114 private static final String NAMESPACE_KEY = "namespace";
115 public static final String INPUT = "input";
116 public static final String INPUT_SUFFIX = "_input";
117 public static final String OUTPUT = "output";
118 public static final String OUTPUT_SUFFIX = "_output";
119 private static final String STRING_TYPE = "string";
120 private static final String OBJECT_TYPE = "object";
121 private static final String NUMBER_TYPE = "number";
122 private static final String INTEGER_TYPE = "integer";
123 private static final String INT32_FORMAT = "int32";
124 private static final String INT64_FORMAT = "int64";
125 private static final String BOOLEAN_TYPE = "boolean";
126 // Special characters used in automaton inside Generex.
127 // See https://www.brics.dk/automaton/doc/dk/brics/automaton/RegExp.html
128 private static final Pattern AUTOMATON_SPECIAL_CHARACTERS = Pattern.compile("[@&\"<>#~]");
130 private Module topLevelModule;
132 public DefinitionGenerator() {
136 * Creates Json definitions from provided module according to swagger spec.
138 * @param module - Yang module to be converted
139 * @param schemaContext - SchemaContext of all Yang files used by Api Doc
140 * @param definitionNames - Store for definition names
141 * @return ObjectNode containing data used for creating examples and definitions in Api Doc
142 * @throws IOException if I/O operation fails
146 public ObjectNode convertToJsonSchema(final Module module, final EffectiveModelContext schemaContext,
147 final ObjectNode definitions, final DefinitionNames definitionNames,
148 final OAversion oaversion, final boolean isForSingleModule)
150 topLevelModule = module;
152 processIdentities(module, definitions, definitionNames, schemaContext);
153 processContainersAndLists(module, definitions, definitionNames, schemaContext, oaversion);
154 processRPCs(module, definitions, definitionNames, schemaContext, oaversion);
156 if (isForSingleModule) {
157 processModule(module, definitions, definitionNames, schemaContext, oaversion);
163 public ObjectNode convertToJsonSchema(final Module module, final EffectiveModelContext schemaContext,
164 final DefinitionNames definitionNames, final OAversion oaversion,
165 final boolean isForSingleModule)
167 final ObjectNode definitions = JsonNodeFactory.instance.objectNode();
168 if (isForSingleModule) {
169 definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
171 return convertToJsonSchema(module, schemaContext, definitions, definitionNames, oaversion, isForSingleModule);
174 private void processModule(final Module module, final ObjectNode definitions, final DefinitionNames definitionNames,
175 final EffectiveModelContext schemaContext, final OAversion oaversion) {
176 final ObjectNode definition = JsonNodeFactory.instance.objectNode();
177 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
178 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
179 final String moduleName = module.getName();
180 final String definitionName = moduleName + MODULE_NAME_SUFFIX;
181 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
182 for (final DataSchemaNode node : module.getChildNodes()) {
183 stack.enterSchemaTree(node.getQName());
184 final String localName = node.getQName().getLocalName();
185 if (node.isConfiguration()) {
186 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
187 for (final DataSchemaNode childNode : ((DataNodeContainer) node).getChildNodes()) {
188 final ObjectNode childNodeProperties = JsonNodeFactory.instance.objectNode();
190 final String ref = getAppropriateModelPrefix(oaversion)
191 + moduleName + CONFIG
193 + definitionNames.getDiscriminator(node);
195 if (node instanceof ListSchemaNode) {
196 childNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
197 final ObjectNode items = JsonNodeFactory.instance.objectNode();
198 items.put(REF_KEY, ref);
199 childNodeProperties.set(ITEMS_KEY, items);
200 childNodeProperties.put(DESCRIPTION_KEY, childNode.getDescription().orElse(""));
201 childNodeProperties.put(TITLE_KEY, localName + CONFIG);
204 Description can't be added, because nothing allowed alongside $ref.
205 allOf is not an option, because ServiceNow can't parse it.
207 childNodeProperties.put(REF_KEY, ref);
209 //add module name prefix to property name, when ServiceNow can process colons
210 properties.set(localName, childNodeProperties);
212 } else if (node instanceof LeafSchemaNode) {
214 Add module name prefix to property name, when ServiceNow can process colons(second parameter
217 processLeafNode((LeafSchemaNode) node, localName, properties, required, stack,
218 definitions, definitionNames, oaversion);
223 definition.put(TITLE_KEY, definitionName);
224 definition.put(TYPE_KEY, OBJECT_TYPE);
225 definition.set(PROPERTIES_KEY, properties);
226 definition.put(DESCRIPTION_KEY, module.getDescription().orElse(""));
227 setRequiredIfNotEmpty(definition, required);
229 definitions.set(definitionName, definition);
232 private void processContainersAndLists(final Module module, final ObjectNode definitions,
233 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext, final OAversion oaversion)
235 final String moduleName = module.getName();
236 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
237 for (final DataSchemaNode childNode : module.getChildNodes()) {
238 stack.enterSchemaTree(childNode.getQName());
239 // For every container and list in the module
240 if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
241 if (childNode.isConfiguration()) {
242 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
243 true, stack, oaversion);
245 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
246 false, stack, oaversion);
247 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack, oaversion);
253 private void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
254 final ObjectNode definitions, final DefinitionNames definitionNames,
255 final SchemaInferenceStack stack, final OAversion oaversion)
257 for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
258 stack.enterSchemaTree(actionDef.getQName());
259 processOperations(actionDef, moduleName, definitions, definitionNames, stack, oaversion);
264 private void processRPCs(final Module module, final ObjectNode definitions, final DefinitionNames definitionNames,
265 final EffectiveModelContext schemaContext, final OAversion oaversion) throws IOException {
266 final String moduleName = module.getName();
267 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
268 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
269 stack.enterSchemaTree(rpcDefinition.getQName());
270 processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack, oaversion);
275 private void processOperations(final OperationDefinition operationDef, final String parentName,
276 final ObjectNode definitions, final DefinitionNames definitionNames,
277 final SchemaInferenceStack stack, final OAversion oaversion)
279 final String operationName = operationDef.getQName().getLocalName();
280 processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
281 definitionNames, stack, oaversion);
282 processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
283 definitionNames, stack, oaversion);
286 private void processOperationInputOutput(final ContainerLike container, final String operationName,
287 final String parentName, final boolean isInput,
288 final ObjectNode definitions, final DefinitionNames definitionNames,
289 final SchemaInferenceStack stack, final OAversion oaversion)
291 stack.enterSchemaTree(container.getQName());
292 if (!container.getChildNodes().isEmpty()) {
293 final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
294 final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
295 processChildren(childSchema, container.getChildNodes(), parentName, definitions, definitionNames,
296 false, stack, oaversion);
298 childSchema.put(TYPE_KEY, OBJECT_TYPE);
299 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
300 xml.put(NAME_KEY, isInput ? INPUT : OUTPUT);
301 childSchema.set(XML_KEY, xml);
302 childSchema.put(TITLE_KEY, filename);
303 final String discriminator =
304 definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
305 definitions.set(filename + discriminator, childSchema);
307 processTopData(filename, discriminator, definitions, container, oaversion);
312 private static ObjectNode processTopData(final String filename, final String discriminator,
313 final ObjectNode definitions, final SchemaNode schemaNode, final OAversion oaversion) {
314 final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
315 final String name = filename + discriminator;
316 final String ref = getAppropriateModelPrefix(oaversion) + name;
317 final String topName = filename + TOP;
319 if (schemaNode instanceof ListSchemaNode) {
320 dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
321 final ObjectNode items = JsonNodeFactory.instance.objectNode();
322 items.put(REF_KEY, ref);
323 dataNodeProperties.set(ITEMS_KEY, items);
324 dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
327 Description can't be added, because nothing allowed alongside $ref.
328 allOf is not an option, because ServiceNow can't parse it.
330 dataNodeProperties.put(REF_KEY, ref);
333 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
335 Add module name prefix to property name, when needed, when ServiceNow can process colons,
336 use RestDocGenUtil#resolveNodesName for creating property name
338 properties.set(schemaNode.getQName().getLocalName(), dataNodeProperties);
339 final ObjectNode finalChildSchema = JsonNodeFactory.instance.objectNode();
340 finalChildSchema.put(TYPE_KEY, OBJECT_TYPE);
341 finalChildSchema.set(PROPERTIES_KEY, properties);
342 finalChildSchema.put(TITLE_KEY, topName);
345 definitions.set(topName + discriminator, finalChildSchema);
347 return dataNodeProperties;
351 * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
352 * @param module The module from which the identity stmt will be processed
353 * @param definitions The ObjectNode in which the parsed identity will be put as a 'model' obj
354 * @param definitionNames Store for definition names
356 private static void processIdentities(final Module module, final ObjectNode definitions,
357 final DefinitionNames definitionNames, final EffectiveModelContext context) {
358 final String moduleName = module.getName();
359 final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
360 LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
362 for (final IdentitySchemaNode idNode : idNodes) {
363 final ObjectNode identityObj = buildIdentityObject(idNode, context);
364 final String idName = idNode.getQName().getLocalName();
365 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
366 final String name = idName + discriminator;
367 definitions.set(name, identityObj);
371 private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
372 final ArrayNode enumPayload, final EffectiveModelContext context) {
373 for (final IdentitySchemaNode derivedId : derivedIds) {
374 enumPayload.add(derivedId.getQName().getLocalName());
375 populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
379 private ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
380 final ObjectNode definitions, final DefinitionNames definitionNames,
381 final boolean isConfig, final SchemaInferenceStack stack,
382 final OAversion oaversion) throws IOException {
383 if (dataNode instanceof ListSchemaNode || dataNode instanceof ContainerSchemaNode) {
384 final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
385 final SchemaNode schemaNode = (SchemaNode) dataNode;
386 final String localName = schemaNode.getQName().getLocalName();
387 final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
388 final String nameAsParent = parentName + "_" + localName;
389 final ObjectNode properties =
390 processChildren(childSchema, containerChildren, parentName + "_" + localName, definitions,
391 definitionNames, isConfig, stack, oaversion);
393 final String nodeName = parentName + (isConfig ? CONFIG : "") + "_" + localName;
394 final String postNodeName = parentName + CONFIG + "_" + localName + POST_SUFFIX;
395 final String postXmlNodeName = postNodeName + XML_SUFFIX;
396 final String parentNameConfigLocalName = parentName + CONFIG + "_" + localName;
398 final String description = schemaNode.getDescription().orElse("");
399 final String discriminator;
401 if (!definitionNames.isListedNode(schemaNode)) {
402 final List<String> names = List.of(parentNameConfigLocalName,
403 parentNameConfigLocalName + TOP,
408 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
410 discriminator = definitionNames.getDiscriminator(schemaNode);
414 final ObjectNode postSchema = createPostJsonSchema(schemaNode, properties, postNodeName, description);
415 String truePostNodeName = postNodeName + discriminator;
416 definitions.set(truePostNodeName, postSchema);
418 final ObjectNode postXmlSchema = JsonNodeFactory.instance.objectNode();
419 postXmlSchema.put(REF_KEY, getAppropriateModelPrefix(oaversion) + truePostNodeName);
420 definitions.set(postXmlNodeName + discriminator, postXmlSchema);
423 childSchema.put(TYPE_KEY, OBJECT_TYPE);
424 childSchema.set(PROPERTIES_KEY, properties);
425 childSchema.put(TITLE_KEY, nodeName);
426 childSchema.put(DESCRIPTION_KEY, description);
428 final String defName = nodeName + discriminator;
429 childSchema.set(XML_KEY, buildXmlParameter(schemaNode));
430 definitions.set(defName, childSchema);
432 return processTopData(nodeName, discriminator, definitions, schemaNode, oaversion);
437 private static ObjectNode createPostJsonSchema(final SchemaNode dataNode, final ObjectNode properties,
438 final String postNodeName, final String description) {
439 final ObjectNode postSchema = JsonNodeFactory.instance.objectNode();
440 final ObjectNode postItemProperties;
441 if (dataNode instanceof ListSchemaNode) {
442 postItemProperties = createListItemProperties(properties, (ListSchemaNode) dataNode);
444 postItemProperties = properties.deepCopy();
446 postSchema.put(TYPE_KEY, OBJECT_TYPE);
447 postSchema.set(PROPERTIES_KEY, postItemProperties);
448 postSchema.put(TITLE_KEY, postNodeName);
449 postSchema.put(DESCRIPTION_KEY, description);
450 postSchema.set(XML_KEY, buildXmlParameter(dataNode));
454 private static ObjectNode createListItemProperties(final ObjectNode properties, final ListSchemaNode listNode) {
455 final ObjectNode postListItemProperties = JsonNodeFactory.instance.objectNode();
456 final List<QName> keyDefinition = listNode.getKeyDefinition();
457 final Set<String> keys = listNode.getChildNodes().stream()
458 .filter(node -> keyDefinition.contains(node.getQName()))
459 .map(node -> node.getQName().getLocalName())
460 .collect(Collectors.toSet());
462 Iterator<Map.Entry<String, JsonNode>> it = properties.fields();
463 while (it.hasNext()) {
464 Map.Entry<String, JsonNode> property = it.next();
465 if (!keys.contains(property.getKey())) {
466 postListItemProperties.set(property.getKey(), property.getValue());
470 return postListItemProperties;
474 * Processes the nodes.
476 private ObjectNode processChildren(
477 final ObjectNode parentNode, final Collection<? extends DataSchemaNode> nodes, final String parentName,
478 final ObjectNode definitions, final DefinitionNames definitionNames, final boolean isConfig,
479 final SchemaInferenceStack stack, final OAversion oaversion) throws IOException {
480 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
481 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
482 for (final DataSchemaNode node : nodes) {
483 if (!isConfig || node.isConfiguration()) {
484 processChildNode(node, parentName, definitions, definitionNames, isConfig, stack, properties,
488 parentNode.set(PROPERTIES_KEY, properties);
489 setRequiredIfNotEmpty(parentNode, required);
493 private void processChildNode(
494 final DataSchemaNode node, final String parentName, final ObjectNode definitions,
495 final DefinitionNames definitionNames, final boolean isConfig,
496 final SchemaInferenceStack stack, final ObjectNode properties, final OAversion oaversion)
499 stack.enterSchemaTree(node.getQName());
502 Add module name prefix to property name, when needed, when ServiceNow can process colons,
503 use RestDocGenUtil#resolveNodesName for creating property name
505 final String name = node.getQName().getLocalName();
507 if (node instanceof LeafSchemaNode leaf) {
508 processLeafNode(leaf, name, properties, JsonNodeFactory.instance.arrayNode(), stack, definitions,
509 definitionNames, oaversion);
511 } else if (node instanceof AnyxmlSchemaNode anyxml) {
512 processAnyXMLNode(anyxml, name, properties, JsonNodeFactory.instance.arrayNode());
514 } else if (node instanceof AnydataSchemaNode anydata) {
515 processAnydataNode(anydata, name, properties, JsonNodeFactory.instance.arrayNode());
519 final ObjectNode property;
520 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
521 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions,
522 definitionNames, isConfig, stack, oaversion);
524 processActionNodeContainer(node, parentName, definitions, definitionNames, stack, oaversion);
526 } else if (node instanceof LeafListSchemaNode leafList) {
527 property = processLeafListNode(leafList, stack, definitions, definitionNames, oaversion);
529 } else if (node instanceof ChoiceSchemaNode choice) {
530 for (final CaseSchemaNode variant : choice.getCases()) {
531 stack.enterSchemaTree(variant.getQName());
532 for (final DataSchemaNode childNode : variant.getChildNodes()) {
533 processChildNode(childNode, parentName, definitions, definitionNames, isConfig, stack,
534 properties, oaversion);
541 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
543 if (property != null) {
544 properties.set(name, property);
551 private ObjectNode processLeafListNode(final LeafListSchemaNode listNode, final SchemaInferenceStack stack,
552 final ObjectNode definitions, final DefinitionNames definitionNames,
553 final OAversion oaversion) {
554 final ObjectNode props = JsonNodeFactory.instance.objectNode();
555 props.put(TYPE_KEY, ARRAY_TYPE);
557 final ObjectNode itemsVal = JsonNodeFactory.instance.objectNode();
558 final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
559 processElementCount(optConstraint, props);
561 processTypeDef(listNode.getType(), listNode, itemsVal, stack, definitions, definitionNames, oaversion);
562 props.set(ITEMS_KEY, itemsVal);
564 props.put(DESCRIPTION_KEY, listNode.getDescription().orElse(""));
569 private static void processElementCount(final Optional<ElementCountConstraint> constraint, final ObjectNode props) {
570 if (constraint.isPresent()) {
571 final ElementCountConstraint constr = constraint.get();
572 final Integer minElements = constr.getMinElements();
573 if (minElements != null) {
574 props.put(MIN_ITEMS, minElements);
576 final Integer maxElements = constr.getMaxElements();
577 if (maxElements != null) {
578 props.put(MAX_ITEMS, maxElements);
583 private static void processMandatory(final MandatoryAware node, final String nodeName, final ArrayNode required) {
584 if (node.isMandatory()) {
585 required.add(nodeName);
589 private ObjectNode processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
590 final ObjectNode properties, final ArrayNode required,
591 final SchemaInferenceStack stack, final ObjectNode definitions,
592 final DefinitionNames definitionNames, final OAversion oaversion) {
593 final ObjectNode property = JsonNodeFactory.instance.objectNode();
595 final String leafDescription = leafNode.getDescription().orElse("");
597 Description can't be added, because nothing allowed alongside $ref.
598 allOf is not an option, because ServiceNow can't parse it.
600 if (!(leafNode.getType() instanceof IdentityrefTypeDefinition)) {
601 property.put(DESCRIPTION_KEY, leafDescription);
604 processTypeDef(leafNode.getType(), leafNode, property, stack, definitions, definitionNames, oaversion);
605 properties.set(jsonLeafName, property);
606 property.set(XML_KEY, buildXmlParameter(leafNode));
607 processMandatory(leafNode, jsonLeafName, required);
612 private static ObjectNode processAnydataNode(final AnydataSchemaNode leafNode, final String name,
613 final ObjectNode properties, final ArrayNode required) {
614 final ObjectNode property = JsonNodeFactory.instance.objectNode();
616 final String leafDescription = leafNode.getDescription().orElse("");
617 property.put(DESCRIPTION_KEY, leafDescription);
619 final String localName = leafNode.getQName().getLocalName();
620 setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
621 property.put(TYPE_KEY, STRING_TYPE);
622 property.set(XML_KEY, buildXmlParameter(leafNode));
623 processMandatory(leafNode, name, required);
624 properties.set(name, property);
629 private static ObjectNode processAnyXMLNode(final AnyxmlSchemaNode leafNode, final String name,
630 final ObjectNode properties, final ArrayNode required) {
631 final ObjectNode property = JsonNodeFactory.instance.objectNode();
633 final String leafDescription = leafNode.getDescription().orElse("");
634 property.put(DESCRIPTION_KEY, leafDescription);
636 final String localName = leafNode.getQName().getLocalName();
637 setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
638 property.put(TYPE_KEY, STRING_TYPE);
639 property.set(XML_KEY, buildXmlParameter(leafNode));
640 processMandatory(leafNode, name, required);
641 properties.set(name, property);
646 private String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
647 final ObjectNode property, final SchemaInferenceStack stack,
648 final ObjectNode definitions, final DefinitionNames definitionNames,
649 final OAversion oaversion) {
650 final String jsonType;
651 if (leafTypeDef instanceof BinaryTypeDefinition) {
652 jsonType = processBinaryType(property);
654 } else if (leafTypeDef instanceof BitsTypeDefinition) {
655 jsonType = processBitsType((BitsTypeDefinition) leafTypeDef, property);
657 } else if (leafTypeDef instanceof EnumTypeDefinition) {
658 jsonType = processEnumType((EnumTypeDefinition) leafTypeDef, property);
660 } else if (leafTypeDef instanceof IdentityrefTypeDefinition) {
661 jsonType = processIdentityRefType((IdentityrefTypeDefinition) leafTypeDef, property, definitions,
662 definitionNames, oaversion, stack.getEffectiveModelContext());
664 } else if (leafTypeDef instanceof StringTypeDefinition) {
665 jsonType = processStringType(leafTypeDef, property, node.getQName().getLocalName());
667 } else if (leafTypeDef instanceof UnionTypeDefinition) {
668 jsonType = processUnionType((UnionTypeDefinition) leafTypeDef);
670 } else if (leafTypeDef instanceof EmptyTypeDefinition) {
671 jsonType = OBJECT_TYPE;
672 } else if (leafTypeDef instanceof LeafrefTypeDefinition) {
673 return processTypeDef(stack.resolveLeafref((LeafrefTypeDefinition) leafTypeDef), node, property,
674 stack, definitions, definitionNames, oaversion);
675 } else if (leafTypeDef instanceof BooleanTypeDefinition) {
676 jsonType = BOOLEAN_TYPE;
677 setDefaultValue(property, true);
678 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
679 jsonType = processNumberType((RangeRestrictedTypeDefinition<?, ?>) leafTypeDef, property);
680 } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
681 jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
683 jsonType = STRING_TYPE;
685 if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
686 putIfNonNull(property, TYPE_KEY, jsonType);
687 if (leafTypeDef.getDefaultValue().isPresent()) {
688 final Object defaultValue = leafTypeDef.getDefaultValue().get();
689 if (defaultValue instanceof String stringDefaultValue) {
690 if (leafTypeDef instanceof BooleanTypeDefinition) {
691 setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
692 } else if (leafTypeDef instanceof DecimalTypeDefinition
693 || leafTypeDef instanceof Uint64TypeDefinition) {
694 setDefaultValue(property, new BigDecimal(stringDefaultValue));
695 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
696 //uint8,16,32 int8,16,32,64
697 if (isHexadecimalOrOctal((RangeRestrictedTypeDefinition<?, ?>)leafTypeDef)) {
698 setDefaultValue(property, stringDefaultValue);
700 setDefaultValue(property, Long.valueOf(stringDefaultValue));
703 setDefaultValue(property, stringDefaultValue);
706 //we should never get here. getDefaultValue always gives us string
707 setDefaultValue(property, defaultValue.toString());
714 private static String processBinaryType(final ObjectNode property) {
715 property.put(FORMAT_KEY, "byte");
719 private static String processEnumType(final EnumTypeDefinition enumLeafType,
720 final ObjectNode property) {
721 final List<EnumPair> enumPairs = enumLeafType.getValues();
722 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
723 for (final EnumPair enumPair : enumPairs) {
724 enumNames.add(new TextNode(enumPair.getName()));
727 property.set(ENUM_KEY, enumNames);
728 setDefaultValue(property, enumLeafType.getValues().iterator().next().getName());
732 private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
733 final ObjectNode definitions, final DefinitionNames definitionNames,
734 final OAversion oaversion, final EffectiveModelContext schemaContext) {
735 final String definitionName;
736 if (isImported(leafTypeDef)) {
737 definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
739 final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
740 definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
742 property.put(REF_KEY, getAppropriateModelPrefix(oaversion) + definitionName);
746 private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
747 final ObjectNode definitions, final DefinitionNames definitionNames,
748 final EffectiveModelContext context) {
749 final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
750 final String identityName = idNode.getQName().getLocalName();
751 if (!definitionNames.isListedNode(idNode)) {
752 final ObjectNode identityObj = buildIdentityObject(idNode, context);
753 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
754 final String name = identityName + discriminator;
755 definitions.set(name, identityObj);
758 return identityName + definitionNames.getDiscriminator(idNode);
762 private static ObjectNode buildIdentityObject(final IdentitySchemaNode idNode,
763 final EffectiveModelContext context) {
764 final ObjectNode identityObj = JsonNodeFactory.instance.objectNode();
765 final String identityName = idNode.getQName().getLocalName();
766 LOG.debug("Processing Identity: {}", identityName);
768 identityObj.put(TITLE_KEY, identityName);
769 identityObj.put(DESCRIPTION_KEY, idNode.getDescription().orElse(""));
771 final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
773 final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
774 enumPayload.add(identityName);
775 populateEnumWithDerived(derivedIds, enumPayload, context);
776 identityObj.set(ENUM_KEY, enumPayload);
777 identityObj.put(TYPE_KEY, STRING_TYPE);
781 private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
782 return !leafTypeDef.getQName().getModule().equals(topLevelModule.getQNameModule());
785 private static String processBitsType(final BitsTypeDefinition bitsType,
786 final ObjectNode property) {
787 property.put(MIN_ITEMS, 0);
788 property.put(UNIQUE_ITEMS_KEY, true);
789 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
790 final Collection<? extends Bit> bits = bitsType.getBits();
791 for (final Bit bit : bits) {
792 enumNames.add(new TextNode(bit.getName()));
794 property.set(ENUM_KEY, enumNames);
795 property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
799 private static String processStringType(final TypeDefinition<?> stringType, final ObjectNode property,
800 final String nodeName) {
801 StringTypeDefinition type = (StringTypeDefinition) stringType;
802 Optional<LengthConstraint> lengthConstraints = ((StringTypeDefinition) stringType).getLengthConstraint();
803 while (lengthConstraints.isEmpty() && type.getBaseType() != null) {
804 type = type.getBaseType();
805 lengthConstraints = type.getLengthConstraint();
808 if (lengthConstraints.isPresent()) {
809 final Range<Integer> range = lengthConstraints.get().getAllowedRanges().span();
810 putIfNonNull(property, MIN_LENGTH_KEY, range.lowerEndpoint());
811 putIfNonNull(property, MAX_LENGTH_KEY, range.upperEndpoint());
814 if (type.getPatternConstraints().iterator().hasNext()) {
815 final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
816 String regex = pattern.getJavaPatternString();
817 regex = regex.substring(1, regex.length() - 1);
818 // Escape special characters to prevent issues inside Generex.
819 regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
820 String defaultValue = "";
822 final Generex generex = new Generex(regex);
823 defaultValue = generex.random();
824 } catch (IllegalArgumentException ex) {
825 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
827 setDefaultValue(property, defaultValue);
829 setDefaultValue(property, "Some " + nodeName);
834 private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
835 final ObjectNode property) {
836 final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
837 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
839 if (isHexadecimalOrOctal(leafTypeDef)) {
843 if (leafTypeDef instanceof DecimalTypeDefinition) {
844 maybeLower.ifPresent(number -> setDefaultValue(property, ((Decimal64) number).decimalValue()));
847 if (leafTypeDef instanceof Uint8TypeDefinition
848 || leafTypeDef instanceof Uint16TypeDefinition
849 || leafTypeDef instanceof Int8TypeDefinition
850 || leafTypeDef instanceof Int16TypeDefinition
851 || leafTypeDef instanceof Int32TypeDefinition) {
853 property.put(FORMAT_KEY, INT32_FORMAT);
854 maybeLower.ifPresent(number -> setDefaultValue(property, Integer.valueOf(number.toString())));
855 } else if (leafTypeDef instanceof Uint32TypeDefinition
856 || leafTypeDef instanceof Int64TypeDefinition) {
858 property.put(FORMAT_KEY, INT64_FORMAT);
859 maybeLower.ifPresent(number -> setDefaultValue(property, Long.valueOf(number.toString())));
862 setDefaultValue(property, 0);
867 private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
868 final Optional<?> optDefaultValue = typeDef.getDefaultValue();
869 if (optDefaultValue.isPresent()) {
870 final String defaultValue = (String) optDefaultValue.get();
871 return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
876 private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
877 final EffectiveModelContext schemaContext) {
878 // create example instance-identifier to the first container of node's module if exists or leave it empty
879 final var module = schemaContext.findModule(node.getQName().getModule());
880 if (module.isPresent()) {
881 final var container = module.get().getChildNodes().stream()
882 .filter(n -> n instanceof ContainerSchemaNode)
884 container.ifPresent(c -> setDefaultValue(property, String.format("/%s:%s", module.get().getPrefix(),
885 c.getQName().getLocalName())));
891 private static String processUnionType(final UnionTypeDefinition unionType) {
892 boolean isStringTakePlace = false;
893 boolean isNumberTakePlace = false;
894 boolean isBooleanTakePlace = false;
895 for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
896 if (!isStringTakePlace) {
897 if (typeDef instanceof StringTypeDefinition
898 || typeDef instanceof BitsTypeDefinition
899 || typeDef instanceof BinaryTypeDefinition
900 || typeDef instanceof IdentityrefTypeDefinition
901 || typeDef instanceof EnumTypeDefinition
902 || typeDef instanceof LeafrefTypeDefinition
903 || typeDef instanceof UnionTypeDefinition) {
904 isStringTakePlace = true;
905 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
906 isNumberTakePlace = true;
907 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
908 isBooleanTakePlace = true;
912 if (isStringTakePlace) {
915 if (isBooleanTakePlace) {
916 if (isNumberTakePlace) {
924 private static ObjectNode buildXmlParameter(final SchemaNode node) {
925 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
926 final QName qName = node.getQName();
927 xml.put(NAME_KEY, qName.getLocalName());
928 xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
932 private static void putIfNonNull(final ObjectNode property, final String key, final Number number) {
933 if (key != null && number != null) {
934 if (number instanceof Double) {
935 property.put(key, (Double) number);
936 } else if (number instanceof Float) {
937 property.put(key, (Float) number);
938 } else if (number instanceof Integer) {
939 property.put(key, (Integer) number);
940 } else if (number instanceof Short) {
941 property.put(key, (Short) number);
942 } else if (number instanceof Long) {
943 property.put(key, (Long) number);
948 private static void putIfNonNull(final ObjectNode property, final String key, final String value) {
949 if (key != null && value != null) {
950 property.put(key, value);
954 private static void setRequiredIfNotEmpty(final ObjectNode node, final ArrayNode required) {
955 if (required.size() > 0) {
956 node.set(REQUIRED_KEY, required);
960 private static void setDefaultValue(final ObjectNode property, final String value) {
961 property.put(DEFAULT_KEY, value);
964 private static void setDefaultValue(final ObjectNode property, final Integer value) {
965 property.put(DEFAULT_KEY, value);
968 private static void setDefaultValue(final ObjectNode property, final Long value) {
969 property.put(DEFAULT_KEY, value);
972 private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
973 property.put(DEFAULT_KEY, value);
976 private static void setDefaultValue(final ObjectNode property, final Boolean value) {
977 property.put(DEFAULT_KEY, value);