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.CONFIG;
13 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.NAME_KEY;
14 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.TOP;
15 import static org.opendaylight.restconf.openapi.model.builder.OperationBuilder.XML_KEY;
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.CaseSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
43 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
45 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
47 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraint;
48 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraintAware;
49 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.MandatoryAware;
54 import org.opendaylight.yangtools.yang.model.api.Module;
55 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
56 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
57 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
58 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
59 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
60 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
61 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition.Bit;
62 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
63 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
64 import org.opendaylight.yangtools.yang.model.api.type.EmptyTypeDefinition;
65 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
66 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition.EnumPair;
67 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
68 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
69 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
70 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
71 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
72 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
73 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
74 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
75 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
76 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
77 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
78 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
79 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
80 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
81 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
82 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
83 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
84 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
85 import org.slf4j.Logger;
86 import org.slf4j.LoggerFactory;
89 * Generates JSON Schema for data defined in YANG. This class is not thread-safe.
91 public class DefinitionGenerator {
93 private static final Logger LOG = LoggerFactory.getLogger(DefinitionGenerator.class);
95 private static final String UNIQUE_ITEMS_KEY = "uniqueItems";
96 private static final String MAX_ITEMS = "maxItems";
97 private static final String MIN_ITEMS = "minItems";
98 private static final String MAX_LENGTH_KEY = "maxLength";
99 private static final String MIN_LENGTH_KEY = "minLength";
100 private static final String REF_KEY = "$ref";
101 private static final String ITEMS_KEY = "items";
102 private static final String TYPE_KEY = "type";
103 private static final String DESCRIPTION_KEY = "description";
104 private static final String ARRAY_TYPE = "array";
105 private static final String ENUM_KEY = "enum";
106 private static final String TITLE_KEY = "title";
107 private static final String DEFAULT_KEY = "default";
108 private static final String EXAMPLE_KEY = "example";
109 private static final String FORMAT_KEY = "format";
110 private static final String NAMESPACE_KEY = "namespace";
111 public static final String INPUT = "input";
112 public static final String INPUT_SUFFIX = "_input";
113 public static final String OUTPUT = "output";
114 public static final String OUTPUT_SUFFIX = "_output";
115 private static final String STRING_TYPE = "string";
116 private static final String OBJECT_TYPE = "object";
117 private static final String NUMBER_TYPE = "number";
118 private static final String INTEGER_TYPE = "integer";
119 private static final String INT32_FORMAT = "int32";
120 private static final String INT64_FORMAT = "int64";
121 private static final String BOOLEAN_TYPE = "boolean";
122 // Special characters used in Automaton.
123 // See https://www.brics.dk/automaton/doc/dk/brics/automaton/RegExp.html
124 private static final Pattern AUTOMATON_SPECIAL_CHARACTERS = Pattern.compile("[@&\"<>#~]");
125 // Adaptation from YANG regex to Automaton regex
126 // See https://github.com/mifmif/Generex/blob/master/src/main/java/com/mifmif/common/regex/Generex.java
127 private static final Map<String, String> PREDEFINED_CHARACTER_CLASSES = Map.of("\\\\d", "[0-9]",
128 "\\\\D", "[^0-9]", "\\\\s", "[ \t\n\f\r]", "\\\\S", "[^ \t\n\f\r]",
129 "\\\\w", "[a-zA-Z_0-9]", "\\\\W", "[^a-zA-Z_0-9]");
131 private Module topLevelModule;
133 public DefinitionGenerator() {
137 * Creates Json definitions from provided module according to openapi spec.
139 * @param module - Yang module to be converted
140 * @param schemaContext - SchemaContext of all Yang files used by Api Doc
141 * @param definitionNames - Store for definition names
142 * @return {@link Map} containing data used for creating examples and definitions in OpenAPI documentation
143 * @throws IOException if I/O operation fails
145 public Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
146 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
147 final boolean isForSingleModule) throws IOException {
148 topLevelModule = module;
150 processIdentities(module, definitions, definitionNames, schemaContext);
151 processContainersAndLists(module, definitions, definitionNames, schemaContext);
152 processRPCs(module, definitions, definitionNames, schemaContext);
154 if (isForSingleModule) {
155 processModule(module, definitions, definitionNames, schemaContext);
161 public Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
162 final DefinitionNames definitionNames, final boolean isForSingleModule)
164 final Map<String, Schema> definitions = new HashMap<>();
165 if (isForSingleModule) {
166 definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
168 return convertToSchemas(module, schemaContext, definitions, definitionNames, isForSingleModule);
171 private void processModule(final Module module, final Map<String, Schema> definitions,
172 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) {
173 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
174 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
175 final String moduleName = module.getName();
176 final String definitionName = moduleName + MODULE_NAME_SUFFIX;
177 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
178 for (final DataSchemaNode node : module.getChildNodes()) {
179 stack.enterSchemaTree(node.getQName());
180 final String localName = node.getQName().getLocalName();
181 if (node.isConfiguration()) {
182 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
183 if (isSchemaNodeMandatory(node)) {
184 required.add(localName);
186 for (final DataSchemaNode childNode : ((DataNodeContainer) node).getChildNodes()) {
187 final ObjectNode childNodeProperties = JsonNodeFactory.instance.objectNode();
189 final String ref = COMPONENTS_PREFIX
190 + moduleName + CONFIG
192 + definitionNames.getDiscriminator(node);
194 if (node instanceof ListSchemaNode) {
195 childNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
196 final ObjectNode items = JsonNodeFactory.instance.objectNode();
197 items.put(REF_KEY, ref);
198 childNodeProperties.set(ITEMS_KEY, items);
199 childNodeProperties.put(DESCRIPTION_KEY, childNode.getDescription().orElse(""));
200 childNodeProperties.put(TITLE_KEY, localName + CONFIG);
203 Description can't be added, because nothing allowed alongside $ref.
204 allOf is not an option, because ServiceNow can't parse it.
206 childNodeProperties.put(REF_KEY, ref);
208 //add module name prefix to property name, when ServiceNow can process colons
209 properties.set(localName, childNodeProperties);
211 } else if (node instanceof LeafSchemaNode) {
213 Add module name prefix to property name, when ServiceNow can process colons(second parameter
216 processLeafNode((LeafSchemaNode) node, localName, properties, required, stack,
217 definitions, definitionNames, module.getNamespace());
222 final Schema.Builder definitionBuilder = new Schema.Builder()
223 .title(definitionName)
225 .properties(properties)
226 .description(module.getDescription().orElse(""))
227 .required(required.size() > 0 ? required : null);
229 definitions.put(definitionName, definitionBuilder.build());
232 private static boolean isSchemaNodeMandatory(final DataSchemaNode node) {
233 // https://www.rfc-editor.org/rfc/rfc7950#page-14
234 // mandatory node: A mandatory node is one of:
235 if (node instanceof ContainerSchemaNode containerNode) {
236 // A container node without a "presence" statement and that has at least one mandatory node as a child.
237 if (containerNode.isPresenceContainer()) {
240 for (final DataSchemaNode childNode : containerNode.getChildNodes()) {
241 if (childNode instanceof MandatoryAware mandatoryAware && mandatoryAware.isMandatory()) {
246 // A list or leaf-list node with a "min-elements" statement with a value greater than zero.
247 return node instanceof ElementCountConstraintAware constraintAware
248 && constraintAware.getElementCountConstraint()
249 .map(ElementCountConstraint::getMinElements)
254 private void processContainersAndLists(final Module module, final Map<String, Schema> definitions,
255 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
256 final String moduleName = module.getName();
257 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
258 for (final DataSchemaNode childNode : module.getChildNodes()) {
259 stack.enterSchemaTree(childNode.getQName());
260 // For every container and list in the module
261 if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
262 if (childNode.isConfiguration()) {
263 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
266 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
268 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack);
274 private void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
275 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
276 final SchemaInferenceStack stack) throws IOException {
277 for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
278 stack.enterSchemaTree(actionDef.getQName());
279 processOperations(actionDef, moduleName, definitions, definitionNames, stack);
284 private void processRPCs(final Module module, final Map<String, Schema> definitions,
285 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
286 final String moduleName = module.getName();
287 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
288 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
289 stack.enterSchemaTree(rpcDefinition.getQName());
290 processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack);
295 private void processOperations(final OperationDefinition operationDef, final String parentName,
296 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
297 final SchemaInferenceStack stack) throws IOException {
298 final String operationName = operationDef.getQName().getLocalName();
299 processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
300 definitionNames, stack);
301 processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
302 definitionNames, stack);
305 private void processOperationInputOutput(final ContainerLike container, final String operationName,
306 final String parentName, final boolean isInput, final Map<String, Schema> definitions,
307 final DefinitionNames definitionNames, final SchemaInferenceStack stack)
309 stack.enterSchemaTree(container.getQName());
310 if (!container.getChildNodes().isEmpty()) {
311 final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
312 final Schema.Builder childSchemaBuilder = new Schema.Builder()
315 .xml(JsonNodeFactory.instance.objectNode().put(NAME_KEY, isInput ? INPUT : OUTPUT));
316 processChildren(childSchemaBuilder, container.getChildNodes(), parentName, definitions, definitionNames,
318 final String discriminator =
319 definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
320 definitions.put(filename + discriminator, childSchemaBuilder.build());
321 processTopData(filename, discriminator, definitions, container);
326 private static ObjectNode processTopData(final String filename, final String discriminator,
327 final Map<String, Schema> definitions, final SchemaNode schemaNode) {
328 final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
329 final String name = filename + discriminator;
330 final String ref = COMPONENTS_PREFIX + name;
331 final String topName = filename + TOP;
333 if (schemaNode instanceof ListSchemaNode) {
334 dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
335 final ObjectNode items = JsonNodeFactory.instance.objectNode();
336 items.put(REF_KEY, ref);
337 dataNodeProperties.set(ITEMS_KEY, items);
338 dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
341 Description can't be added, because nothing allowed alongside $ref.
342 allOf is not an option, because ServiceNow can't parse it.
344 dataNodeProperties.put(REF_KEY, ref);
347 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
349 Add module name prefix to property name, when needed, when ServiceNow can process colons,
350 use RestDocGenUtil#resolveNodesName for creating property name
352 properties.set(schemaNode.getQName().getLocalName(), dataNodeProperties);
353 final var schema = new Schema.Builder()
355 .properties(properties)
359 definitions.put(topName + discriminator, schema);
361 return dataNodeProperties;
365 * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
366 * @param module The module from which the identity stmt will be processed
367 * @param definitions The ObjectNode in which the parsed identity will be put as a 'model' obj
368 * @param definitionNames Store for definition names
370 private static void processIdentities(final Module module, final Map<String, Schema> definitions,
371 final DefinitionNames definitionNames, final EffectiveModelContext context) {
372 final String moduleName = module.getName();
373 final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
374 LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
376 for (final IdentitySchemaNode idNode : idNodes) {
377 final Schema identityObj = buildIdentityObject(idNode, context);
378 final String idName = idNode.getQName().getLocalName();
379 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
380 final String name = idName + discriminator;
381 definitions.put(name, identityObj);
385 private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
386 final ArrayNode enumPayload, final EffectiveModelContext context) {
387 for (final IdentitySchemaNode derivedId : derivedIds) {
388 enumPayload.add(derivedId.getQName().getLocalName());
389 populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
393 private ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
394 final Map<String, Schema> definitions, final DefinitionNames definitionNames, final boolean isConfig,
395 final SchemaInferenceStack stack) throws IOException {
396 if (dataNode instanceof ListSchemaNode || dataNode instanceof ContainerSchemaNode) {
397 final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
398 final SchemaNode schemaNode = (SchemaNode) dataNode;
399 final String localName = schemaNode.getQName().getLocalName();
400 final String nodeName = parentName + (isConfig ? CONFIG : "") + "_" + localName;
401 final Schema.Builder childSchemaBuilder = new Schema.Builder()
404 .description(schemaNode.getDescription().orElse(""));
406 childSchemaBuilder.properties(processChildren(childSchemaBuilder, containerChildren,
407 parentName + "_" + localName, definitions, definitionNames, isConfig, stack));
409 final String discriminator;
410 if (!definitionNames.isListedNode(schemaNode)) {
411 final String parentNameConfigLocalName = parentName + CONFIG + "_" + localName;
412 final String nameAsParent = parentName + "_" + localName;
413 final List<String> names = List.of(parentNameConfigLocalName, parentNameConfigLocalName + TOP,
414 nameAsParent, nameAsParent + TOP);
415 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
417 discriminator = definitionNames.getDiscriminator(schemaNode);
420 final String defName = nodeName + discriminator;
421 childSchemaBuilder.xml(buildXmlParameter(schemaNode));
422 definitions.put(defName, childSchemaBuilder.build());
424 return processTopData(nodeName, discriminator, definitions, schemaNode);
430 * Processes the nodes.
432 private ObjectNode processChildren(final Schema.Builder parentNodeBuilder,
433 final Collection<? extends DataSchemaNode> nodes, final String parentName,
434 final Map<String, Schema> definitions, final DefinitionNames definitionNames, final boolean isConfig,
435 final SchemaInferenceStack stack) throws IOException {
436 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
437 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
438 for (final DataSchemaNode node : nodes) {
439 if (!isConfig || node.isConfiguration()) {
440 processChildNode(node, parentName, definitions, definitionNames, isConfig, stack, properties, required);
443 parentNodeBuilder.properties(properties).required(required.size() > 0 ? required : null);
447 private void processChildNode(final DataSchemaNode node, final String parentName,
448 final Map<String, Schema> definitions, final DefinitionNames definitionNames, final boolean isConfig,
449 final SchemaInferenceStack stack, final ObjectNode properties, final ArrayNode required)
451 final XMLNamespace parentNamespace = stack.toSchemaNodeIdentifier().lastNodeIdentifier().getNamespace();
452 stack.enterSchemaTree(node.getQName());
455 Add module name prefix to property name, when needed, when ServiceNow can process colons,
456 use RestDocGenUtil#resolveNodesName for creating property name
458 final String name = node.getQName().getLocalName();
460 if (node instanceof LeafSchemaNode leaf) {
461 processLeafNode(leaf, name, properties, required, stack, definitions, definitionNames, parentNamespace);
463 } else if (node instanceof AnyxmlSchemaNode anyxml) {
464 processAnyXMLNode(anyxml, name, properties, required, parentNamespace);
466 } else if (node instanceof AnydataSchemaNode anydata) {
467 processAnydataNode(anydata, name, properties, required, parentNamespace);
471 final ObjectNode property;
472 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
473 if (isSchemaNodeMandatory(node)) {
476 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions,
477 definitionNames, isConfig, stack);
479 processActionNodeContainer(node, parentName, definitions, definitionNames, stack);
481 } else if (node instanceof LeafListSchemaNode leafList) {
482 if (isSchemaNodeMandatory(node)) {
485 property = processLeafListNode(leafList, stack, definitions, definitionNames);
487 } else if (node instanceof ChoiceSchemaNode choice) {
488 if (!choice.getCases().isEmpty()) {
489 CaseSchemaNode caseSchemaNode = choice.getDefaultCase()
490 .orElse(choice.getCases().stream()
491 .findFirst().orElseThrow());
492 stack.enterSchemaTree(caseSchemaNode.getQName());
493 for (final DataSchemaNode childNode : caseSchemaNode.getChildNodes()) {
494 processChildNode(childNode, parentName, definitions, definitionNames, isConfig, stack,
495 properties, required);
502 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
504 if (property != null) {
505 properties.set(name, property);
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 processElementCount(optConstraint, 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 Optional<ElementCountConstraint> constraint, final ObjectNode props) {
530 if (constraint.isPresent()) {
531 final ElementCountConstraint constr = constraint.orElseThrow();
532 final Integer minElements = constr.getMinElements();
533 if (minElements != null) {
534 props.put(MIN_ITEMS, minElements);
536 final Integer maxElements = constr.getMaxElements();
537 if (maxElements != null) {
538 props.put(MAX_ITEMS, maxElements);
543 private static void processMandatory(final MandatoryAware node, final String nodeName, final ArrayNode required) {
544 if (node.isMandatory()) {
545 required.add(nodeName);
549 private ObjectNode processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
550 final ObjectNode properties, final ArrayNode required, final SchemaInferenceStack stack,
551 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
552 final XMLNamespace parentNamespace) {
553 final ObjectNode property = JsonNodeFactory.instance.objectNode();
555 final String leafDescription = leafNode.getDescription().orElse("");
557 Description can't be added, because nothing allowed alongside $ref.
558 allOf is not an option, because ServiceNow can't parse it.
560 if (!(leafNode.getType() instanceof IdentityrefTypeDefinition)) {
561 property.put(DESCRIPTION_KEY, leafDescription);
564 processTypeDef(leafNode.getType(), leafNode, property, stack, definitions, definitionNames);
565 properties.set(jsonLeafName, property);
566 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
567 // If the parent is not from the same model, define the child XML namespace.
568 property.set(XML_KEY, buildXmlParameter(leafNode));
570 processMandatory(leafNode, jsonLeafName, required);
575 private static ObjectNode processAnydataNode(final AnydataSchemaNode leafNode, final String name,
576 final ObjectNode properties, final ArrayNode required, final XMLNamespace parentNamespace) {
577 final ObjectNode property = JsonNodeFactory.instance.objectNode();
579 final String leafDescription = leafNode.getDescription().orElse("");
580 property.put(DESCRIPTION_KEY, leafDescription);
582 final String localName = leafNode.getQName().getLocalName();
583 setExampleValue(property, String.format("<%s> ... </%s>", localName, localName));
584 property.put(TYPE_KEY, STRING_TYPE);
585 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
586 // If the parent is not from the same model, define the child XML namespace.
587 property.set(XML_KEY, buildXmlParameter(leafNode));
589 processMandatory(leafNode, name, required);
590 properties.set(name, property);
595 private static ObjectNode processAnyXMLNode(final AnyxmlSchemaNode leafNode, final String name,
596 final ObjectNode properties, final ArrayNode required, final XMLNamespace parentNamespace) {
597 final ObjectNode property = JsonNodeFactory.instance.objectNode();
599 final String leafDescription = leafNode.getDescription().orElse("");
600 property.put(DESCRIPTION_KEY, leafDescription);
602 final String localName = leafNode.getQName().getLocalName();
603 setExampleValue(property, String.format("<%s> ... </%s>", localName, localName));
604 property.put(TYPE_KEY, STRING_TYPE);
605 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
606 // If the parent is not from the same model, define the child XML namespace.
607 property.set(XML_KEY, buildXmlParameter(leafNode));
609 processMandatory(leafNode, name, required);
610 properties.set(name, property);
615 private String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
616 final ObjectNode property, final SchemaInferenceStack stack, final Map<String, Schema> definitions,
617 final DefinitionNames definitionNames) {
618 final String jsonType;
619 if (leafTypeDef instanceof BinaryTypeDefinition) {
620 jsonType = processBinaryType(property);
622 } else if (leafTypeDef instanceof BitsTypeDefinition) {
623 jsonType = processBitsType((BitsTypeDefinition) leafTypeDef, property);
625 } else if (leafTypeDef instanceof EnumTypeDefinition) {
626 jsonType = processEnumType((EnumTypeDefinition) leafTypeDef, property);
628 } else if (leafTypeDef instanceof IdentityrefTypeDefinition) {
629 jsonType = processIdentityRefType((IdentityrefTypeDefinition) leafTypeDef, property, definitions,
630 definitionNames, stack.getEffectiveModelContext());
632 } else if (leafTypeDef instanceof StringTypeDefinition) {
633 jsonType = processStringType(leafTypeDef, property, node.getQName().getLocalName());
635 } else if (leafTypeDef instanceof UnionTypeDefinition) {
636 jsonType = processUnionType((UnionTypeDefinition) leafTypeDef);
638 } else if (leafTypeDef instanceof EmptyTypeDefinition) {
639 jsonType = OBJECT_TYPE;
640 } else if (leafTypeDef instanceof LeafrefTypeDefinition) {
641 return processTypeDef(stack.resolveLeafref((LeafrefTypeDefinition) leafTypeDef), node, property,
642 stack, definitions, definitionNames);
643 } else if (leafTypeDef instanceof BooleanTypeDefinition) {
644 jsonType = BOOLEAN_TYPE;
645 setExampleValue(property, true);
646 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
647 jsonType = processNumberType((RangeRestrictedTypeDefinition<?, ?>) leafTypeDef, property);
648 } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
649 jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
651 jsonType = STRING_TYPE;
653 if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
654 putIfNonNull(property, TYPE_KEY, jsonType);
655 if (leafTypeDef.getDefaultValue().isPresent()) {
656 final Object defaultValue = leafTypeDef.getDefaultValue().orElseThrow();
657 if (defaultValue instanceof String stringDefaultValue) {
658 if (leafTypeDef instanceof BooleanTypeDefinition) {
659 setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
660 } else if (leafTypeDef instanceof DecimalTypeDefinition
661 || leafTypeDef instanceof Uint64TypeDefinition) {
662 setDefaultValue(property, new BigDecimal(stringDefaultValue));
663 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
664 //uint8,16,32 int8,16,32,64
665 if (isHexadecimalOrOctal((RangeRestrictedTypeDefinition<?, ?>)leafTypeDef)) {
666 setDefaultValue(property, stringDefaultValue);
668 setDefaultValue(property, Long.valueOf(stringDefaultValue));
671 setDefaultValue(property, stringDefaultValue);
674 //we should never get here. getDefaultValue always gives us string
675 setDefaultValue(property, defaultValue.toString());
682 private static String processBinaryType(final ObjectNode property) {
683 property.put(FORMAT_KEY, "byte");
687 private static String processEnumType(final EnumTypeDefinition enumLeafType, final ObjectNode property) {
688 final List<EnumPair> enumPairs = enumLeafType.getValues();
689 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
690 for (final EnumPair enumPair : enumPairs) {
691 enumNames.add(new TextNode(enumPair.getName()));
694 property.set(ENUM_KEY, enumNames);
695 enumLeafType.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
696 setExampleValue(property, enumLeafType.getValues().iterator().next().getName());
700 private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
701 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
702 final EffectiveModelContext schemaContext) {
703 final String definitionName;
704 if (isImported(leafTypeDef)) {
705 definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
707 final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
708 definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
710 property.put(REF_KEY, COMPONENTS_PREFIX + definitionName);
714 private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
715 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
716 final EffectiveModelContext context) {
717 final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
718 final String identityName = idNode.getQName().getLocalName();
719 if (!definitionNames.isListedNode(idNode)) {
720 final Schema identityObj = buildIdentityObject(idNode, context);
721 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
722 final String name = identityName + discriminator;
723 definitions.put(name, identityObj);
726 return identityName + definitionNames.getDiscriminator(idNode);
730 private static Schema buildIdentityObject(final IdentitySchemaNode idNode, final EffectiveModelContext context) {
731 final String identityName = idNode.getQName().getLocalName();
732 LOG.debug("Processing Identity: {}", identityName);
734 final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
735 final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
736 enumPayload.add(identityName);
737 populateEnumWithDerived(derivedIds, enumPayload, context);
739 return new Schema.Builder()
741 .description(idNode.getDescription().orElse(""))
742 .schemaEnum(enumPayload)
747 private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
748 return !leafTypeDef.getQName().getModule().equals(topLevelModule.getQNameModule());
751 private static String processBitsType(final BitsTypeDefinition bitsType, final ObjectNode property) {
752 property.put(MIN_ITEMS, 0);
753 property.put(UNIQUE_ITEMS_KEY, true);
754 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
755 final Collection<? extends Bit> bits = bitsType.getBits();
756 for (final Bit bit : bits) {
757 enumNames.add(new TextNode(bit.getName()));
759 property.set(ENUM_KEY, enumNames);
760 property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
764 private static String processStringType(final TypeDefinition<?> stringType, final ObjectNode property,
765 final String nodeName) {
766 StringTypeDefinition type = (StringTypeDefinition) stringType;
767 Optional<LengthConstraint> lengthConstraints = ((StringTypeDefinition) stringType).getLengthConstraint();
768 while (lengthConstraints.isEmpty() && type.getBaseType() != null) {
769 type = type.getBaseType();
770 lengthConstraints = type.getLengthConstraint();
773 if (lengthConstraints.isPresent()) {
774 final Range<Integer> range = lengthConstraints.orElseThrow().getAllowedRanges().span();
775 putIfNonNull(property, MIN_LENGTH_KEY, range.lowerEndpoint());
776 putIfNonNull(property, MAX_LENGTH_KEY, range.upperEndpoint());
779 if (type.getPatternConstraints().iterator().hasNext()) {
780 final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
781 String regex = pattern.getRegularExpressionString();
782 // Escape special characters to prevent issues inside Automaton.
783 regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
784 for (final var charClass : PREDEFINED_CHARACTER_CLASSES.entrySet()) {
785 regex = regex.replaceAll(charClass.getKey(), charClass.getValue());
787 String defaultValue = "";
789 final RegExp regExp = new RegExp(regex);
790 defaultValue = regExp.toAutomaton().getShortestExample(true);
791 } catch (IllegalArgumentException ex) {
792 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
794 setExampleValue(property, defaultValue);
796 setExampleValue(property, "Some " + nodeName);
801 private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
802 final ObjectNode property) {
803 final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
804 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
806 if (isHexadecimalOrOctal(leafTypeDef)) {
810 if (leafTypeDef instanceof DecimalTypeDefinition) {
811 maybeLower.ifPresent(number -> setExampleValue(property, ((Decimal64) number).decimalValue()));
814 if (leafTypeDef instanceof Uint8TypeDefinition
815 || leafTypeDef instanceof Uint16TypeDefinition
816 || leafTypeDef instanceof Int8TypeDefinition
817 || leafTypeDef instanceof Int16TypeDefinition
818 || leafTypeDef instanceof Int32TypeDefinition) {
820 property.put(FORMAT_KEY, INT32_FORMAT);
821 maybeLower.ifPresent(number -> setExampleValue(property, Integer.valueOf(number.toString())));
822 } else if (leafTypeDef instanceof Uint32TypeDefinition
823 || leafTypeDef instanceof Int64TypeDefinition) {
825 property.put(FORMAT_KEY, INT64_FORMAT);
826 maybeLower.ifPresent(number -> setExampleValue(property, Long.valueOf(number.toString())));
829 setExampleValue(property, 0);
834 private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
835 final Optional<?> optDefaultValue = typeDef.getDefaultValue();
836 if (optDefaultValue.isPresent()) {
837 final String defaultValue = (String) optDefaultValue.orElseThrow();
838 return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
843 private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
844 final EffectiveModelContext schemaContext) {
845 // create example instance-identifier to the first container of node's module if exists or leave it empty
846 final var module = schemaContext.findModule(node.getQName().getModule());
847 if (module.isPresent()) {
848 final var container = module.orElseThrow().getChildNodes().stream()
849 .filter(n -> n instanceof ContainerSchemaNode)
851 container.ifPresent(c -> setExampleValue(property, String.format("/%s:%s", module.orElseThrow().getPrefix(),
852 c.getQName().getLocalName())));
858 private static String processUnionType(final UnionTypeDefinition unionType) {
859 boolean isStringTakePlace = false;
860 boolean isNumberTakePlace = false;
861 boolean isBooleanTakePlace = false;
862 for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
863 if (!isStringTakePlace) {
864 if (typeDef instanceof StringTypeDefinition
865 || typeDef instanceof BitsTypeDefinition
866 || typeDef instanceof BinaryTypeDefinition
867 || typeDef instanceof IdentityrefTypeDefinition
868 || typeDef instanceof EnumTypeDefinition
869 || typeDef instanceof LeafrefTypeDefinition
870 || typeDef instanceof UnionTypeDefinition) {
871 isStringTakePlace = true;
872 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
873 isNumberTakePlace = true;
874 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
875 isBooleanTakePlace = true;
879 if (isStringTakePlace) {
882 if (isBooleanTakePlace) {
883 if (isNumberTakePlace) {
891 private static ObjectNode buildXmlParameter(final SchemaNode node) {
892 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
893 final QName qName = node.getQName();
894 xml.put(NAME_KEY, qName.getLocalName());
895 xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
899 private static void putIfNonNull(final ObjectNode property, final String key, final Number number) {
900 if (key != null && number != null) {
901 if (number instanceof Double) {
902 property.put(key, (Double) number);
903 } else if (number instanceof Float) {
904 property.put(key, (Float) number);
905 } else if (number instanceof Integer) {
906 property.put(key, (Integer) number);
907 } else if (number instanceof Short) {
908 property.put(key, (Short) number);
909 } else if (number instanceof Long) {
910 property.put(key, (Long) number);
915 private static void putIfNonNull(final ObjectNode property, final String key, final String value) {
916 if (key != null && value != null) {
917 property.put(key, value);
921 private static void setExampleValue(final ObjectNode property, final String value) {
922 property.put(EXAMPLE_KEY, value);
925 private static void setExampleValue(final ObjectNode property, final Integer value) {
926 property.put(EXAMPLE_KEY, value);
929 private static void setExampleValue(final ObjectNode property, final Long value) {
930 property.put(EXAMPLE_KEY, value);
933 private static void setExampleValue(final ObjectNode property, final BigDecimal value) {
934 property.put(EXAMPLE_KEY, value);
937 private static void setExampleValue(final ObjectNode property, final Boolean value) {
938 property.put(EXAMPLE_KEY, value);
941 private static void setDefaultValue(final ObjectNode property, final String value) {
942 property.put(DEFAULT_KEY, value);
945 private static void setDefaultValue(final ObjectNode property, final Long value) {
946 property.put(DEFAULT_KEY, value);
949 private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
950 property.put(DEFAULT_KEY, value);
953 private static void setDefaultValue(final ObjectNode property, final Boolean value) {
954 property.put(DEFAULT_KEY, value);