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, final OAversion oaversion,
148 final boolean isForSingleModule) throws IOException {
149 topLevelModule = module;
151 processIdentities(module, definitions, definitionNames, schemaContext);
152 processContainersAndLists(module, definitions, definitionNames, schemaContext, oaversion);
153 processRPCs(module, definitions, definitionNames, schemaContext, oaversion);
155 if (isForSingleModule) {
156 processModule(module, definitions, definitionNames, schemaContext, oaversion);
162 public ObjectNode convertToJsonSchema(final Module module, final EffectiveModelContext schemaContext,
163 final DefinitionNames definitionNames, final OAversion oaversion, final boolean isForSingleModule)
165 final ObjectNode definitions = JsonNodeFactory.instance.objectNode();
166 if (isForSingleModule) {
167 definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
169 return convertToJsonSchema(module, schemaContext, definitions, definitionNames, oaversion, isForSingleModule);
172 private void processModule(final Module module, final ObjectNode definitions, final DefinitionNames definitionNames,
173 final EffectiveModelContext schemaContext, final OAversion oaversion) {
174 final ObjectNode definition = JsonNodeFactory.instance.objectNode();
175 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
176 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
177 final String moduleName = module.getName();
178 final String definitionName = moduleName + MODULE_NAME_SUFFIX;
179 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
180 for (final DataSchemaNode node : module.getChildNodes()) {
181 stack.enterSchemaTree(node.getQName());
182 final String localName = node.getQName().getLocalName();
183 if (node.isConfiguration()) {
184 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
185 for (final DataSchemaNode childNode : ((DataNodeContainer) node).getChildNodes()) {
186 final ObjectNode childNodeProperties = JsonNodeFactory.instance.objectNode();
188 final String ref = getAppropriateModelPrefix(oaversion)
189 + moduleName + CONFIG
191 + definitionNames.getDiscriminator(node);
193 if (node instanceof ListSchemaNode) {
194 childNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
195 final ObjectNode items = JsonNodeFactory.instance.objectNode();
196 items.put(REF_KEY, ref);
197 childNodeProperties.set(ITEMS_KEY, items);
198 childNodeProperties.put(DESCRIPTION_KEY, childNode.getDescription().orElse(""));
199 childNodeProperties.put(TITLE_KEY, localName + CONFIG);
202 Description can't be added, because nothing allowed alongside $ref.
203 allOf is not an option, because ServiceNow can't parse it.
205 childNodeProperties.put(REF_KEY, ref);
207 //add module name prefix to property name, when ServiceNow can process colons
208 properties.set(localName, childNodeProperties);
210 } else if (node instanceof LeafSchemaNode) {
212 Add module name prefix to property name, when ServiceNow can process colons(second parameter
215 processLeafNode((LeafSchemaNode) node, localName, properties, required, stack,
216 definitions, definitionNames, oaversion);
221 definition.put(TITLE_KEY, definitionName);
222 definition.put(TYPE_KEY, OBJECT_TYPE);
223 definition.set(PROPERTIES_KEY, properties);
224 definition.put(DESCRIPTION_KEY, module.getDescription().orElse(""));
225 setRequiredIfNotEmpty(definition, required);
227 definitions.set(definitionName, definition);
230 private void processContainersAndLists(final Module module, final ObjectNode definitions,
231 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext,
232 final OAversion oaversion) throws IOException {
233 final String moduleName = module.getName();
234 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
235 for (final DataSchemaNode childNode : module.getChildNodes()) {
236 stack.enterSchemaTree(childNode.getQName());
237 // For every container and list in the module
238 if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
239 if (childNode.isConfiguration()) {
240 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
241 true, stack, oaversion);
243 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
244 false, stack, oaversion);
245 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack, oaversion);
251 private void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
252 final ObjectNode definitions, final DefinitionNames definitionNames, final SchemaInferenceStack stack,
253 final OAversion oaversion) throws IOException {
254 for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
255 stack.enterSchemaTree(actionDef.getQName());
256 processOperations(actionDef, moduleName, definitions, definitionNames, stack, oaversion);
261 private void processRPCs(final Module module, final ObjectNode definitions, final DefinitionNames definitionNames,
262 final EffectiveModelContext schemaContext, final OAversion oaversion) throws IOException {
263 final String moduleName = module.getName();
264 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
265 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
266 stack.enterSchemaTree(rpcDefinition.getQName());
267 processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack, oaversion);
272 private void processOperations(final OperationDefinition operationDef, final String parentName,
273 final ObjectNode definitions, final DefinitionNames definitionNames,
274 final SchemaInferenceStack stack, final OAversion oaversion) throws IOException {
275 final String operationName = operationDef.getQName().getLocalName();
276 processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
277 definitionNames, stack, oaversion);
278 processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
279 definitionNames, stack, oaversion);
282 private void processOperationInputOutput(final ContainerLike container, final String operationName,
283 final String parentName, final boolean isInput, final ObjectNode definitions,
284 final DefinitionNames definitionNames, final SchemaInferenceStack stack, final OAversion oaversion)
286 stack.enterSchemaTree(container.getQName());
287 if (!container.getChildNodes().isEmpty()) {
288 final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
289 final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
290 processChildren(childSchema, container.getChildNodes(), parentName, definitions, definitionNames,
291 false, stack, oaversion);
293 childSchema.put(TYPE_KEY, OBJECT_TYPE);
294 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
295 xml.put(NAME_KEY, isInput ? INPUT : OUTPUT);
296 childSchema.set(XML_KEY, xml);
297 childSchema.put(TITLE_KEY, filename);
298 final String discriminator =
299 definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
300 definitions.set(filename + discriminator, childSchema);
302 processTopData(filename, discriminator, definitions, container, oaversion);
307 private static ObjectNode processTopData(final String filename, final String discriminator,
308 final ObjectNode definitions, final SchemaNode schemaNode, final OAversion oaversion) {
309 final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
310 final String name = filename + discriminator;
311 final String ref = getAppropriateModelPrefix(oaversion) + name;
312 final String topName = filename + TOP;
314 if (schemaNode instanceof ListSchemaNode) {
315 dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
316 final ObjectNode items = JsonNodeFactory.instance.objectNode();
317 items.put(REF_KEY, ref);
318 dataNodeProperties.set(ITEMS_KEY, items);
319 dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
322 Description can't be added, because nothing allowed alongside $ref.
323 allOf is not an option, because ServiceNow can't parse it.
325 dataNodeProperties.put(REF_KEY, ref);
328 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
330 Add module name prefix to property name, when needed, when ServiceNow can process colons,
331 use RestDocGenUtil#resolveNodesName for creating property name
333 properties.set(schemaNode.getQName().getLocalName(), dataNodeProperties);
334 final ObjectNode finalChildSchema = JsonNodeFactory.instance.objectNode();
335 finalChildSchema.put(TYPE_KEY, OBJECT_TYPE);
336 finalChildSchema.set(PROPERTIES_KEY, properties);
337 finalChildSchema.put(TITLE_KEY, topName);
340 definitions.set(topName + discriminator, finalChildSchema);
342 return dataNodeProperties;
346 * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
347 * @param module The module from which the identity stmt will be processed
348 * @param definitions The ObjectNode in which the parsed identity will be put as a 'model' obj
349 * @param definitionNames Store for definition names
351 private static void processIdentities(final Module module, final ObjectNode definitions,
352 final DefinitionNames definitionNames, final EffectiveModelContext context) {
353 final String moduleName = module.getName();
354 final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
355 LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
357 for (final IdentitySchemaNode idNode : idNodes) {
358 final ObjectNode identityObj = buildIdentityObject(idNode, context);
359 final String idName = idNode.getQName().getLocalName();
360 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
361 final String name = idName + discriminator;
362 definitions.set(name, identityObj);
366 private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
367 final ArrayNode enumPayload, final EffectiveModelContext context) {
368 for (final IdentitySchemaNode derivedId : derivedIds) {
369 enumPayload.add(derivedId.getQName().getLocalName());
370 populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
374 private ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
375 final ObjectNode definitions, final DefinitionNames definitionNames, final boolean isConfig,
376 final SchemaInferenceStack stack, final OAversion oaversion) throws IOException {
377 if (dataNode instanceof ListSchemaNode || dataNode instanceof ContainerSchemaNode) {
378 final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
379 final SchemaNode schemaNode = (SchemaNode) dataNode;
380 final String localName = schemaNode.getQName().getLocalName();
381 final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
382 final String nameAsParent = parentName + "_" + localName;
383 final ObjectNode properties =
384 processChildren(childSchema, containerChildren, parentName + "_" + localName, definitions,
385 definitionNames, isConfig, stack, oaversion);
387 final String nodeName = parentName + (isConfig ? CONFIG : "") + "_" + localName;
388 final String postNodeName = parentName + CONFIG + "_" + localName + POST_SUFFIX;
389 final String postXmlNodeName = postNodeName + XML_SUFFIX;
390 final String parentNameConfigLocalName = parentName + CONFIG + "_" + localName;
392 final String description = schemaNode.getDescription().orElse("");
393 final String discriminator;
395 if (!definitionNames.isListedNode(schemaNode)) {
396 final List<String> names = List.of(parentNameConfigLocalName,
397 parentNameConfigLocalName + TOP,
402 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
404 discriminator = definitionNames.getDiscriminator(schemaNode);
408 final ObjectNode postSchema = createPostJsonSchema(schemaNode, properties, postNodeName, description);
409 String truePostNodeName = postNodeName + discriminator;
410 definitions.set(truePostNodeName, postSchema);
412 final ObjectNode postXmlSchema = JsonNodeFactory.instance.objectNode();
413 postXmlSchema.put(REF_KEY, getAppropriateModelPrefix(oaversion) + truePostNodeName);
414 definitions.set(postXmlNodeName + discriminator, postXmlSchema);
417 childSchema.put(TYPE_KEY, OBJECT_TYPE);
418 childSchema.set(PROPERTIES_KEY, properties);
419 childSchema.put(TITLE_KEY, nodeName);
420 childSchema.put(DESCRIPTION_KEY, description);
422 final String defName = nodeName + discriminator;
423 childSchema.set(XML_KEY, buildXmlParameter(schemaNode));
424 definitions.set(defName, childSchema);
426 return processTopData(nodeName, discriminator, definitions, schemaNode, oaversion);
431 private static ObjectNode createPostJsonSchema(final SchemaNode dataNode, final ObjectNode properties,
432 final String postNodeName, final String description) {
433 final ObjectNode postSchema = JsonNodeFactory.instance.objectNode();
434 final ObjectNode postItemProperties;
435 if (dataNode instanceof ListSchemaNode) {
436 postItemProperties = createListItemProperties(properties, (ListSchemaNode) dataNode);
438 postItemProperties = properties.deepCopy();
440 postSchema.put(TYPE_KEY, OBJECT_TYPE);
441 postSchema.set(PROPERTIES_KEY, postItemProperties);
442 postSchema.put(TITLE_KEY, postNodeName);
443 postSchema.put(DESCRIPTION_KEY, description);
444 postSchema.set(XML_KEY, buildXmlParameter(dataNode));
448 private static ObjectNode createListItemProperties(final ObjectNode properties, final ListSchemaNode listNode) {
449 final ObjectNode postListItemProperties = JsonNodeFactory.instance.objectNode();
450 final List<QName> keyDefinition = listNode.getKeyDefinition();
451 final Set<String> keys = listNode.getChildNodes().stream()
452 .filter(node -> keyDefinition.contains(node.getQName()))
453 .map(node -> node.getQName().getLocalName())
454 .collect(Collectors.toSet());
456 Iterator<Map.Entry<String, JsonNode>> it = properties.fields();
457 while (it.hasNext()) {
458 Map.Entry<String, JsonNode> property = it.next();
459 if (!keys.contains(property.getKey())) {
460 postListItemProperties.set(property.getKey(), property.getValue());
464 return postListItemProperties;
468 * Processes the nodes.
470 private ObjectNode processChildren(final ObjectNode parentNode, final Collection<? extends DataSchemaNode> nodes,
471 final String parentName, final ObjectNode definitions, final DefinitionNames definitionNames,
472 final boolean isConfig, final SchemaInferenceStack stack, final OAversion oaversion) throws IOException {
473 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
474 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
475 for (final DataSchemaNode node : nodes) {
476 if (!isConfig || node.isConfiguration()) {
477 processChildNode(node, parentName, definitions, definitionNames, isConfig, stack, properties,
481 parentNode.set(PROPERTIES_KEY, properties);
482 setRequiredIfNotEmpty(parentNode, required);
486 private void processChildNode(final DataSchemaNode node, final String parentName, final ObjectNode definitions,
487 final DefinitionNames definitionNames, final boolean isConfig, final SchemaInferenceStack stack,
488 final ObjectNode properties, final OAversion oaversion) throws IOException {
490 stack.enterSchemaTree(node.getQName());
493 Add module name prefix to property name, when needed, when ServiceNow can process colons,
494 use RestDocGenUtil#resolveNodesName for creating property name
496 final String name = node.getQName().getLocalName();
498 if (node instanceof LeafSchemaNode leaf) {
499 processLeafNode(leaf, name, properties, JsonNodeFactory.instance.arrayNode(), stack, definitions,
500 definitionNames, oaversion);
502 } else if (node instanceof AnyxmlSchemaNode anyxml) {
503 processAnyXMLNode(anyxml, name, properties, JsonNodeFactory.instance.arrayNode());
505 } else if (node instanceof AnydataSchemaNode anydata) {
506 processAnydataNode(anydata, name, properties, JsonNodeFactory.instance.arrayNode());
510 final ObjectNode property;
511 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
512 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions,
513 definitionNames, isConfig, stack, oaversion);
515 processActionNodeContainer(node, parentName, definitions, definitionNames, stack, oaversion);
517 } else if (node instanceof LeafListSchemaNode leafList) {
518 property = processLeafListNode(leafList, stack, definitions, definitionNames, oaversion);
520 } else if (node instanceof ChoiceSchemaNode choice) {
521 for (final CaseSchemaNode variant : choice.getCases()) {
522 stack.enterSchemaTree(variant.getQName());
523 for (final DataSchemaNode childNode : variant.getChildNodes()) {
524 processChildNode(childNode, parentName, definitions, definitionNames, isConfig, stack,
525 properties, oaversion);
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, final OAversion oaversion) {
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, oaversion);
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.get();
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, final OAversion oaversion) {
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, oaversion);
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, final OAversion oaversion) {
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, oaversion, 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, oaversion);
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().get();
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,
708 final ObjectNode property) {
709 final List<EnumPair> enumPairs = enumLeafType.getValues();
710 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
711 for (final EnumPair enumPair : enumPairs) {
712 enumNames.add(new TextNode(enumPair.getName()));
715 property.set(ENUM_KEY, enumNames);
716 setDefaultValue(property, enumLeafType.getValues().iterator().next().getName());
720 private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
721 final ObjectNode definitions, final DefinitionNames definitionNames, final OAversion oaversion,
722 final EffectiveModelContext schemaContext) {
723 final String definitionName;
724 if (isImported(leafTypeDef)) {
725 definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
727 final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
728 definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
730 property.put(REF_KEY, getAppropriateModelPrefix(oaversion) + definitionName);
734 private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
735 final ObjectNode definitions, final DefinitionNames definitionNames, final EffectiveModelContext context) {
736 final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
737 final String identityName = idNode.getQName().getLocalName();
738 if (!definitionNames.isListedNode(idNode)) {
739 final ObjectNode identityObj = buildIdentityObject(idNode, context);
740 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
741 final String name = identityName + discriminator;
742 definitions.set(name, identityObj);
745 return identityName + definitionNames.getDiscriminator(idNode);
749 private static ObjectNode buildIdentityObject(final IdentitySchemaNode idNode,
750 final EffectiveModelContext context) {
751 final ObjectNode identityObj = JsonNodeFactory.instance.objectNode();
752 final String identityName = idNode.getQName().getLocalName();
753 LOG.debug("Processing Identity: {}", identityName);
755 identityObj.put(TITLE_KEY, identityName);
756 identityObj.put(DESCRIPTION_KEY, idNode.getDescription().orElse(""));
758 final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
760 final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
761 enumPayload.add(identityName);
762 populateEnumWithDerived(derivedIds, enumPayload, context);
763 identityObj.set(ENUM_KEY, enumPayload);
764 identityObj.put(TYPE_KEY, STRING_TYPE);
768 private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
769 return !leafTypeDef.getQName().getModule().equals(topLevelModule.getQNameModule());
772 private static String processBitsType(final BitsTypeDefinition bitsType, final ObjectNode property) {
773 property.put(MIN_ITEMS, 0);
774 property.put(UNIQUE_ITEMS_KEY, true);
775 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
776 final Collection<? extends Bit> bits = bitsType.getBits();
777 for (final Bit bit : bits) {
778 enumNames.add(new TextNode(bit.getName()));
780 property.set(ENUM_KEY, enumNames);
781 property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
785 private static String processStringType(final TypeDefinition<?> stringType, final ObjectNode property,
786 final String nodeName) {
787 StringTypeDefinition type = (StringTypeDefinition) stringType;
788 Optional<LengthConstraint> lengthConstraints = ((StringTypeDefinition) stringType).getLengthConstraint();
789 while (lengthConstraints.isEmpty() && type.getBaseType() != null) {
790 type = type.getBaseType();
791 lengthConstraints = type.getLengthConstraint();
794 if (lengthConstraints.isPresent()) {
795 final Range<Integer> range = lengthConstraints.get().getAllowedRanges().span();
796 putIfNonNull(property, MIN_LENGTH_KEY, range.lowerEndpoint());
797 putIfNonNull(property, MAX_LENGTH_KEY, range.upperEndpoint());
800 if (type.getPatternConstraints().iterator().hasNext()) {
801 final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
802 String regex = pattern.getJavaPatternString();
803 regex = regex.substring(1, regex.length() - 1);
804 // Escape special characters to prevent issues inside Generex.
805 regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
806 String defaultValue = "";
808 final Generex generex = new Generex(regex);
809 defaultValue = generex.random();
810 } catch (IllegalArgumentException ex) {
811 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
813 setDefaultValue(property, defaultValue);
815 setDefaultValue(property, "Some " + nodeName);
820 private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
821 final ObjectNode property) {
822 final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
823 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
825 if (isHexadecimalOrOctal(leafTypeDef)) {
829 if (leafTypeDef instanceof DecimalTypeDefinition) {
830 maybeLower.ifPresent(number -> setDefaultValue(property, ((Decimal64) number).decimalValue()));
833 if (leafTypeDef instanceof Uint8TypeDefinition
834 || leafTypeDef instanceof Uint16TypeDefinition
835 || leafTypeDef instanceof Int8TypeDefinition
836 || leafTypeDef instanceof Int16TypeDefinition
837 || leafTypeDef instanceof Int32TypeDefinition) {
839 property.put(FORMAT_KEY, INT32_FORMAT);
840 maybeLower.ifPresent(number -> setDefaultValue(property, Integer.valueOf(number.toString())));
841 } else if (leafTypeDef instanceof Uint32TypeDefinition
842 || leafTypeDef instanceof Int64TypeDefinition) {
844 property.put(FORMAT_KEY, INT64_FORMAT);
845 maybeLower.ifPresent(number -> setDefaultValue(property, Long.valueOf(number.toString())));
848 setDefaultValue(property, 0);
853 private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
854 final Optional<?> optDefaultValue = typeDef.getDefaultValue();
855 if (optDefaultValue.isPresent()) {
856 final String defaultValue = (String) optDefaultValue.get();
857 return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
862 private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
863 final EffectiveModelContext schemaContext) {
864 // create example instance-identifier to the first container of node's module if exists or leave it empty
865 final var module = schemaContext.findModule(node.getQName().getModule());
866 if (module.isPresent()) {
867 final var container = module.get().getChildNodes().stream()
868 .filter(n -> n instanceof ContainerSchemaNode)
870 container.ifPresent(c -> setDefaultValue(property, String.format("/%s:%s", module.get().getPrefix(),
871 c.getQName().getLocalName())));
877 private static String processUnionType(final UnionTypeDefinition unionType) {
878 boolean isStringTakePlace = false;
879 boolean isNumberTakePlace = false;
880 boolean isBooleanTakePlace = false;
881 for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
882 if (!isStringTakePlace) {
883 if (typeDef instanceof StringTypeDefinition
884 || typeDef instanceof BitsTypeDefinition
885 || typeDef instanceof BinaryTypeDefinition
886 || typeDef instanceof IdentityrefTypeDefinition
887 || typeDef instanceof EnumTypeDefinition
888 || typeDef instanceof LeafrefTypeDefinition
889 || typeDef instanceof UnionTypeDefinition) {
890 isStringTakePlace = true;
891 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
892 isNumberTakePlace = true;
893 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
894 isBooleanTakePlace = true;
898 if (isStringTakePlace) {
901 if (isBooleanTakePlace) {
902 if (isNumberTakePlace) {
910 private static ObjectNode buildXmlParameter(final SchemaNode node) {
911 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
912 final QName qName = node.getQName();
913 xml.put(NAME_KEY, qName.getLocalName());
914 xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
918 private static void putIfNonNull(final ObjectNode property, final String key, final Number number) {
919 if (key != null && number != null) {
920 if (number instanceof Double) {
921 property.put(key, (Double) number);
922 } else if (number instanceof Float) {
923 property.put(key, (Float) number);
924 } else if (number instanceof Integer) {
925 property.put(key, (Integer) number);
926 } else if (number instanceof Short) {
927 property.put(key, (Short) number);
928 } else if (number instanceof Long) {
929 property.put(key, (Long) number);
934 private static void putIfNonNull(final ObjectNode property, final String key, final String value) {
935 if (key != null && value != null) {
936 property.put(key, value);
940 private static void setRequiredIfNotEmpty(final ObjectNode node, final ArrayNode required) {
941 if (required.size() > 0) {
942 node.set(REQUIRED_KEY, required);
946 private static void setDefaultValue(final ObjectNode property, final String value) {
947 property.put(DEFAULT_KEY, value);
950 private static void setDefaultValue(final ObjectNode property, final Integer value) {
951 property.put(DEFAULT_KEY, value);
954 private static void setDefaultValue(final ObjectNode property, final Long value) {
955 property.put(DEFAULT_KEY, value);
958 private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
959 property.put(DEFAULT_KEY, value);
962 private static void setDefaultValue(final ObjectNode property, final Boolean value) {
963 property.put(DEFAULT_KEY, value);