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.restconf.openapi.impl;
10 import static org.opendaylight.restconf.openapi.impl.BaseYangOpenApiGenerator.MODULE_NAME_SUFFIX;
11 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.COMPONENTS_PREFIX;
12 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.NAME_KEY;
13 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.TOP;
14 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.XML_KEY;
16 import com.fasterxml.jackson.databind.node.ArrayNode;
17 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
18 import com.fasterxml.jackson.databind.node.ObjectNode;
19 import com.fasterxml.jackson.databind.node.TextNode;
20 import com.google.common.collect.Range;
21 import com.google.common.collect.RangeSet;
22 import dk.brics.automaton.RegExp;
23 import java.io.IOException;
24 import java.math.BigDecimal;
25 import java.util.Collection;
26 import java.util.HashMap;
27 import java.util.List;
29 import java.util.Optional;
30 import java.util.regex.Pattern;
31 import org.opendaylight.restconf.openapi.model.Schema;
32 import org.opendaylight.yangtools.yang.common.Decimal64;
33 import org.opendaylight.yangtools.yang.common.QName;
34 import org.opendaylight.yangtools.yang.common.XMLNamespace;
35 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
36 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
37 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
41 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
43 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
45 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraint;
46 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraintAware;
47 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.MandatoryAware;
52 import org.opendaylight.yangtools.yang.model.api.Module;
53 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
54 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
55 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
57 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
58 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
59 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition.Bit;
60 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
61 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
62 import org.opendaylight.yangtools.yang.model.api.type.EmptyTypeDefinition;
63 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
64 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition.EnumPair;
65 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
66 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
67 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
68 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
69 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
70 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
71 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
72 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
73 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
74 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
75 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
76 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
77 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
78 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
79 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
80 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
81 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
82 import org.slf4j.Logger;
83 import org.slf4j.LoggerFactory;
86 * Generates JSON Schema for data defined in YANG. This class is not thread-safe.
88 public class DefinitionGenerator {
90 private static final Logger LOG = LoggerFactory.getLogger(DefinitionGenerator.class);
92 private static final String UNIQUE_ITEMS_KEY = "uniqueItems";
93 private static final String MAX_ITEMS = "maxItems";
94 private static final String MIN_ITEMS = "minItems";
95 private static final String MAX_LENGTH_KEY = "maxLength";
96 private static final String MIN_LENGTH_KEY = "minLength";
97 private static final String REF_KEY = "$ref";
98 private static final String ITEMS_KEY = "items";
99 private static final String TYPE_KEY = "type";
100 private static final String DESCRIPTION_KEY = "description";
101 private static final String ARRAY_TYPE = "array";
102 private static final String ENUM_KEY = "enum";
103 private static final String TITLE_KEY = "title";
104 private static final String DEFAULT_KEY = "default";
105 private static final String EXAMPLE_KEY = "example";
106 private static final String FORMAT_KEY = "format";
107 private static final String NAMESPACE_KEY = "namespace";
108 public static final String INPUT = "input";
109 public static final String INPUT_SUFFIX = "_input";
110 public static final String OUTPUT = "output";
111 public static final String OUTPUT_SUFFIX = "_output";
112 private static final String STRING_TYPE = "string";
113 private static final String OBJECT_TYPE = "object";
114 private static final String NUMBER_TYPE = "number";
115 private static final String INTEGER_TYPE = "integer";
116 private static final String INT32_FORMAT = "int32";
117 private static final String INT64_FORMAT = "int64";
118 private static final String BOOLEAN_TYPE = "boolean";
119 // Special characters used in Automaton.
120 // See https://www.brics.dk/automaton/doc/dk/brics/automaton/RegExp.html
121 private static final Pattern AUTOMATON_SPECIAL_CHARACTERS = Pattern.compile("[@&\"<>#~]");
122 // Adaptation from YANG regex to Automaton regex
123 // See https://github.com/mifmif/Generex/blob/master/src/main/java/com/mifmif/common/regex/Generex.java
124 private static final Map<String, String> PREDEFINED_CHARACTER_CLASSES = Map.of("\\\\d", "[0-9]",
125 "\\\\D", "[^0-9]", "\\\\s", "[ \t\n\f\r]", "\\\\S", "[^ \t\n\f\r]",
126 "\\\\w", "[a-zA-Z_0-9]", "\\\\W", "[^a-zA-Z_0-9]");
128 private Module topLevelModule;
130 public DefinitionGenerator() {
134 * Creates Json definitions from provided module according to openapi spec.
136 * @param module - Yang module to be converted
137 * @param schemaContext - SchemaContext of all Yang files used by Api Doc
138 * @param definitionNames - Store for definition names
139 * @return {@link Map} containing data used for creating examples and definitions in OpenAPI documentation
140 * @throws IOException if I/O operation fails
142 public Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
143 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
144 final boolean isForSingleModule) throws IOException {
145 topLevelModule = module;
147 processIdentities(module, definitions, definitionNames, schemaContext);
148 processContainersAndLists(module, definitions, definitionNames, schemaContext);
149 processRPCs(module, definitions, definitionNames, schemaContext);
151 if (isForSingleModule) {
152 processModule(module, definitions, definitionNames, schemaContext);
158 public Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
159 final DefinitionNames definitionNames, final boolean isForSingleModule)
161 final Map<String, Schema> definitions = new HashMap<>();
162 if (isForSingleModule) {
163 definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
165 return convertToSchemas(module, schemaContext, definitions, definitionNames, isForSingleModule);
168 private void processModule(final Module module, final Map<String, Schema> definitions,
169 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) {
170 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
171 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
172 final String moduleName = module.getName();
173 final String definitionName = moduleName + MODULE_NAME_SUFFIX;
174 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
175 for (final DataSchemaNode node : module.getChildNodes()) {
176 stack.enterSchemaTree(node.getQName());
177 final String localName = node.getQName().getLocalName();
178 if (node.isConfiguration()) {
179 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
180 if (isSchemaNodeMandatory(node)) {
181 required.add(localName);
183 for (final DataSchemaNode childNode : ((DataNodeContainer) node).getChildNodes()) {
184 final ObjectNode childNodeProperties = JsonNodeFactory.instance.objectNode();
186 final String ref = COMPONENTS_PREFIX
189 + definitionNames.getDiscriminator(node);
191 if (node instanceof ListSchemaNode) {
192 childNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
193 final ObjectNode items = JsonNodeFactory.instance.objectNode();
194 items.put(REF_KEY, ref);
195 childNodeProperties.set(ITEMS_KEY, items);
196 childNodeProperties.put(DESCRIPTION_KEY, childNode.getDescription().orElse(""));
197 childNodeProperties.put(TITLE_KEY, localName);
200 Description can't be added, because nothing allowed alongside $ref.
201 allOf is not an option, because ServiceNow can't parse it.
203 childNodeProperties.put(REF_KEY, ref);
205 //add module name prefix to property name, when ServiceNow can process colons
206 properties.set(localName, childNodeProperties);
208 } else if (node instanceof LeafSchemaNode) {
210 Add module name prefix to property name, when ServiceNow can process colons(second parameter
213 final ObjectNode leafNode = processLeafNode((LeafSchemaNode) node, localName, required, stack,
214 definitions, definitionNames, module.getNamespace());
215 properties.set(localName, leafNode);
220 final Schema.Builder definitionBuilder = new Schema.Builder()
221 .title(definitionName)
223 .properties(properties)
224 .description(module.getDescription().orElse(""))
225 .required(required.size() > 0 ? required : null);
227 definitions.put(definitionName, definitionBuilder.build());
230 private static boolean isSchemaNodeMandatory(final DataSchemaNode node) {
231 // https://www.rfc-editor.org/rfc/rfc7950#page-14
232 // mandatory node: A mandatory node is one of:
233 if (node instanceof ContainerSchemaNode containerNode) {
234 // A container node without a "presence" statement and that has at least one mandatory node as a child.
235 if (containerNode.isPresenceContainer()) {
238 for (final DataSchemaNode childNode : containerNode.getChildNodes()) {
239 if (childNode instanceof MandatoryAware mandatoryAware && mandatoryAware.isMandatory()) {
244 // A list or leaf-list node with a "min-elements" statement with a value greater than zero.
245 return node instanceof ElementCountConstraintAware constraintAware
246 && constraintAware.getElementCountConstraint()
247 .map(ElementCountConstraint::getMinElements)
252 private void processContainersAndLists(final Module module, final Map<String, Schema> definitions,
253 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
254 final String moduleName = module.getName();
255 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
256 for (final DataSchemaNode childNode : module.getChildNodes()) {
257 stack.enterSchemaTree(childNode.getQName());
258 // For every container and list in the module
259 if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
260 if (childNode.isConfiguration()) {
261 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
264 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack);
270 private void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
271 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
272 final SchemaInferenceStack stack) throws IOException {
273 for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
274 stack.enterSchemaTree(actionDef.getQName());
275 processOperations(actionDef, moduleName, definitions, definitionNames, stack);
280 private void processRPCs(final Module module, final Map<String, Schema> definitions,
281 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
282 final String moduleName = module.getName();
283 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
284 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
285 stack.enterSchemaTree(rpcDefinition.getQName());
286 processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack);
291 private void processOperations(final OperationDefinition operationDef, final String parentName,
292 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
293 final SchemaInferenceStack stack) throws IOException {
294 final String operationName = operationDef.getQName().getLocalName();
295 processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
296 definitionNames, stack);
297 processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
298 definitionNames, stack);
301 private void processOperationInputOutput(final ContainerLike container, final String operationName,
302 final String parentName, final boolean isInput, final Map<String, Schema> definitions,
303 final DefinitionNames definitionNames, final SchemaInferenceStack stack)
305 stack.enterSchemaTree(container.getQName());
306 if (!container.getChildNodes().isEmpty()) {
307 final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
308 final Schema.Builder childSchemaBuilder = new Schema.Builder()
311 .xml(JsonNodeFactory.instance.objectNode().put(NAME_KEY, isInput ? INPUT : OUTPUT));
312 processChildren(childSchemaBuilder, container.getChildNodes(), parentName, definitions, definitionNames,
314 final String discriminator =
315 definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
316 definitions.put(filename + discriminator, childSchemaBuilder.build());
317 processTopData(filename, discriminator, definitions, container);
322 private static ObjectNode processTopData(final String filename, final String discriminator,
323 final Map<String, Schema> definitions, final SchemaNode schemaNode) {
324 final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
325 final String name = filename + discriminator;
326 final String ref = COMPONENTS_PREFIX + name;
327 final String topName = filename + TOP;
329 if (schemaNode instanceof ListSchemaNode) {
330 dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
331 final ObjectNode items = JsonNodeFactory.instance.objectNode();
332 items.put(REF_KEY, ref);
333 dataNodeProperties.set(ITEMS_KEY, items);
334 dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
337 Description can't be added, because nothing allowed alongside $ref.
338 allOf is not an option, because ServiceNow can't parse it.
340 dataNodeProperties.put(REF_KEY, ref);
343 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
345 Add module name prefix to property name, when needed, when ServiceNow can process colons,
346 use RestDocGenUtil#resolveNodesName for creating property name
348 properties.set(schemaNode.getQName().getLocalName(), dataNodeProperties);
349 final var schema = new Schema.Builder()
351 .properties(properties)
355 definitions.put(topName + discriminator, schema);
357 return dataNodeProperties;
361 * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
362 * @param module The module from which the identity stmt will be processed
363 * @param definitions The ObjectNode in which the parsed identity will be put as a 'model' obj
364 * @param definitionNames Store for definition names
366 private static void processIdentities(final Module module, final Map<String, Schema> definitions,
367 final DefinitionNames definitionNames, final EffectiveModelContext context) {
368 final String moduleName = module.getName();
369 final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
370 LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
372 for (final IdentitySchemaNode idNode : idNodes) {
373 final Schema identityObj = buildIdentityObject(idNode, context);
374 final String idName = idNode.getQName().getLocalName();
375 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
376 final String name = idName + discriminator;
377 definitions.put(name, identityObj);
381 private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
382 final ArrayNode enumPayload, final EffectiveModelContext context) {
383 for (final IdentitySchemaNode derivedId : derivedIds) {
384 enumPayload.add(derivedId.getQName().getLocalName());
385 populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
389 private ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
390 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
391 final SchemaInferenceStack stack) throws IOException {
392 final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
393 final SchemaNode schemaNode = (SchemaNode) dataNode;
394 final String localName = schemaNode.getQName().getLocalName();
395 final String nodeName = parentName + "_" + localName;
396 final Schema.Builder childSchemaBuilder = new Schema.Builder()
399 .description(schemaNode.getDescription().orElse(""));
401 childSchemaBuilder.properties(processChildren(childSchemaBuilder, containerChildren,
402 parentName + "_" + localName, definitions, definitionNames, stack));
404 final String discriminator;
405 if (!definitionNames.isListedNode(schemaNode)) {
406 final String parentNameConfigLocalName = parentName + "_" + localName;
407 final String nameAsParent = parentName + "_" + localName;
408 final List<String> names = List.of(parentNameConfigLocalName, parentNameConfigLocalName + TOP,
409 nameAsParent, nameAsParent + TOP);
410 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
412 discriminator = definitionNames.getDiscriminator(schemaNode);
415 final String defName = nodeName + discriminator;
416 childSchemaBuilder.xml(buildXmlParameter(schemaNode));
417 definitions.put(defName, childSchemaBuilder.build());
419 return processTopData(nodeName, discriminator, definitions, schemaNode);
423 * Processes the nodes.
425 private ObjectNode processChildren(final Schema.Builder parentNodeBuilder,
426 final Collection<? extends DataSchemaNode> nodes, final String parentName,
427 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
428 final SchemaInferenceStack stack) throws IOException {
429 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
430 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
431 for (final DataSchemaNode node : nodes) {
432 if (node.isConfiguration()) {
433 if (node instanceof ChoiceSchemaNode choice) {
434 stack.enterSchemaTree(node.getQName());
435 final Map<String, ObjectNode> choiceProperties = processChoiceNodeRecursively(parentName,
436 definitions, definitionNames, stack, required, choice);
437 choiceProperties.forEach(properties::set);
440 final ObjectNode property = processChildNode(node, parentName, definitions, definitionNames,
442 properties.set(node.getQName().getLocalName(), property);
446 parentNodeBuilder.properties(properties).required(required.size() > 0 ? required : null);
450 private Map<String, ObjectNode> processChoiceNodeRecursively(final String parentName,
451 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
452 final SchemaInferenceStack stack, final ArrayNode required, final ChoiceSchemaNode choice)
454 if (!choice.getCases().isEmpty()) {
455 final var properties = new HashMap<String, ObjectNode>();
456 final var caseSchemaNode = choice.getDefaultCase().orElse(choice.getCases().stream()
457 .findFirst().orElseThrow());
458 stack.enterSchemaTree(caseSchemaNode.getQName());
459 for (final var childNode : caseSchemaNode.getChildNodes()) {
460 if (childNode instanceof ChoiceSchemaNode childChoice) {
461 stack.enterSchemaTree(childNode.getQName());
462 final var childProperties = processChoiceNodeRecursively(parentName, definitions, definitionNames,
463 stack, required, childChoice);
464 properties.putAll(childProperties);
467 final var property = processChildNode(childNode, parentName, definitions, definitionNames, stack,
469 properties.put(childNode.getQName().getLocalName(), property);
478 private ObjectNode processChildNode(final DataSchemaNode node, final String parentName,
479 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
480 final SchemaInferenceStack stack, final ArrayNode required) throws IOException {
481 final XMLNamespace parentNamespace = stack.toSchemaNodeIdentifier().lastNodeIdentifier().getNamespace();
482 stack.enterSchemaTree(node.getQName());
484 Add module name prefix to property name, when needed, when ServiceNow can process colons,
485 use RestDocGenUtil#resolveNodesName for creating property name
487 final String name = node.getQName().getLocalName();
488 final ObjectNode property;
489 if (node instanceof LeafSchemaNode leaf) {
490 property = processLeafNode(leaf, name, required, stack, definitions, definitionNames, parentNamespace);
491 } else if (node instanceof AnyxmlSchemaNode || node instanceof AnydataSchemaNode) {
492 property = processUnknownDataSchemaNode(node, name, required, parentNamespace);
493 } else if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
494 if (isSchemaNodeMandatory(node)) {
497 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions, definitionNames,
499 processActionNodeContainer(node, parentName, definitions, definitionNames, stack);
500 } else if (node instanceof LeafListSchemaNode leafList) {
501 if (isSchemaNodeMandatory(node)) {
504 property = processLeafListNode(leafList, stack, definitions, definitionNames);
506 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
512 private ObjectNode processLeafListNode(final LeafListSchemaNode listNode, final SchemaInferenceStack stack,
513 final Map<String, Schema> definitions, final DefinitionNames definitionNames) {
514 final ObjectNode props = JsonNodeFactory.instance.objectNode();
515 props.put(TYPE_KEY, ARRAY_TYPE);
517 final ObjectNode itemsVal = JsonNodeFactory.instance.objectNode();
518 final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
519 optConstraint.ifPresent(elementCountConstraint -> processElementCount(elementCountConstraint, props));
521 processTypeDef(listNode.getType(), listNode, itemsVal, stack, definitions, definitionNames);
522 props.set(ITEMS_KEY, itemsVal);
524 props.put(DESCRIPTION_KEY, listNode.getDescription().orElse(""));
529 private static void processElementCount(final ElementCountConstraint constraint, final ObjectNode props) {
530 final Integer minElements = constraint.getMinElements();
531 if (minElements != null) {
532 props.put(MIN_ITEMS, minElements);
534 final Integer maxElements = constraint.getMaxElements();
535 if (maxElements != null) {
536 props.put(MAX_ITEMS, maxElements);
540 private static void processMandatory(final MandatoryAware node, final String nodeName, final ArrayNode required) {
541 if (node.isMandatory()) {
542 required.add(nodeName);
546 private ObjectNode processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
547 final ArrayNode required, final SchemaInferenceStack stack, final Map<String, Schema> definitions,
548 final DefinitionNames definitionNames, final XMLNamespace parentNamespace) {
549 final ObjectNode property = JsonNodeFactory.instance.objectNode();
551 final String leafDescription = leafNode.getDescription().orElse("");
553 Description can't be added, because nothing allowed alongside $ref.
554 allOf is not an option, because ServiceNow can't parse it.
556 if (!(leafNode.getType() instanceof IdentityrefTypeDefinition)) {
557 property.put(DESCRIPTION_KEY, leafDescription);
560 processTypeDef(leafNode.getType(), leafNode, property, stack, definitions, definitionNames);
561 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
562 // If the parent is not from the same model, define the child XML namespace.
563 property.set(XML_KEY, buildXmlParameter(leafNode));
565 processMandatory(leafNode, jsonLeafName, required);
569 private static ObjectNode processUnknownDataSchemaNode(final DataSchemaNode leafNode, final String name,
570 final ArrayNode required, final XMLNamespace parentNamespace) {
571 assert (leafNode instanceof AnydataSchemaNode || leafNode instanceof AnyxmlSchemaNode);
573 final ObjectNode property = JsonNodeFactory.instance.objectNode();
575 final String leafDescription = leafNode.getDescription().orElse("");
576 property.put(DESCRIPTION_KEY, leafDescription);
578 final String localName = leafNode.getQName().getLocalName();
579 setExampleValue(property, String.format("<%s> ... </%s>", localName, localName));
580 property.put(TYPE_KEY, STRING_TYPE);
581 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
582 // If the parent is not from the same model, define the child XML namespace.
583 property.set(XML_KEY, buildXmlParameter(leafNode));
585 processMandatory((MandatoryAware) leafNode, name, required);
589 private String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
590 final ObjectNode property, final SchemaInferenceStack stack, final Map<String, Schema> definitions,
591 final DefinitionNames definitionNames) {
592 final String jsonType;
593 if (leafTypeDef instanceof BinaryTypeDefinition binaryType) {
594 jsonType = processBinaryType(binaryType, property);
595 } else if (leafTypeDef instanceof BitsTypeDefinition bitsType) {
596 jsonType = processBitsType(bitsType, property);
597 } else if (leafTypeDef instanceof EnumTypeDefinition enumType) {
598 jsonType = processEnumType(enumType, property);
599 } else if (leafTypeDef instanceof IdentityrefTypeDefinition identityrefType) {
600 jsonType = processIdentityRefType(identityrefType, property, definitions,
601 definitionNames, stack.getEffectiveModelContext());
602 } else if (leafTypeDef instanceof StringTypeDefinition stringType) {
603 jsonType = processStringType(stringType, property, node.getQName().getLocalName());
604 } else if (leafTypeDef instanceof UnionTypeDefinition unionType) {
605 jsonType = processUnionType(unionType, property, node.getQName().getLocalName());
606 } else if (leafTypeDef instanceof EmptyTypeDefinition) {
607 jsonType = OBJECT_TYPE;
608 } else if (leafTypeDef instanceof LeafrefTypeDefinition leafrefType) {
609 return processTypeDef(stack.resolveLeafref(leafrefType), node, property,
610 stack, definitions, definitionNames);
611 } else if (leafTypeDef instanceof BooleanTypeDefinition) {
612 jsonType = BOOLEAN_TYPE;
613 leafTypeDef.getDefaultValue().ifPresent(v -> setDefaultValue(property, Boolean.valueOf((String) v)));
614 setExampleValue(property, true);
615 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
616 jsonType = processNumberType(rangeRestrictedType, property);
617 } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
618 jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
620 jsonType = STRING_TYPE;
622 if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
623 if (TYPE_KEY != null && jsonType != null) {
624 property.put(TYPE_KEY, jsonType);
626 if (leafTypeDef.getDefaultValue().isPresent()) {
627 final Object defaultValue = leafTypeDef.getDefaultValue().orElseThrow();
628 if (defaultValue instanceof String stringDefaultValue) {
629 if (leafTypeDef instanceof BooleanTypeDefinition) {
630 setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
631 } else if (leafTypeDef instanceof DecimalTypeDefinition
632 || leafTypeDef instanceof Uint64TypeDefinition) {
633 setDefaultValue(property, new BigDecimal(stringDefaultValue));
634 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
635 //uint8,16,32 int8,16,32,64
636 if (isHexadecimalOrOctal(rangeRestrictedType)) {
637 setDefaultValue(property, stringDefaultValue);
639 setDefaultValue(property, Long.valueOf(stringDefaultValue));
642 setDefaultValue(property, stringDefaultValue);
645 //we should never get here. getDefaultValue always gives us string
646 setDefaultValue(property, defaultValue.toString());
653 private static String processBinaryType(final BinaryTypeDefinition definition, final ObjectNode property) {
654 definition.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
655 property.put(FORMAT_KEY, "byte");
659 private static String processEnumType(final EnumTypeDefinition enumLeafType, final ObjectNode property) {
660 final List<EnumPair> enumPairs = enumLeafType.getValues();
661 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
662 for (final EnumPair enumPair : enumPairs) {
663 enumNames.add(new TextNode(enumPair.getName()));
666 property.set(ENUM_KEY, enumNames);
667 enumLeafType.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
668 setExampleValue(property, enumLeafType.getValues().iterator().next().getName());
672 private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
673 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
674 final EffectiveModelContext schemaContext) {
675 final String definitionName;
676 if (isImported(leafTypeDef)) {
677 definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
679 final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
680 definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
682 property.put(REF_KEY, COMPONENTS_PREFIX + definitionName);
686 private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
687 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
688 final EffectiveModelContext context) {
689 final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
690 final String identityName = idNode.getQName().getLocalName();
691 if (!definitionNames.isListedNode(idNode)) {
692 final Schema identityObj = buildIdentityObject(idNode, context);
693 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
694 final String name = identityName + discriminator;
695 definitions.put(name, identityObj);
698 return identityName + definitionNames.getDiscriminator(idNode);
702 private static Schema buildIdentityObject(final IdentitySchemaNode idNode, final EffectiveModelContext context) {
703 final String identityName = idNode.getQName().getLocalName();
704 LOG.debug("Processing Identity: {}", identityName);
706 final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
707 final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
708 enumPayload.add(identityName);
709 populateEnumWithDerived(derivedIds, enumPayload, context);
711 return new Schema.Builder()
713 .description(idNode.getDescription().orElse(""))
714 .schemaEnum(enumPayload)
719 private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
720 return !leafTypeDef.getQName().getModule().equals(topLevelModule.getQNameModule());
723 private static String processBitsType(final BitsTypeDefinition bitsType, final ObjectNode property) {
724 property.put(MIN_ITEMS, 0);
725 property.put(UNIQUE_ITEMS_KEY, true);
726 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
727 final Collection<? extends Bit> bits = bitsType.getBits();
728 for (final Bit bit : bits) {
729 enumNames.add(new TextNode(bit.getName()));
731 property.set(ENUM_KEY, enumNames);
732 property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
733 bitsType.getDefaultValue().ifPresent(v -> setDefaultValue(property, (String) v));
737 private static String processStringType(final StringTypeDefinition stringType, final ObjectNode property,
738 final String nodeName) {
739 var type = stringType;
740 while (type.getLengthConstraint().isEmpty() && type.getBaseType() != null) {
741 type = type.getBaseType();
744 type.getLengthConstraint().ifPresent(constraint -> {
745 final Range<Integer> range = constraint.getAllowedRanges().span();
746 property.put(MIN_LENGTH_KEY, range.lowerEndpoint());
747 property.put(MAX_LENGTH_KEY, range.upperEndpoint());
750 if (type.getPatternConstraints().iterator().hasNext()) {
751 final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
752 String regex = pattern.getRegularExpressionString();
753 // Escape special characters to prevent issues inside Automaton.
754 regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
755 for (final var charClass : PREDEFINED_CHARACTER_CLASSES.entrySet()) {
756 regex = regex.replaceAll(charClass.getKey(), charClass.getValue());
758 String defaultValue = "";
760 final RegExp regExp = new RegExp(regex);
761 defaultValue = regExp.toAutomaton().getShortestExample(true);
762 } catch (IllegalArgumentException ex) {
763 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
765 setExampleValue(property, defaultValue);
767 setExampleValue(property, "Some " + nodeName);
770 stringType.getDefaultValue().ifPresent(v -> setDefaultValue(property, (String) v));
774 private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
775 final ObjectNode property) {
776 final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
777 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
779 if (isHexadecimalOrOctal(leafTypeDef)) {
783 if (leafTypeDef instanceof DecimalTypeDefinition) {
784 maybeLower.ifPresent(number -> setExampleValue(property, ((Decimal64) number).decimalValue()));
787 if (leafTypeDef instanceof Uint8TypeDefinition
788 || leafTypeDef instanceof Uint16TypeDefinition
789 || leafTypeDef instanceof Int8TypeDefinition
790 || leafTypeDef instanceof Int16TypeDefinition
791 || leafTypeDef instanceof Int32TypeDefinition) {
793 property.put(FORMAT_KEY, INT32_FORMAT);
794 maybeLower.ifPresent(number -> setExampleValue(property, Integer.valueOf(number.toString())));
795 } else if (leafTypeDef instanceof Uint32TypeDefinition
796 || leafTypeDef instanceof Int64TypeDefinition) {
798 property.put(FORMAT_KEY, INT64_FORMAT);
799 maybeLower.ifPresent(number -> setExampleValue(property, Long.valueOf(number.toString())));
802 setExampleValue(property, 0);
807 private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
808 final Optional<?> optDefaultValue = typeDef.getDefaultValue();
809 if (optDefaultValue.isPresent()) {
810 final String defaultValue = (String) optDefaultValue.orElseThrow();
811 return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
816 private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
817 final EffectiveModelContext schemaContext) {
818 // create example instance-identifier to the first container of node's module if exists or leave it empty
819 final var module = schemaContext.findModule(node.getQName().getModule());
820 if (module.isPresent()) {
821 final var container = module.orElseThrow().getChildNodes().stream()
822 .filter(n -> n instanceof ContainerSchemaNode)
824 container.ifPresent(c -> setExampleValue(property, String.format("/%s:%s", module.orElseThrow().getPrefix(),
825 c.getQName().getLocalName())));
831 private static String processUnionType(final UnionTypeDefinition unionType, final ObjectNode property,
832 final String nodeName) {
833 boolean isStringTakePlace = false;
834 boolean isNumberTakePlace = false;
835 boolean isBooleanTakePlace = false;
836 for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
837 if (!isStringTakePlace) {
838 if (typeDef instanceof StringTypeDefinition
839 || typeDef instanceof BitsTypeDefinition
840 || typeDef instanceof BinaryTypeDefinition
841 || typeDef instanceof IdentityrefTypeDefinition
842 || typeDef instanceof EnumTypeDefinition
843 || typeDef instanceof LeafrefTypeDefinition
844 || typeDef instanceof UnionTypeDefinition) {
845 isStringTakePlace = true;
846 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
847 isNumberTakePlace = true;
848 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
849 isBooleanTakePlace = true;
853 if (isStringTakePlace) {
854 unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, (String) v));
855 setExampleValue(property, "Some " + nodeName);
858 if (isBooleanTakePlace) {
859 if (isNumberTakePlace) {
860 // FIXME deal with other number formats
861 unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Long.valueOf((String) v)));
862 setExampleValue(property, 0);
865 unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Boolean.valueOf((String) v)));
866 setExampleValue(property, true);
869 // FIXME deal with other number formats
870 unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Long.valueOf((String) v)));
871 setExampleValue(property, 0);
875 private static ObjectNode buildXmlParameter(final SchemaNode node) {
876 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
877 final QName qName = node.getQName();
878 xml.put(NAME_KEY, qName.getLocalName());
879 xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
883 private static void setExampleValue(final ObjectNode property, final String value) {
884 property.put(EXAMPLE_KEY, value);
887 private static void setExampleValue(final ObjectNode property, final Integer value) {
888 property.put(EXAMPLE_KEY, value);
891 private static void setExampleValue(final ObjectNode property, final Long value) {
892 property.put(EXAMPLE_KEY, value);
895 private static void setExampleValue(final ObjectNode property, final BigDecimal value) {
896 property.put(EXAMPLE_KEY, value);
899 private static void setExampleValue(final ObjectNode property, final Boolean value) {
900 property.put(EXAMPLE_KEY, value);
903 private static void setDefaultValue(final ObjectNode property, final String value) {
904 property.put(DEFAULT_KEY, value);
907 private static void setDefaultValue(final ObjectNode property, final Long value) {
908 property.put(DEFAULT_KEY, value);
911 private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
912 property.put(DEFAULT_KEY, value);
915 private static void setDefaultValue(final ObjectNode property, final Boolean value) {
916 property.put(DEFAULT_KEY, value);