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;
15 import static org.opendaylight.restconf.openapi.util.RestDocgenUtil.resolveFullNameFromNode;
17 import com.fasterxml.jackson.databind.node.ArrayNode;
18 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
19 import com.fasterxml.jackson.databind.node.ObjectNode;
20 import com.fasterxml.jackson.databind.node.TextNode;
21 import com.google.common.collect.Range;
22 import com.google.common.collect.RangeSet;
23 import dk.brics.automaton.RegExp;
24 import java.io.IOException;
25 import java.math.BigDecimal;
26 import java.util.Collection;
27 import java.util.HashMap;
28 import java.util.List;
30 import java.util.Optional;
31 import java.util.regex.Pattern;
32 import org.opendaylight.restconf.openapi.model.Schema;
33 import org.opendaylight.yangtools.yang.common.Decimal64;
34 import org.opendaylight.yangtools.yang.common.QName;
35 import org.opendaylight.yangtools.yang.common.XMLNamespace;
36 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
37 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
38 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
42 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
44 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
46 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraint;
47 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraintAware;
48 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.MandatoryAware;
53 import org.opendaylight.yangtools.yang.model.api.Module;
54 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
55 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
56 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
57 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
58 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
59 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
60 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition.Bit;
61 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
62 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
63 import org.opendaylight.yangtools.yang.model.api.type.EmptyTypeDefinition;
64 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
65 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition.EnumPair;
66 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
67 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
68 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
69 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
70 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
71 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
72 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
73 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
74 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
75 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
76 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
77 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
78 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
79 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
80 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
81 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
82 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
83 import org.slf4j.Logger;
84 import org.slf4j.LoggerFactory;
87 * Generates JSON Schema for data defined in YANG. This class is not thread-safe.
89 public final class DefinitionGenerator {
91 private static final Logger LOG = LoggerFactory.getLogger(DefinitionGenerator.class);
93 private static final String UNIQUE_ITEMS_KEY = "uniqueItems";
94 private static final String MAX_ITEMS = "maxItems";
95 private static final String MIN_ITEMS = "minItems";
96 private static final String MAX_LENGTH_KEY = "maxLength";
97 private static final String MIN_LENGTH_KEY = "minLength";
98 private static final String REF_KEY = "$ref";
99 private static final String ITEMS_KEY = "items";
100 private static final String TYPE_KEY = "type";
101 private static final String DESCRIPTION_KEY = "description";
102 private static final String ARRAY_TYPE = "array";
103 private static final String ENUM_KEY = "enum";
104 private static final String TITLE_KEY = "title";
105 private static final String DEFAULT_KEY = "default";
106 private static final String EXAMPLE_KEY = "example";
107 private static final String FORMAT_KEY = "format";
108 private static final String NAMESPACE_KEY = "namespace";
109 public static final String INPUT = "input";
110 public static final String INPUT_SUFFIX = "_input";
111 public static final String OUTPUT = "output";
112 public static final String OUTPUT_SUFFIX = "_output";
113 private static final String STRING_TYPE = "string";
114 private static final String OBJECT_TYPE = "object";
115 private static final String NUMBER_TYPE = "number";
116 private static final String INTEGER_TYPE = "integer";
117 private static final String INT32_FORMAT = "int32";
118 private static final String INT64_FORMAT = "int64";
119 private static final String BOOLEAN_TYPE = "boolean";
120 // Special characters used in Automaton.
121 // See https://www.brics.dk/automaton/doc/dk/brics/automaton/RegExp.html
122 private static final Pattern AUTOMATON_SPECIAL_CHARACTERS = Pattern.compile("[@&\"<>#~]");
123 // Adaptation from YANG regex to Automaton regex
124 // See https://github.com/mifmif/Generex/blob/master/src/main/java/com/mifmif/common/regex/Generex.java
125 private static final Map<String, String> PREDEFINED_CHARACTER_CLASSES = Map.of("\\\\d", "[0-9]",
126 "\\\\D", "[^0-9]", "\\\\s", "[ \t\n\f\r]", "\\\\S", "[^ \t\n\f\r]",
127 "\\\\w", "[a-zA-Z_0-9]", "\\\\W", "[^a-zA-Z_0-9]");
129 private 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 private static 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 {
146 processIdentities(module, definitions, definitionNames, schemaContext);
147 processContainersAndLists(module, definitions, definitionNames, schemaContext);
148 processRPCs(module, definitions, definitionNames, schemaContext);
150 if (isForSingleModule) {
151 processModule(module, definitions, definitionNames, schemaContext);
157 public static Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
158 final DefinitionNames definitionNames, final boolean isForSingleModule)
160 final Map<String, Schema> definitions = new HashMap<>();
161 if (isForSingleModule) {
162 definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
164 return convertToSchemas(module, schemaContext, definitions, definitionNames, isForSingleModule);
167 private static void processModule(final Module module, final Map<String, Schema> definitions,
168 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) {
169 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
170 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
171 final String moduleName = module.getName();
172 final String definitionName = moduleName + MODULE_NAME_SUFFIX;
173 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
174 for (final DataSchemaNode node : module.getChildNodes()) {
175 stack.enterSchemaTree(node.getQName());
176 final String localName = node.getQName().getLocalName();
177 if (node.isConfiguration()) {
178 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
179 if (isSchemaNodeMandatory(node)) {
180 required.add(localName);
182 for (final DataSchemaNode childNode : ((DataNodeContainer) node).getChildNodes()) {
183 final ObjectNode childNodeProperties = JsonNodeFactory.instance.objectNode();
185 final String ref = COMPONENTS_PREFIX
188 + definitionNames.getDiscriminator(node);
190 if (node instanceof ListSchemaNode) {
191 childNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
192 final ObjectNode items = JsonNodeFactory.instance.objectNode();
193 items.put(REF_KEY, ref);
194 childNodeProperties.set(ITEMS_KEY, items);
195 childNodeProperties.put(DESCRIPTION_KEY, childNode.getDescription().orElse(""));
196 childNodeProperties.put(TITLE_KEY, localName);
199 Description can't be added, because nothing allowed alongside $ref.
200 allOf is not an option, because ServiceNow can't parse it.
202 childNodeProperties.put(REF_KEY, ref);
204 //add module name prefix to property name, when ServiceNow can process colons
205 properties.set(localName, childNodeProperties);
207 } else if (node instanceof LeafSchemaNode) {
209 Add module name prefix to property name, when ServiceNow can process colons(second parameter
212 final ObjectNode leafNode = processLeafNode((LeafSchemaNode) node, localName, required, stack,
213 definitions, definitionNames, module.getNamespace(), module);
214 properties.set(localName, leafNode);
219 final Schema.Builder definitionBuilder = new Schema.Builder()
220 .title(definitionName)
222 .properties(properties)
223 .description(module.getDescription().orElse(""))
224 .required(required.size() > 0 ? required : null);
226 definitions.put(definitionName, definitionBuilder.build());
229 private static boolean isSchemaNodeMandatory(final DataSchemaNode node) {
230 // https://www.rfc-editor.org/rfc/rfc7950#page-14
231 // mandatory node: A mandatory node is one of:
232 if (node instanceof ContainerSchemaNode containerNode) {
233 // A container node without a "presence" statement and that has at least one mandatory node as a child.
234 if (containerNode.isPresenceContainer()) {
237 for (final DataSchemaNode childNode : containerNode.getChildNodes()) {
238 if (childNode instanceof MandatoryAware mandatoryAware && mandatoryAware.isMandatory()) {
243 // A list or leaf-list node with a "min-elements" statement with a value greater than zero.
244 return node instanceof ElementCountConstraintAware constraintAware
245 && constraintAware.getElementCountConstraint()
246 .map(ElementCountConstraint::getMinElements)
251 private static void processContainersAndLists(final Module module, final Map<String, Schema> definitions,
252 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
253 final String moduleName = module.getName();
254 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
255 for (final DataSchemaNode childNode : module.getChildNodes()) {
256 stack.enterSchemaTree(childNode.getQName());
257 // For every container and list in the module
258 if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
259 if (childNode.isConfiguration()) {
260 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
263 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack, module);
269 private static void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
270 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
271 final SchemaInferenceStack stack, final Module module) throws IOException {
272 for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
273 stack.enterSchemaTree(actionDef.getQName());
274 processOperations(actionDef, moduleName, definitions, definitionNames, stack, module);
279 private static void processRPCs(final Module module, final Map<String, Schema> definitions,
280 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
281 final String moduleName = module.getName();
282 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
283 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
284 stack.enterSchemaTree(rpcDefinition.getQName());
285 processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack, module);
290 private static void processOperations(final OperationDefinition operationDef, final String parentName,
291 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
292 final SchemaInferenceStack stack, final Module module) throws IOException {
293 final String operationName = operationDef.getQName().getLocalName();
294 processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
295 definitionNames, stack, module);
296 processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
297 definitionNames, stack, module);
300 private static void processOperationInputOutput(final ContainerLike container, final String operationName,
301 final String parentName, final boolean isInput, final Map<String, Schema> definitions,
302 final DefinitionNames definitionNames, final SchemaInferenceStack stack,
303 final Module module) throws IOException {
304 stack.enterSchemaTree(container.getQName());
305 if (!container.getChildNodes().isEmpty()) {
306 final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
307 final Schema.Builder childSchemaBuilder = new Schema.Builder()
310 .xml(JsonNodeFactory.instance.objectNode().put(NAME_KEY, isInput ? INPUT : OUTPUT));
311 processChildren(childSchemaBuilder, container.getChildNodes(), parentName, definitions, definitionNames,
313 final String discriminator =
314 definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
315 definitions.put(filename + discriminator, childSchemaBuilder.build());
316 processTopData(filename, discriminator, definitions, container, stack.getEffectiveModelContext());
321 private static ObjectNode processTopData(final String filename, final String discriminator,
322 final Map<String, Schema> definitions, final SchemaNode schemaNode, final EffectiveModelContext context) {
323 final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
324 final String name = filename + discriminator;
325 final String ref = COMPONENTS_PREFIX + name;
326 final String topName = filename + TOP;
328 if (schemaNode instanceof ListSchemaNode) {
329 dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
330 final ObjectNode items = JsonNodeFactory.instance.objectNode();
331 items.put(REF_KEY, ref);
332 dataNodeProperties.set(ITEMS_KEY, items);
333 dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
336 Description can't be added, because nothing allowed alongside $ref.
337 allOf is not an option, because ServiceNow can't parse it.
339 dataNodeProperties.put(REF_KEY, ref);
342 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
343 properties.set(resolveFullNameFromNode(schemaNode.getQName(), context), dataNodeProperties);
344 final var schema = new Schema.Builder()
346 .properties(properties)
350 definitions.put(topName + discriminator, schema);
352 return dataNodeProperties;
356 * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
357 * @param module The module from which the identity stmt will be processed
358 * @param definitions The ObjectNode in which the parsed identity will be put as a 'model' obj
359 * @param definitionNames Store for definition names
361 private static void processIdentities(final Module module, final Map<String, Schema> definitions,
362 final DefinitionNames definitionNames, final EffectiveModelContext context) {
363 final String moduleName = module.getName();
364 final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
365 LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
367 for (final IdentitySchemaNode idNode : idNodes) {
368 final Schema identityObj = buildIdentityObject(idNode, context);
369 final String idName = idNode.getQName().getLocalName();
370 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
371 final String name = idName + discriminator;
372 definitions.put(name, identityObj);
376 private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
377 final ArrayNode enumPayload, final EffectiveModelContext context) {
378 for (final IdentitySchemaNode derivedId : derivedIds) {
379 enumPayload.add(derivedId.getQName().getLocalName());
380 populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
384 private static ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
385 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
386 final SchemaInferenceStack stack, final Module module) throws IOException {
387 final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
388 final SchemaNode schemaNode = (SchemaNode) dataNode;
389 final String localName = schemaNode.getQName().getLocalName();
390 final String nodeName = parentName + "_" + localName;
391 final Schema.Builder childSchemaBuilder = new Schema.Builder()
394 .description(schemaNode.getDescription().orElse(""));
396 childSchemaBuilder.properties(processChildren(childSchemaBuilder, containerChildren,
397 parentName + "_" + localName, definitions, definitionNames, stack, module));
399 final String discriminator;
400 if (!definitionNames.isListedNode(schemaNode)) {
401 final String parentNameConfigLocalName = parentName + "_" + localName;
402 final String nameAsParent = parentName + "_" + localName;
403 final List<String> names = List.of(parentNameConfigLocalName, parentNameConfigLocalName + TOP,
404 nameAsParent, nameAsParent + TOP);
405 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
407 discriminator = definitionNames.getDiscriminator(schemaNode);
410 final String defName = nodeName + discriminator;
411 childSchemaBuilder.xml(buildXmlParameter(schemaNode));
412 definitions.put(defName, childSchemaBuilder.build());
414 return processTopData(nodeName, discriminator, definitions, schemaNode, stack.getEffectiveModelContext());
418 * Processes the nodes.
420 private static ObjectNode processChildren(final Schema.Builder parentNodeBuilder,
421 final Collection<? extends DataSchemaNode> nodes, final String parentName,
422 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
423 final SchemaInferenceStack stack, final Module module) throws IOException {
424 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
425 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
426 for (final DataSchemaNode node : nodes) {
427 if (node.isConfiguration()) {
428 if (node instanceof ChoiceSchemaNode choice) {
429 stack.enterSchemaTree(node.getQName());
430 final Map<String, ObjectNode> choiceProperties = processChoiceNodeRecursively(parentName,
431 definitions, definitionNames, stack, required, choice, module);
432 choiceProperties.forEach(properties::set);
435 final ObjectNode property = processChildNode(node, parentName, definitions, definitionNames,
436 stack, required, module);
437 properties.set(node.getQName().getLocalName(), property);
441 parentNodeBuilder.properties(properties).required(required.size() > 0 ? required : null);
445 private static Map<String, ObjectNode> processChoiceNodeRecursively(final String parentName,
446 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
447 final SchemaInferenceStack stack, final ArrayNode required, final ChoiceSchemaNode choice,
448 final Module module) throws IOException {
449 if (!choice.getCases().isEmpty()) {
450 final var properties = new HashMap<String, ObjectNode>();
451 final var caseSchemaNode = choice.getDefaultCase().orElse(choice.getCases().stream()
452 .findFirst().orElseThrow());
453 stack.enterSchemaTree(caseSchemaNode.getQName());
454 for (final var childNode : caseSchemaNode.getChildNodes()) {
455 if (childNode instanceof ChoiceSchemaNode childChoice) {
456 stack.enterSchemaTree(childNode.getQName());
457 final var childProperties = processChoiceNodeRecursively(parentName, definitions, definitionNames,
458 stack, required, childChoice, module);
459 properties.putAll(childProperties);
462 final var property = processChildNode(childNode, parentName, definitions, definitionNames, stack,
464 properties.put(childNode.getQName().getLocalName(), property);
473 private static ObjectNode processChildNode(final DataSchemaNode node, final String parentName,
474 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
475 final SchemaInferenceStack stack, final ArrayNode required,
476 final Module module) throws IOException {
477 final XMLNamespace parentNamespace = stack.toSchemaNodeIdentifier().lastNodeIdentifier().getNamespace();
478 stack.enterSchemaTree(node.getQName());
480 Add module name prefix to property name, when needed, when ServiceNow can process colons,
481 use RestDocGenUtil#resolveNodesName for creating property name
483 final String name = node.getQName().getLocalName();
484 final ObjectNode property;
485 if (node instanceof LeafSchemaNode leaf) {
486 property = processLeafNode(leaf, name, required, stack, definitions, definitionNames, parentNamespace,
488 } else if (node instanceof AnyxmlSchemaNode || node instanceof AnydataSchemaNode) {
489 property = processUnknownDataSchemaNode(node, name, required, parentNamespace);
490 } else if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
491 if (isSchemaNodeMandatory(node)) {
494 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions, definitionNames,
496 processActionNodeContainer(node, parentName, definitions, definitionNames, stack, module);
497 } else if (node instanceof LeafListSchemaNode leafList) {
498 if (isSchemaNodeMandatory(node)) {
501 property = processLeafListNode(leafList, stack, definitions, definitionNames, module);
503 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
509 private static ObjectNode processLeafListNode(final LeafListSchemaNode listNode, final SchemaInferenceStack stack,
510 final Map<String, Schema> definitions, final DefinitionNames definitionNames, final Module module) {
511 final ObjectNode props = JsonNodeFactory.instance.objectNode();
512 props.put(TYPE_KEY, ARRAY_TYPE);
514 final ObjectNode itemsVal = JsonNodeFactory.instance.objectNode();
515 final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
516 optConstraint.ifPresent(elementCountConstraint -> processElementCount(elementCountConstraint, props));
518 processTypeDef(listNode.getType(), listNode, itemsVal, stack, definitions, definitionNames, module);
519 props.set(ITEMS_KEY, itemsVal);
521 props.put(DESCRIPTION_KEY, listNode.getDescription().orElse(""));
526 private static void processElementCount(final ElementCountConstraint constraint, final ObjectNode props) {
527 final Integer minElements = constraint.getMinElements();
528 if (minElements != null) {
529 props.put(MIN_ITEMS, minElements);
531 final Integer maxElements = constraint.getMaxElements();
532 if (maxElements != null) {
533 props.put(MAX_ITEMS, maxElements);
537 private static void processMandatory(final MandatoryAware node, final String nodeName, final ArrayNode required) {
538 if (node.isMandatory()) {
539 required.add(nodeName);
543 private static ObjectNode processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
544 final ArrayNode required, final SchemaInferenceStack stack, final Map<String, Schema> definitions,
545 final DefinitionNames definitionNames, final XMLNamespace parentNamespace, final Module module) {
546 final ObjectNode property = JsonNodeFactory.instance.objectNode();
548 final String leafDescription = leafNode.getDescription().orElse("");
550 Description can't be added, because nothing allowed alongside $ref.
551 allOf is not an option, because ServiceNow can't parse it.
553 if (!(leafNode.getType() instanceof IdentityrefTypeDefinition)) {
554 property.put(DESCRIPTION_KEY, leafDescription);
557 processTypeDef(leafNode.getType(), leafNode, property, stack, definitions, definitionNames, module);
558 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
559 // If the parent is not from the same model, define the child XML namespace.
560 property.set(XML_KEY, buildXmlParameter(leafNode));
562 processMandatory(leafNode, jsonLeafName, required);
566 private static ObjectNode processUnknownDataSchemaNode(final DataSchemaNode leafNode, final String name,
567 final ArrayNode required, final XMLNamespace parentNamespace) {
568 assert (leafNode instanceof AnydataSchemaNode || leafNode instanceof AnyxmlSchemaNode);
570 final ObjectNode property = JsonNodeFactory.instance.objectNode();
572 final String leafDescription = leafNode.getDescription().orElse("");
573 property.put(DESCRIPTION_KEY, leafDescription);
575 final String localName = leafNode.getQName().getLocalName();
576 setExampleValue(property, String.format("<%s> ... </%s>", localName, localName));
577 property.put(TYPE_KEY, STRING_TYPE);
578 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
579 // If the parent is not from the same model, define the child XML namespace.
580 property.set(XML_KEY, buildXmlParameter(leafNode));
582 processMandatory((MandatoryAware) leafNode, name, required);
586 private static String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
587 final ObjectNode property, final SchemaInferenceStack stack,final Map<String, Schema> definitions,
588 final DefinitionNames definitionNames, final Module module) {
589 final String jsonType;
590 if (leafTypeDef instanceof BinaryTypeDefinition binaryType) {
591 jsonType = processBinaryType(binaryType, property);
592 } else if (leafTypeDef instanceof BitsTypeDefinition bitsType) {
593 jsonType = processBitsType(bitsType, property);
594 } else if (leafTypeDef instanceof EnumTypeDefinition enumType) {
595 jsonType = processEnumType(enumType, property);
596 } else if (leafTypeDef instanceof IdentityrefTypeDefinition identityrefType) {
597 jsonType = processIdentityRefType(identityrefType, property, definitions,
598 definitionNames, stack.getEffectiveModelContext(), module);
599 } else if (leafTypeDef instanceof StringTypeDefinition stringType) {
600 jsonType = processStringType(stringType, property, node.getQName().getLocalName());
601 } else if (leafTypeDef instanceof UnionTypeDefinition unionType) {
602 jsonType = processTypeDef(unionType.getTypes().iterator().next(), node, property, stack, definitions,
603 definitionNames, module);
604 } else if (leafTypeDef instanceof EmptyTypeDefinition) {
605 jsonType = OBJECT_TYPE;
606 } else if (leafTypeDef instanceof LeafrefTypeDefinition leafrefType) {
607 return processTypeDef(stack.resolveLeafref(leafrefType), node, property,
608 stack, definitions, definitionNames, module);
609 } else if (leafTypeDef instanceof BooleanTypeDefinition) {
610 jsonType = BOOLEAN_TYPE;
611 leafTypeDef.getDefaultValue().ifPresent(v -> setDefaultValue(property, Boolean.valueOf((String) v)));
612 setExampleValue(property, true);
613 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
614 jsonType = processNumberType(rangeRestrictedType, property);
615 } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
616 jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
618 jsonType = STRING_TYPE;
620 if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
621 if (TYPE_KEY != null && jsonType != null) {
622 property.put(TYPE_KEY, jsonType);
624 if (leafTypeDef.getDefaultValue().isPresent()) {
625 final Object defaultValue = leafTypeDef.getDefaultValue().orElseThrow();
626 if (defaultValue instanceof String stringDefaultValue) {
627 if (leafTypeDef instanceof BooleanTypeDefinition) {
628 setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
629 } else if (leafTypeDef instanceof DecimalTypeDefinition
630 || leafTypeDef instanceof Uint64TypeDefinition) {
631 setDefaultValue(property, new BigDecimal(stringDefaultValue));
632 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
633 //uint8,16,32 int8,16,32,64
634 if (isHexadecimalOrOctal(rangeRestrictedType)) {
635 setDefaultValue(property, stringDefaultValue);
637 setDefaultValue(property, Long.valueOf(stringDefaultValue));
640 setDefaultValue(property, stringDefaultValue);
643 //we should never get here. getDefaultValue always gives us string
644 setDefaultValue(property, defaultValue.toString());
651 private static String processBinaryType(final BinaryTypeDefinition definition, final ObjectNode property) {
652 definition.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
653 property.put(FORMAT_KEY, "byte");
657 private static String processEnumType(final EnumTypeDefinition enumLeafType, final ObjectNode property) {
658 final List<EnumPair> enumPairs = enumLeafType.getValues();
659 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
660 for (final EnumPair enumPair : enumPairs) {
661 enumNames.add(new TextNode(enumPair.getName()));
664 property.set(ENUM_KEY, enumNames);
665 enumLeafType.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
666 setExampleValue(property, enumLeafType.getValues().iterator().next().getName());
670 private static String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
671 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
672 final EffectiveModelContext schemaContext, final Module module) {
673 final String definitionName;
674 if (isImported(leafTypeDef, module)) {
675 definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
677 final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
678 definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
680 property.put(REF_KEY, COMPONENTS_PREFIX + definitionName);
684 private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
685 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
686 final EffectiveModelContext context) {
687 final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
688 final String identityName = idNode.getQName().getLocalName();
689 if (!definitionNames.isListedNode(idNode)) {
690 final Schema identityObj = buildIdentityObject(idNode, context);
691 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
692 final String name = identityName + discriminator;
693 definitions.put(name, identityObj);
696 return identityName + definitionNames.getDiscriminator(idNode);
700 private static Schema buildIdentityObject(final IdentitySchemaNode idNode, final EffectiveModelContext context) {
701 final String identityName = idNode.getQName().getLocalName();
702 LOG.debug("Processing Identity: {}", identityName);
704 final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
705 final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
706 enumPayload.add(identityName);
707 populateEnumWithDerived(derivedIds, enumPayload, context);
709 return new Schema.Builder()
711 .description(idNode.getDescription().orElse(""))
712 .schemaEnum(enumPayload)
717 private static boolean isImported(final IdentityrefTypeDefinition leafTypeDef, final Module module) {
718 return !leafTypeDef.getQName().getModule().equals(module.getQNameModule());
721 private static String processBitsType(final BitsTypeDefinition bitsType, final ObjectNode property) {
722 property.put(MIN_ITEMS, 0);
723 property.put(UNIQUE_ITEMS_KEY, true);
724 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
725 final Collection<? extends Bit> bits = bitsType.getBits();
726 for (final Bit bit : bits) {
727 enumNames.add(new TextNode(bit.getName()));
729 property.set(ENUM_KEY, enumNames);
730 property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
731 bitsType.getDefaultValue().ifPresent(v -> setDefaultValue(property, (String) v));
735 private static String processStringType(final StringTypeDefinition stringType, final ObjectNode property,
736 final String nodeName) {
737 var type = stringType;
738 while (type.getLengthConstraint().isEmpty() && type.getBaseType() != null) {
739 type = type.getBaseType();
742 type.getLengthConstraint().ifPresent(constraint -> {
743 final Range<Integer> range = constraint.getAllowedRanges().span();
744 property.put(MIN_LENGTH_KEY, range.lowerEndpoint());
745 property.put(MAX_LENGTH_KEY, range.upperEndpoint());
748 if (type.getPatternConstraints().iterator().hasNext()) {
749 final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
750 String regex = pattern.getRegularExpressionString();
751 // Escape special characters to prevent issues inside Automaton.
752 regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
753 for (final var charClass : PREDEFINED_CHARACTER_CLASSES.entrySet()) {
754 regex = regex.replaceAll(charClass.getKey(), charClass.getValue());
756 String defaultValue = "";
758 final RegExp regExp = new RegExp(regex);
759 defaultValue = regExp.toAutomaton().getShortestExample(true);
760 } catch (IllegalArgumentException ex) {
761 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
763 setExampleValue(property, defaultValue);
765 setExampleValue(property, "Some " + nodeName);
768 stringType.getDefaultValue().ifPresent(v -> setDefaultValue(property, (String) v));
772 private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
773 final ObjectNode property) {
774 final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
775 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
777 if (isHexadecimalOrOctal(leafTypeDef)) {
781 if (leafTypeDef instanceof DecimalTypeDefinition) {
782 maybeLower.ifPresent(number -> setExampleValue(property, ((Decimal64) number).decimalValue()));
785 if (leafTypeDef instanceof Uint8TypeDefinition
786 || leafTypeDef instanceof Uint16TypeDefinition
787 || leafTypeDef instanceof Int8TypeDefinition
788 || leafTypeDef instanceof Int16TypeDefinition
789 || leafTypeDef instanceof Int32TypeDefinition) {
791 property.put(FORMAT_KEY, INT32_FORMAT);
792 maybeLower.ifPresent(number -> setExampleValue(property, Integer.valueOf(number.toString())));
793 } else if (leafTypeDef instanceof Uint32TypeDefinition
794 || leafTypeDef instanceof Int64TypeDefinition) {
796 property.put(FORMAT_KEY, INT64_FORMAT);
797 maybeLower.ifPresent(number -> setExampleValue(property, Long.valueOf(number.toString())));
800 setExampleValue(property, 0);
805 private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
806 final Optional<?> optDefaultValue = typeDef.getDefaultValue();
807 if (optDefaultValue.isPresent()) {
808 final String defaultValue = (String) optDefaultValue.orElseThrow();
809 return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
814 private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
815 final EffectiveModelContext schemaContext) {
816 // create example instance-identifier to the first container of node's module if exists or leave it empty
817 final var module = schemaContext.findModule(node.getQName().getModule());
818 if (module.isPresent()) {
819 final var container = module.orElseThrow().getChildNodes().stream()
820 .filter(n -> n instanceof ContainerSchemaNode)
822 container.ifPresent(c -> setExampleValue(property, String.format("/%s:%s", module.orElseThrow().getPrefix(),
823 c.getQName().getLocalName())));
829 private static ObjectNode buildXmlParameter(final SchemaNode node) {
830 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
831 final QName qName = node.getQName();
832 xml.put(NAME_KEY, qName.getLocalName());
833 xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
837 private static void setExampleValue(final ObjectNode property, final String value) {
838 property.put(EXAMPLE_KEY, value);
841 private static void setExampleValue(final ObjectNode property, final Integer value) {
842 property.put(EXAMPLE_KEY, value);
845 private static void setExampleValue(final ObjectNode property, final Long value) {
846 property.put(EXAMPLE_KEY, value);
849 private static void setExampleValue(final ObjectNode property, final BigDecimal value) {
850 property.put(EXAMPLE_KEY, value);
853 private static void setExampleValue(final ObjectNode property, final Boolean value) {
854 property.put(EXAMPLE_KEY, value);
857 private static void setDefaultValue(final ObjectNode property, final String value) {
858 property.put(DEFAULT_KEY, value);
861 private static void setDefaultValue(final ObjectNode property, final Long value) {
862 property.put(DEFAULT_KEY, value);
865 private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
866 property.put(DEFAULT_KEY, value);
869 private static void setDefaultValue(final ObjectNode property, final Boolean value) {
870 property.put(DEFAULT_KEY, value);