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.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 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 Module topLevelModule;
131 public DefinitionGenerator() {
135 * Creates Json definitions from provided module according to openapi spec.
137 * @param module - Yang module to be converted
138 * @param schemaContext - SchemaContext of all Yang files used by Api Doc
139 * @param definitionNames - Store for definition names
140 * @return {@link Map} containing data used for creating examples and definitions in OpenAPI documentation
141 * @throws IOException if I/O operation fails
143 public Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
144 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
145 final boolean isForSingleModule) throws IOException {
146 topLevelModule = module;
148 processIdentities(module, definitions, definitionNames, schemaContext);
149 processContainersAndLists(module, definitions, definitionNames, schemaContext);
150 processRPCs(module, definitions, definitionNames, schemaContext);
152 if (isForSingleModule) {
153 processModule(module, definitions, definitionNames, schemaContext);
159 public Map<String, Schema> convertToSchemas(final Module module, final EffectiveModelContext schemaContext,
160 final DefinitionNames definitionNames, final boolean isForSingleModule)
162 final Map<String, Schema> definitions = new HashMap<>();
163 if (isForSingleModule) {
164 definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
166 return convertToSchemas(module, schemaContext, definitions, definitionNames, isForSingleModule);
169 private void processModule(final Module module, final Map<String, Schema> definitions,
170 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) {
171 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
172 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
173 final String moduleName = module.getName();
174 final String definitionName = moduleName + MODULE_NAME_SUFFIX;
175 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
176 for (final DataSchemaNode node : module.getChildNodes()) {
177 stack.enterSchemaTree(node.getQName());
178 final String localName = node.getQName().getLocalName();
179 if (node.isConfiguration()) {
180 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
181 if (isSchemaNodeMandatory(node)) {
182 required.add(localName);
184 for (final DataSchemaNode childNode : ((DataNodeContainer) node).getChildNodes()) {
185 final ObjectNode childNodeProperties = JsonNodeFactory.instance.objectNode();
187 final String ref = COMPONENTS_PREFIX
188 + moduleName + CONFIG
190 + definitionNames.getDiscriminator(node);
192 if (node instanceof ListSchemaNode) {
193 childNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
194 final ObjectNode items = JsonNodeFactory.instance.objectNode();
195 items.put(REF_KEY, ref);
196 childNodeProperties.set(ITEMS_KEY, items);
197 childNodeProperties.put(DESCRIPTION_KEY, childNode.getDescription().orElse(""));
198 childNodeProperties.put(TITLE_KEY, localName + CONFIG);
201 Description can't be added, because nothing allowed alongside $ref.
202 allOf is not an option, because ServiceNow can't parse it.
204 childNodeProperties.put(REF_KEY, ref);
206 //add module name prefix to property name, when ServiceNow can process colons
207 properties.set(localName, childNodeProperties);
209 } else if (node instanceof LeafSchemaNode) {
211 Add module name prefix to property name, when ServiceNow can process colons(second parameter
214 final ObjectNode leafNode = processLeafNode((LeafSchemaNode) node, localName, required, stack,
215 definitions, definitionNames, module.getNamespace());
216 properties.set(localName, leafNode);
221 final Schema.Builder definitionBuilder = new Schema.Builder()
222 .title(definitionName)
224 .properties(properties)
225 .description(module.getDescription().orElse(""))
226 .required(required.size() > 0 ? required : null);
228 definitions.put(definitionName, definitionBuilder.build());
231 private static boolean isSchemaNodeMandatory(final DataSchemaNode node) {
232 // https://www.rfc-editor.org/rfc/rfc7950#page-14
233 // mandatory node: A mandatory node is one of:
234 if (node instanceof ContainerSchemaNode containerNode) {
235 // A container node without a "presence" statement and that has at least one mandatory node as a child.
236 if (containerNode.isPresenceContainer()) {
239 for (final DataSchemaNode childNode : containerNode.getChildNodes()) {
240 if (childNode instanceof MandatoryAware mandatoryAware && mandatoryAware.isMandatory()) {
245 // A list or leaf-list node with a "min-elements" statement with a value greater than zero.
246 return node instanceof ElementCountConstraintAware constraintAware
247 && constraintAware.getElementCountConstraint()
248 .map(ElementCountConstraint::getMinElements)
253 private void processContainersAndLists(final Module module, final Map<String, Schema> definitions,
254 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
255 final String moduleName = module.getName();
256 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
257 for (final DataSchemaNode childNode : module.getChildNodes()) {
258 stack.enterSchemaTree(childNode.getQName());
259 // For every container and list in the module
260 if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
261 if (childNode.isConfiguration()) {
262 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
265 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack);
271 private void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
272 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
273 final SchemaInferenceStack stack) throws IOException {
274 for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
275 stack.enterSchemaTree(actionDef.getQName());
276 processOperations(actionDef, moduleName, definitions, definitionNames, stack);
281 private void processRPCs(final Module module, final Map<String, Schema> definitions,
282 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
283 final String moduleName = module.getName();
284 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
285 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
286 stack.enterSchemaTree(rpcDefinition.getQName());
287 processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack);
292 private void processOperations(final OperationDefinition operationDef, final String parentName,
293 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
294 final SchemaInferenceStack stack) throws IOException {
295 final String operationName = operationDef.getQName().getLocalName();
296 processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
297 definitionNames, stack);
298 processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
299 definitionNames, stack);
302 private void processOperationInputOutput(final ContainerLike container, final String operationName,
303 final String parentName, final boolean isInput, final Map<String, Schema> definitions,
304 final DefinitionNames definitionNames, final SchemaInferenceStack stack)
306 stack.enterSchemaTree(container.getQName());
307 if (!container.getChildNodes().isEmpty()) {
308 final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
309 final Schema.Builder childSchemaBuilder = new Schema.Builder()
312 .xml(JsonNodeFactory.instance.objectNode().put(NAME_KEY, isInput ? INPUT : OUTPUT));
313 processChildren(childSchemaBuilder, container.getChildNodes(), parentName, definitions, definitionNames,
315 final String discriminator =
316 definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
317 definitions.put(filename + discriminator, childSchemaBuilder.build());
318 processTopData(filename, discriminator, definitions, container);
323 private static ObjectNode processTopData(final String filename, final String discriminator,
324 final Map<String, Schema> definitions, final SchemaNode schemaNode) {
325 final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
326 final String name = filename + discriminator;
327 final String ref = COMPONENTS_PREFIX + name;
328 final String topName = filename + TOP;
330 if (schemaNode instanceof ListSchemaNode) {
331 dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
332 final ObjectNode items = JsonNodeFactory.instance.objectNode();
333 items.put(REF_KEY, ref);
334 dataNodeProperties.set(ITEMS_KEY, items);
335 dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
338 Description can't be added, because nothing allowed alongside $ref.
339 allOf is not an option, because ServiceNow can't parse it.
341 dataNodeProperties.put(REF_KEY, ref);
344 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
346 Add module name prefix to property name, when needed, when ServiceNow can process colons,
347 use RestDocGenUtil#resolveNodesName for creating property name
349 properties.set(schemaNode.getQName().getLocalName(), dataNodeProperties);
350 final var schema = new Schema.Builder()
352 .properties(properties)
356 definitions.put(topName + discriminator, schema);
358 return dataNodeProperties;
362 * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
363 * @param module The module from which the identity stmt will be processed
364 * @param definitions The ObjectNode in which the parsed identity will be put as a 'model' obj
365 * @param definitionNames Store for definition names
367 private static void processIdentities(final Module module, final Map<String, Schema> definitions,
368 final DefinitionNames definitionNames, final EffectiveModelContext context) {
369 final String moduleName = module.getName();
370 final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
371 LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
373 for (final IdentitySchemaNode idNode : idNodes) {
374 final Schema identityObj = buildIdentityObject(idNode, context);
375 final String idName = idNode.getQName().getLocalName();
376 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
377 final String name = idName + discriminator;
378 definitions.put(name, identityObj);
382 private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
383 final ArrayNode enumPayload, final EffectiveModelContext context) {
384 for (final IdentitySchemaNode derivedId : derivedIds) {
385 enumPayload.add(derivedId.getQName().getLocalName());
386 populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
390 private ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
391 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
392 final SchemaInferenceStack stack) throws IOException {
393 final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
394 final SchemaNode schemaNode = (SchemaNode) dataNode;
395 final String localName = schemaNode.getQName().getLocalName();
396 final String nodeName = parentName + CONFIG + "_" + localName;
397 final Schema.Builder childSchemaBuilder = new Schema.Builder()
400 .description(schemaNode.getDescription().orElse(""));
402 childSchemaBuilder.properties(processChildren(childSchemaBuilder, containerChildren,
403 parentName + "_" + localName, definitions, definitionNames, stack));
405 final String discriminator;
406 if (!definitionNames.isListedNode(schemaNode)) {
407 final String parentNameConfigLocalName = parentName + CONFIG + "_" + localName;
408 final String nameAsParent = parentName + "_" + localName;
409 final List<String> names = List.of(parentNameConfigLocalName, parentNameConfigLocalName + TOP,
410 nameAsParent, nameAsParent + TOP);
411 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
413 discriminator = definitionNames.getDiscriminator(schemaNode);
416 final String defName = nodeName + discriminator;
417 childSchemaBuilder.xml(buildXmlParameter(schemaNode));
418 definitions.put(defName, childSchemaBuilder.build());
420 return processTopData(nodeName, discriminator, definitions, schemaNode);
424 * Processes the nodes.
426 private ObjectNode processChildren(final Schema.Builder parentNodeBuilder,
427 final Collection<? extends DataSchemaNode> nodes, final String parentName,
428 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
429 final SchemaInferenceStack stack) throws IOException {
430 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
431 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
432 for (final DataSchemaNode node : nodes) {
433 if (node.isConfiguration()) {
434 if (node instanceof ChoiceSchemaNode choice) {
435 stack.enterSchemaTree(node.getQName());
436 final Map<String, ObjectNode> choiceProperties = processChoiceNodeRecursively(parentName,
437 definitions, definitionNames, stack, required, choice);
438 choiceProperties.forEach(properties::set);
441 final ObjectNode property = processChildNode(node, parentName, definitions, definitionNames,
443 properties.set(node.getQName().getLocalName(), property);
447 parentNodeBuilder.properties(properties).required(required.size() > 0 ? required : null);
451 private Map<String, ObjectNode> processChoiceNodeRecursively(final String parentName,
452 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
453 final SchemaInferenceStack stack, final ArrayNode required, final ChoiceSchemaNode choice)
455 if (!choice.getCases().isEmpty()) {
456 final var properties = new HashMap<String, ObjectNode>();
457 final var caseSchemaNode = choice.getDefaultCase().orElse(choice.getCases().stream()
458 .findFirst().orElseThrow());
459 stack.enterSchemaTree(caseSchemaNode.getQName());
460 for (final var childNode : caseSchemaNode.getChildNodes()) {
461 if (childNode instanceof ChoiceSchemaNode childChoice) {
462 stack.enterSchemaTree(childNode.getQName());
463 final var childProperties = processChoiceNodeRecursively(parentName, definitions, definitionNames,
464 stack, required, childChoice);
465 properties.putAll(childProperties);
468 final var property = processChildNode(childNode, parentName, definitions, definitionNames, stack,
470 properties.put(childNode.getQName().getLocalName(), property);
479 private ObjectNode processChildNode(final DataSchemaNode node, final String parentName,
480 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
481 final SchemaInferenceStack stack, final ArrayNode required) throws IOException {
482 final XMLNamespace parentNamespace = stack.toSchemaNodeIdentifier().lastNodeIdentifier().getNamespace();
483 stack.enterSchemaTree(node.getQName());
485 Add module name prefix to property name, when needed, when ServiceNow can process colons,
486 use RestDocGenUtil#resolveNodesName for creating property name
488 final String name = node.getQName().getLocalName();
489 final ObjectNode property;
490 if (node instanceof LeafSchemaNode leaf) {
491 property = processLeafNode(leaf, name, required, stack, definitions, definitionNames, parentNamespace);
492 } else if (node instanceof AnyxmlSchemaNode anyxml) {
493 property = processAnyXMLNode(anyxml, name, required, parentNamespace);
494 } else if (node instanceof AnydataSchemaNode anydata) {
495 property = processAnydataNode(anydata, name, required, parentNamespace);
496 } else if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
497 if (isSchemaNodeMandatory(node)) {
500 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions, definitionNames,
502 processActionNodeContainer(node, parentName, definitions, definitionNames, stack);
503 } else if (node instanceof LeafListSchemaNode leafList) {
504 if (isSchemaNodeMandatory(node)) {
507 property = processLeafListNode(leafList, stack, definitions, definitionNames);
509 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
515 private ObjectNode processLeafListNode(final LeafListSchemaNode listNode, final SchemaInferenceStack stack,
516 final Map<String, Schema> definitions, final DefinitionNames definitionNames) {
517 final ObjectNode props = JsonNodeFactory.instance.objectNode();
518 props.put(TYPE_KEY, ARRAY_TYPE);
520 final ObjectNode itemsVal = JsonNodeFactory.instance.objectNode();
521 final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
522 optConstraint.ifPresent(elementCountConstraint -> processElementCount(elementCountConstraint, props));
524 processTypeDef(listNode.getType(), listNode, itemsVal, stack, definitions, definitionNames);
525 props.set(ITEMS_KEY, itemsVal);
527 props.put(DESCRIPTION_KEY, listNode.getDescription().orElse(""));
532 private static void processElementCount(final ElementCountConstraint constraint, final ObjectNode props) {
533 final Integer minElements = constraint.getMinElements();
534 if (minElements != null) {
535 props.put(MIN_ITEMS, minElements);
537 final Integer maxElements = constraint.getMaxElements();
538 if (maxElements != null) {
539 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 ArrayNode required, final SchemaInferenceStack stack, final Map<String, Schema> definitions,
551 final DefinitionNames definitionNames, final XMLNamespace parentNamespace) {
552 final ObjectNode property = JsonNodeFactory.instance.objectNode();
554 final String leafDescription = leafNode.getDescription().orElse("");
556 Description can't be added, because nothing allowed alongside $ref.
557 allOf is not an option, because ServiceNow can't parse it.
559 if (!(leafNode.getType() instanceof IdentityrefTypeDefinition)) {
560 property.put(DESCRIPTION_KEY, leafDescription);
563 processTypeDef(leafNode.getType(), leafNode, property, stack, definitions, definitionNames);
564 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
565 // If the parent is not from the same model, define the child XML namespace.
566 property.set(XML_KEY, buildXmlParameter(leafNode));
568 processMandatory(leafNode, jsonLeafName, required);
572 private static ObjectNode processAnydataNode(final AnydataSchemaNode leafNode, final String name,
573 final ArrayNode required, final XMLNamespace parentNamespace) {
574 final ObjectNode property = JsonNodeFactory.instance.objectNode();
576 final String leafDescription = leafNode.getDescription().orElse("");
577 property.put(DESCRIPTION_KEY, leafDescription);
579 final String localName = leafNode.getQName().getLocalName();
580 setExampleValue(property, String.format("<%s> ... </%s>", localName, localName));
581 property.put(TYPE_KEY, STRING_TYPE);
582 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
583 // If the parent is not from the same model, define the child XML namespace.
584 property.set(XML_KEY, buildXmlParameter(leafNode));
586 processMandatory(leafNode, name, required);
590 private static ObjectNode processAnyXMLNode(final AnyxmlSchemaNode leafNode, final String name,
591 final ArrayNode required, final XMLNamespace parentNamespace) {
592 final ObjectNode property = JsonNodeFactory.instance.objectNode();
594 final String leafDescription = leafNode.getDescription().orElse("");
595 property.put(DESCRIPTION_KEY, leafDescription);
597 final String localName = leafNode.getQName().getLocalName();
598 setExampleValue(property, String.format("<%s> ... </%s>", localName, localName));
599 property.put(TYPE_KEY, STRING_TYPE);
600 if (!leafNode.getQName().getNamespace().equals(parentNamespace)) {
601 // If the parent is not from the same model, define the child XML namespace.
602 property.set(XML_KEY, buildXmlParameter(leafNode));
604 processMandatory(leafNode, name, required);
608 private String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
609 final ObjectNode property, final SchemaInferenceStack stack, final Map<String, Schema> definitions,
610 final DefinitionNames definitionNames) {
611 final String jsonType;
612 if (leafTypeDef instanceof BinaryTypeDefinition binaryType) {
613 jsonType = processBinaryType(binaryType, property);
614 } else if (leafTypeDef instanceof BitsTypeDefinition bitsType) {
615 jsonType = processBitsType(bitsType, property);
616 } else if (leafTypeDef instanceof EnumTypeDefinition enumType) {
617 jsonType = processEnumType(enumType, property);
618 } else if (leafTypeDef instanceof IdentityrefTypeDefinition identityrefType) {
619 jsonType = processIdentityRefType(identityrefType, property, definitions,
620 definitionNames, stack.getEffectiveModelContext());
621 } else if (leafTypeDef instanceof StringTypeDefinition stringType) {
622 jsonType = processStringType(stringType, property, node.getQName().getLocalName());
623 } else if (leafTypeDef instanceof UnionTypeDefinition unionType) {
624 jsonType = processUnionType(unionType, property, node.getQName().getLocalName());
625 } else if (leafTypeDef instanceof EmptyTypeDefinition) {
626 jsonType = OBJECT_TYPE;
627 } else if (leafTypeDef instanceof LeafrefTypeDefinition leafrefType) {
628 return processTypeDef(stack.resolveLeafref(leafrefType), node, property,
629 stack, definitions, definitionNames);
630 } else if (leafTypeDef instanceof BooleanTypeDefinition) {
631 jsonType = BOOLEAN_TYPE;
632 leafTypeDef.getDefaultValue().ifPresent(v -> setDefaultValue(property, Boolean.valueOf((String) v)));
633 setExampleValue(property, true);
634 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
635 jsonType = processNumberType(rangeRestrictedType, property);
636 } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
637 jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
639 jsonType = STRING_TYPE;
641 if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
642 if (TYPE_KEY != null && jsonType != null) {
643 property.put(TYPE_KEY, jsonType);
645 if (leafTypeDef.getDefaultValue().isPresent()) {
646 final Object defaultValue = leafTypeDef.getDefaultValue().orElseThrow();
647 if (defaultValue instanceof String stringDefaultValue) {
648 if (leafTypeDef instanceof BooleanTypeDefinition) {
649 setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
650 } else if (leafTypeDef instanceof DecimalTypeDefinition
651 || leafTypeDef instanceof Uint64TypeDefinition) {
652 setDefaultValue(property, new BigDecimal(stringDefaultValue));
653 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition<?, ?> rangeRestrictedType) {
654 //uint8,16,32 int8,16,32,64
655 if (isHexadecimalOrOctal(rangeRestrictedType)) {
656 setDefaultValue(property, stringDefaultValue);
658 setDefaultValue(property, Long.valueOf(stringDefaultValue));
661 setDefaultValue(property, stringDefaultValue);
664 //we should never get here. getDefaultValue always gives us string
665 setDefaultValue(property, defaultValue.toString());
672 private static String processBinaryType(final BinaryTypeDefinition definition, final ObjectNode property) {
673 definition.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
674 property.put(FORMAT_KEY, "byte");
678 private static String processEnumType(final EnumTypeDefinition enumLeafType, final ObjectNode property) {
679 final List<EnumPair> enumPairs = enumLeafType.getValues();
680 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
681 for (final EnumPair enumPair : enumPairs) {
682 enumNames.add(new TextNode(enumPair.getName()));
685 property.set(ENUM_KEY, enumNames);
686 enumLeafType.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
687 setExampleValue(property, enumLeafType.getValues().iterator().next().getName());
691 private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
692 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
693 final EffectiveModelContext schemaContext) {
694 final String definitionName;
695 if (isImported(leafTypeDef)) {
696 definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
698 final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
699 definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
701 property.put(REF_KEY, COMPONENTS_PREFIX + definitionName);
705 private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
706 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
707 final EffectiveModelContext context) {
708 final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
709 final String identityName = idNode.getQName().getLocalName();
710 if (!definitionNames.isListedNode(idNode)) {
711 final Schema identityObj = buildIdentityObject(idNode, context);
712 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
713 final String name = identityName + discriminator;
714 definitions.put(name, identityObj);
717 return identityName + definitionNames.getDiscriminator(idNode);
721 private static Schema buildIdentityObject(final IdentitySchemaNode idNode, final EffectiveModelContext context) {
722 final String identityName = idNode.getQName().getLocalName();
723 LOG.debug("Processing Identity: {}", identityName);
725 final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
726 final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
727 enumPayload.add(identityName);
728 populateEnumWithDerived(derivedIds, enumPayload, context);
730 return new Schema.Builder()
732 .description(idNode.getDescription().orElse(""))
733 .schemaEnum(enumPayload)
738 private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
739 return !leafTypeDef.getQName().getModule().equals(topLevelModule.getQNameModule());
742 private static String processBitsType(final BitsTypeDefinition bitsType, final ObjectNode property) {
743 property.put(MIN_ITEMS, 0);
744 property.put(UNIQUE_ITEMS_KEY, true);
745 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
746 final Collection<? extends Bit> bits = bitsType.getBits();
747 for (final Bit bit : bits) {
748 enumNames.add(new TextNode(bit.getName()));
750 property.set(ENUM_KEY, enumNames);
751 property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
752 bitsType.getDefaultValue().ifPresent(v -> setDefaultValue(property, (String) v));
756 private static String processStringType(final StringTypeDefinition stringType, final ObjectNode property,
757 final String nodeName) {
758 var type = stringType;
759 while (type.getLengthConstraint().isEmpty() && type.getBaseType() != null) {
760 type = type.getBaseType();
763 type.getLengthConstraint().ifPresent(constraint -> {
764 final Range<Integer> range = constraint.getAllowedRanges().span();
765 putIfNonNull(property, MIN_LENGTH_KEY, range.lowerEndpoint());
766 putIfNonNull(property, MAX_LENGTH_KEY, range.upperEndpoint());
769 if (type.getPatternConstraints().iterator().hasNext()) {
770 final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
771 String regex = pattern.getRegularExpressionString();
772 // Escape special characters to prevent issues inside Automaton.
773 regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
774 for (final var charClass : PREDEFINED_CHARACTER_CLASSES.entrySet()) {
775 regex = regex.replaceAll(charClass.getKey(), charClass.getValue());
777 String defaultValue = "";
779 final RegExp regExp = new RegExp(regex);
780 defaultValue = regExp.toAutomaton().getShortestExample(true);
781 } catch (IllegalArgumentException ex) {
782 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
784 setExampleValue(property, defaultValue);
786 setExampleValue(property, "Some " + nodeName);
789 stringType.getDefaultValue().ifPresent(v -> setDefaultValue(property, (String) v));
793 private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
794 final ObjectNode property) {
795 final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
796 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
798 if (isHexadecimalOrOctal(leafTypeDef)) {
802 if (leafTypeDef instanceof DecimalTypeDefinition) {
803 maybeLower.ifPresent(number -> setExampleValue(property, ((Decimal64) number).decimalValue()));
806 if (leafTypeDef instanceof Uint8TypeDefinition
807 || leafTypeDef instanceof Uint16TypeDefinition
808 || leafTypeDef instanceof Int8TypeDefinition
809 || leafTypeDef instanceof Int16TypeDefinition
810 || leafTypeDef instanceof Int32TypeDefinition) {
812 property.put(FORMAT_KEY, INT32_FORMAT);
813 maybeLower.ifPresent(number -> setExampleValue(property, Integer.valueOf(number.toString())));
814 } else if (leafTypeDef instanceof Uint32TypeDefinition
815 || leafTypeDef instanceof Int64TypeDefinition) {
817 property.put(FORMAT_KEY, INT64_FORMAT);
818 maybeLower.ifPresent(number -> setExampleValue(property, Long.valueOf(number.toString())));
821 setExampleValue(property, 0);
826 private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
827 final Optional<?> optDefaultValue = typeDef.getDefaultValue();
828 if (optDefaultValue.isPresent()) {
829 final String defaultValue = (String) optDefaultValue.orElseThrow();
830 return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
835 private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
836 final EffectiveModelContext schemaContext) {
837 // create example instance-identifier to the first container of node's module if exists or leave it empty
838 final var module = schemaContext.findModule(node.getQName().getModule());
839 if (module.isPresent()) {
840 final var container = module.orElseThrow().getChildNodes().stream()
841 .filter(n -> n instanceof ContainerSchemaNode)
843 container.ifPresent(c -> setExampleValue(property, String.format("/%s:%s", module.orElseThrow().getPrefix(),
844 c.getQName().getLocalName())));
850 private static String processUnionType(final UnionTypeDefinition unionType, final ObjectNode property,
851 final String nodeName) {
852 boolean isStringTakePlace = false;
853 boolean isNumberTakePlace = false;
854 boolean isBooleanTakePlace = false;
855 for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
856 if (!isStringTakePlace) {
857 if (typeDef instanceof StringTypeDefinition
858 || typeDef instanceof BitsTypeDefinition
859 || typeDef instanceof BinaryTypeDefinition
860 || typeDef instanceof IdentityrefTypeDefinition
861 || typeDef instanceof EnumTypeDefinition
862 || typeDef instanceof LeafrefTypeDefinition
863 || typeDef instanceof UnionTypeDefinition) {
864 isStringTakePlace = true;
865 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
866 isNumberTakePlace = true;
867 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
868 isBooleanTakePlace = true;
872 if (isStringTakePlace) {
873 unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, (String) v));
874 setExampleValue(property, "Some " + nodeName);
877 if (isBooleanTakePlace) {
878 if (isNumberTakePlace) {
879 // FIXME deal with other number formats
880 unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Long.valueOf((String) v)));
881 setExampleValue(property, 0);
884 unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Boolean.valueOf((String) v)));
885 setExampleValue(property, true);
888 // FIXME deal with other number formats
889 unionType.getDefaultValue().ifPresent(v -> setDefaultValue(property, Long.valueOf((String) v)));
890 setExampleValue(property, 0);
894 private static ObjectNode buildXmlParameter(final SchemaNode node) {
895 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
896 final QName qName = node.getQName();
897 xml.put(NAME_KEY, qName.getLocalName());
898 xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
902 private static void putIfNonNull(final ObjectNode property, final String key, final Number number) {
903 if (key != null && number != null) {
904 if (number instanceof Double doubleNum) {
905 property.put(key, doubleNum);
906 } else if (number instanceof Float floatNum) {
907 property.put(key, floatNum);
908 } else if (number instanceof Integer intNum) {
909 property.put(key, intNum);
910 } else if (number instanceof Short shortNum) {
911 property.put(key, shortNum);
912 } else if (number instanceof Long longNum) {
913 property.put(key, longNum);
918 private static void setExampleValue(final ObjectNode property, final String value) {
919 property.put(EXAMPLE_KEY, value);
922 private static void setExampleValue(final ObjectNode property, final Integer value) {
923 property.put(EXAMPLE_KEY, value);
926 private static void setExampleValue(final ObjectNode property, final Long value) {
927 property.put(EXAMPLE_KEY, value);
930 private static void setExampleValue(final ObjectNode property, final BigDecimal value) {
931 property.put(EXAMPLE_KEY, value);
934 private static void setExampleValue(final ObjectNode property, final Boolean value) {
935 property.put(EXAMPLE_KEY, value);
938 private static void setDefaultValue(final ObjectNode property, final String value) {
939 property.put(DEFAULT_KEY, value);
942 private static void setDefaultValue(final ObjectNode property, final Long value) {
943 property.put(DEFAULT_KEY, value);
946 private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
947 property.put(DEFAULT_KEY, value);
950 private static void setDefaultValue(final ObjectNode property, final Boolean value) {
951 property.put(DEFAULT_KEY, value);