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.model.api.ActionDefinition;
36 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
37 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
39 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
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.IdentitySchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
49 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.MandatoryAware;
52 import org.opendaylight.yangtools.yang.model.api.Module;
53 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
54 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
55 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
57 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
58 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
59 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition.Bit;
60 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
61 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
62 import org.opendaylight.yangtools.yang.model.api.type.EmptyTypeDefinition;
63 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
64 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition.EnumPair;
65 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
66 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
67 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
68 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
69 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
70 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
71 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
72 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
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 Schema.Builder definitionBuilder = new Schema.Builder();
172 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
173 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
174 final String moduleName = module.getName();
175 final String definitionName = moduleName + MODULE_NAME_SUFFIX;
176 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
177 for (final DataSchemaNode node : module.getChildNodes()) {
178 stack.enterSchemaTree(node.getQName());
179 final String localName = node.getQName().getLocalName();
180 if (node.isConfiguration()) {
181 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
182 for (final DataSchemaNode childNode : ((DataNodeContainer) node).getChildNodes()) {
183 final ObjectNode childNodeProperties = JsonNodeFactory.instance.objectNode();
185 final String ref = COMPONENTS_PREFIX
186 + moduleName + CONFIG
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 + CONFIG);
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 processLeafNode((LeafSchemaNode) node, localName, properties, required, stack,
213 definitions, definitionNames);
218 definitionBuilder.title(definitionName);
219 definitionBuilder.type(OBJECT_TYPE);
220 definitionBuilder.properties(properties);
221 definitionBuilder.description(module.getDescription().orElse(""));
222 setRequiredIfNotEmpty(definitionBuilder, required);
224 definitions.put(definitionName, definitionBuilder.build());
227 private void processContainersAndLists(final Module module, final Map<String, Schema> definitions,
228 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
229 final String moduleName = module.getName();
230 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
231 for (final DataSchemaNode childNode : module.getChildNodes()) {
232 stack.enterSchemaTree(childNode.getQName());
233 // For every container and list in the module
234 if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
235 if (childNode.isConfiguration()) {
236 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
239 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
241 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack);
247 private void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
248 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
249 final SchemaInferenceStack stack) throws IOException {
250 for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
251 stack.enterSchemaTree(actionDef.getQName());
252 processOperations(actionDef, moduleName, definitions, definitionNames, stack);
257 private void processRPCs(final Module module, final Map<String, Schema> definitions,
258 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext) throws IOException {
259 final String moduleName = module.getName();
260 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
261 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
262 stack.enterSchemaTree(rpcDefinition.getQName());
263 processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack);
268 private void processOperations(final OperationDefinition operationDef, final String parentName,
269 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
270 final SchemaInferenceStack stack) throws IOException {
271 final String operationName = operationDef.getQName().getLocalName();
272 processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
273 definitionNames, stack);
274 processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
275 definitionNames, stack);
278 private void processOperationInputOutput(final ContainerLike container, final String operationName,
279 final String parentName, final boolean isInput, final Map<String, Schema> definitions,
280 final DefinitionNames definitionNames, final SchemaInferenceStack stack)
282 stack.enterSchemaTree(container.getQName());
283 if (!container.getChildNodes().isEmpty()) {
284 final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
285 final Schema.Builder childSchemaBuilder = new Schema.Builder();
286 processChildren(childSchemaBuilder, container.getChildNodes(), parentName, definitions, definitionNames,
289 childSchemaBuilder.type(OBJECT_TYPE);
290 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
291 xml.put(NAME_KEY, isInput ? INPUT : OUTPUT);
292 childSchemaBuilder.xml(xml);
293 childSchemaBuilder.title(filename);
294 final String discriminator =
295 definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
296 definitions.put(filename + discriminator, childSchemaBuilder.build());
298 processTopData(filename, discriminator, definitions, container);
303 private static ObjectNode processTopData(final String filename, final String discriminator,
304 final Map<String, Schema> definitions, final SchemaNode schemaNode) {
305 final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
306 final String name = filename + discriminator;
307 final String ref = COMPONENTS_PREFIX + name;
308 final String topName = filename + TOP;
310 if (schemaNode instanceof ListSchemaNode) {
311 dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
312 final ObjectNode items = JsonNodeFactory.instance.objectNode();
313 items.put(REF_KEY, ref);
314 dataNodeProperties.set(ITEMS_KEY, items);
315 dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
318 Description can't be added, because nothing allowed alongside $ref.
319 allOf is not an option, because ServiceNow can't parse it.
321 dataNodeProperties.put(REF_KEY, ref);
324 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
326 Add module name prefix to property name, when needed, when ServiceNow can process colons,
327 use RestDocGenUtil#resolveNodesName for creating property name
329 properties.set(schemaNode.getQName().getLocalName(), dataNodeProperties);
330 final Schema.Builder finalChildSchemaBuilder = new Schema.Builder();
331 finalChildSchemaBuilder.type(OBJECT_TYPE);
332 finalChildSchemaBuilder.properties(properties);
333 finalChildSchemaBuilder.title(topName);
335 definitions.put(topName + discriminator, finalChildSchemaBuilder.build());
337 return dataNodeProperties;
341 * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
342 * @param module The module from which the identity stmt will be processed
343 * @param definitions The ObjectNode in which the parsed identity will be put as a 'model' obj
344 * @param definitionNames Store for definition names
346 private static void processIdentities(final Module module, final Map<String, Schema> definitions,
347 final DefinitionNames definitionNames, final EffectiveModelContext context) {
348 final String moduleName = module.getName();
349 final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
350 LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
352 for (final IdentitySchemaNode idNode : idNodes) {
353 final Schema identityObj = buildIdentityObject(idNode, context);
354 final String idName = idNode.getQName().getLocalName();
355 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
356 final String name = idName + discriminator;
357 definitions.put(name, identityObj);
361 private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
362 final ArrayNode enumPayload, final EffectiveModelContext context) {
363 for (final IdentitySchemaNode derivedId : derivedIds) {
364 enumPayload.add(derivedId.getQName().getLocalName());
365 populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
369 private ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
370 final Map<String, Schema> definitions, final DefinitionNames definitionNames, final boolean isConfig,
371 final SchemaInferenceStack stack) throws IOException {
372 if (dataNode instanceof ListSchemaNode || dataNode instanceof ContainerSchemaNode) {
373 final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
374 final SchemaNode schemaNode = (SchemaNode) dataNode;
375 final String localName = schemaNode.getQName().getLocalName();
376 final Schema.Builder childSchemaBuilder = new Schema.Builder();
377 final String nameAsParent = parentName + "_" + localName;
378 final ObjectNode properties =
379 processChildren(childSchemaBuilder, containerChildren, parentName + "_" + localName, definitions,
380 definitionNames, isConfig, stack);
382 final String nodeName = parentName + (isConfig ? CONFIG : "") + "_" + localName;
383 final String parentNameConfigLocalName = parentName + CONFIG + "_" + localName;
385 final String description = schemaNode.getDescription().orElse("");
386 final String discriminator;
388 if (!definitionNames.isListedNode(schemaNode)) {
389 final List<String> names = List.of(parentNameConfigLocalName,
390 parentNameConfigLocalName + TOP,
393 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
395 discriminator = definitionNames.getDiscriminator(schemaNode);
398 childSchemaBuilder.type(OBJECT_TYPE);
399 childSchemaBuilder.properties(properties);
400 childSchemaBuilder.title(nodeName);
401 childSchemaBuilder.description(description);
403 final String defName = nodeName + discriminator;
404 childSchemaBuilder.xml(buildXmlParameter(schemaNode));
405 definitions.put(defName, childSchemaBuilder.build());
407 return processTopData(nodeName, discriminator, definitions, schemaNode);
413 * Processes the nodes.
415 private ObjectNode processChildren(final Schema.Builder parentNodeBuilder,
416 final Collection<? extends DataSchemaNode> nodes, final String parentName,
417 final Map<String, Schema> definitions, final DefinitionNames definitionNames, final boolean isConfig,
418 final SchemaInferenceStack stack) throws IOException {
419 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
420 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
421 for (final DataSchemaNode node : nodes) {
422 if (!isConfig || node.isConfiguration()) {
423 processChildNode(node, parentName, definitions, definitionNames, isConfig, stack, properties);
426 parentNodeBuilder.properties(properties);
427 setRequiredIfNotEmpty(parentNodeBuilder, required);
431 private void processChildNode(final DataSchemaNode node, final String parentName,
432 final Map<String, Schema> definitions, final DefinitionNames definitionNames, final boolean isConfig,
433 final SchemaInferenceStack stack, final ObjectNode properties) throws IOException {
435 stack.enterSchemaTree(node.getQName());
438 Add module name prefix to property name, when needed, when ServiceNow can process colons,
439 use RestDocGenUtil#resolveNodesName for creating property name
441 final String name = node.getQName().getLocalName();
443 if (node instanceof LeafSchemaNode leaf) {
444 processLeafNode(leaf, name, properties, JsonNodeFactory.instance.arrayNode(), stack, definitions,
447 } else if (node instanceof AnyxmlSchemaNode anyxml) {
448 processAnyXMLNode(anyxml, name, properties, JsonNodeFactory.instance.arrayNode());
450 } else if (node instanceof AnydataSchemaNode anydata) {
451 processAnydataNode(anydata, name, properties, JsonNodeFactory.instance.arrayNode());
455 final ObjectNode property;
456 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
457 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions,
458 definitionNames, isConfig, stack);
460 processActionNodeContainer(node, parentName, definitions, definitionNames, stack);
462 } else if (node instanceof LeafListSchemaNode leafList) {
463 property = processLeafListNode(leafList, stack, definitions, definitionNames);
465 } else if (node instanceof ChoiceSchemaNode choice) {
466 if (!choice.getCases().isEmpty()) {
467 CaseSchemaNode caseSchemaNode = choice.getDefaultCase()
468 .orElse(choice.getCases().stream()
469 .findFirst().orElseThrow());
470 stack.enterSchemaTree(caseSchemaNode.getQName());
471 for (final DataSchemaNode childNode : caseSchemaNode.getChildNodes()) {
472 processChildNode(childNode, parentName, definitions, definitionNames, isConfig, stack,
480 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
482 if (property != null) {
483 properties.set(name, property);
490 private ObjectNode processLeafListNode(final LeafListSchemaNode listNode, final SchemaInferenceStack stack,
491 final Map<String, Schema> definitions, final DefinitionNames definitionNames) {
492 final ObjectNode props = JsonNodeFactory.instance.objectNode();
493 props.put(TYPE_KEY, ARRAY_TYPE);
495 final ObjectNode itemsVal = JsonNodeFactory.instance.objectNode();
496 final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
497 processElementCount(optConstraint, props);
499 processTypeDef(listNode.getType(), listNode, itemsVal, stack, definitions, definitionNames);
500 props.set(ITEMS_KEY, itemsVal);
502 props.put(DESCRIPTION_KEY, listNode.getDescription().orElse(""));
507 private static void processElementCount(final Optional<ElementCountConstraint> constraint, final ObjectNode props) {
508 if (constraint.isPresent()) {
509 final ElementCountConstraint constr = constraint.orElseThrow();
510 final Integer minElements = constr.getMinElements();
511 if (minElements != null) {
512 props.put(MIN_ITEMS, minElements);
514 final Integer maxElements = constr.getMaxElements();
515 if (maxElements != null) {
516 props.put(MAX_ITEMS, maxElements);
521 private static void processMandatory(final MandatoryAware node, final String nodeName, final ArrayNode required) {
522 if (node.isMandatory()) {
523 required.add(nodeName);
527 private ObjectNode processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
528 final ObjectNode properties, final ArrayNode required, final SchemaInferenceStack stack,
529 final Map<String, Schema> definitions, final DefinitionNames definitionNames) {
530 final ObjectNode property = JsonNodeFactory.instance.objectNode();
532 final String leafDescription = leafNode.getDescription().orElse("");
534 Description can't be added, because nothing allowed alongside $ref.
535 allOf is not an option, because ServiceNow can't parse it.
537 if (!(leafNode.getType() instanceof IdentityrefTypeDefinition)) {
538 property.put(DESCRIPTION_KEY, leafDescription);
541 processTypeDef(leafNode.getType(), leafNode, property, stack, definitions, definitionNames);
542 properties.set(jsonLeafName, property);
543 property.set(XML_KEY, buildXmlParameter(leafNode));
544 processMandatory(leafNode, jsonLeafName, required);
549 private static ObjectNode processAnydataNode(final AnydataSchemaNode leafNode, final String name,
550 final ObjectNode properties, final ArrayNode required) {
551 final ObjectNode property = JsonNodeFactory.instance.objectNode();
553 final String leafDescription = leafNode.getDescription().orElse("");
554 property.put(DESCRIPTION_KEY, leafDescription);
556 final String localName = leafNode.getQName().getLocalName();
557 setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
558 property.put(TYPE_KEY, STRING_TYPE);
559 property.set(XML_KEY, buildXmlParameter(leafNode));
560 processMandatory(leafNode, name, required);
561 properties.set(name, property);
566 private static ObjectNode processAnyXMLNode(final AnyxmlSchemaNode leafNode, final String name,
567 final ObjectNode properties, final ArrayNode required) {
568 final ObjectNode property = JsonNodeFactory.instance.objectNode();
570 final String leafDescription = leafNode.getDescription().orElse("");
571 property.put(DESCRIPTION_KEY, leafDescription);
573 final String localName = leafNode.getQName().getLocalName();
574 setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
575 property.put(TYPE_KEY, STRING_TYPE);
576 property.set(XML_KEY, buildXmlParameter(leafNode));
577 processMandatory(leafNode, name, required);
578 properties.set(name, property);
583 private String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
584 final ObjectNode property, final SchemaInferenceStack stack, final Map<String, Schema> definitions,
585 final DefinitionNames definitionNames) {
586 final String jsonType;
587 if (leafTypeDef instanceof BinaryTypeDefinition) {
588 jsonType = processBinaryType(property);
590 } else if (leafTypeDef instanceof BitsTypeDefinition) {
591 jsonType = processBitsType((BitsTypeDefinition) leafTypeDef, property);
593 } else if (leafTypeDef instanceof EnumTypeDefinition) {
594 jsonType = processEnumType((EnumTypeDefinition) leafTypeDef, property);
596 } else if (leafTypeDef instanceof IdentityrefTypeDefinition) {
597 jsonType = processIdentityRefType((IdentityrefTypeDefinition) leafTypeDef, property, definitions,
598 definitionNames, stack.getEffectiveModelContext());
600 } else if (leafTypeDef instanceof StringTypeDefinition) {
601 jsonType = processStringType(leafTypeDef, property, node.getQName().getLocalName());
603 } else if (leafTypeDef instanceof UnionTypeDefinition) {
604 jsonType = processUnionType((UnionTypeDefinition) leafTypeDef);
606 } else if (leafTypeDef instanceof EmptyTypeDefinition) {
607 jsonType = OBJECT_TYPE;
608 } else if (leafTypeDef instanceof LeafrefTypeDefinition) {
609 return processTypeDef(stack.resolveLeafref((LeafrefTypeDefinition) leafTypeDef), node, property,
610 stack, definitions, definitionNames);
611 } else if (leafTypeDef instanceof BooleanTypeDefinition) {
612 jsonType = BOOLEAN_TYPE;
613 setDefaultValue(property, true);
614 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
615 jsonType = processNumberType((RangeRestrictedTypeDefinition<?, ?>) leafTypeDef, property);
616 } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
617 jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
619 jsonType = STRING_TYPE;
621 if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
622 putIfNonNull(property, TYPE_KEY, jsonType);
623 if (leafTypeDef.getDefaultValue().isPresent()) {
624 final Object defaultValue = leafTypeDef.getDefaultValue().orElseThrow();
625 if (defaultValue instanceof String stringDefaultValue) {
626 if (leafTypeDef instanceof BooleanTypeDefinition) {
627 setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
628 } else if (leafTypeDef instanceof DecimalTypeDefinition
629 || leafTypeDef instanceof Uint64TypeDefinition) {
630 setDefaultValue(property, new BigDecimal(stringDefaultValue));
631 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
632 //uint8,16,32 int8,16,32,64
633 if (isHexadecimalOrOctal((RangeRestrictedTypeDefinition<?, ?>)leafTypeDef)) {
634 setDefaultValue(property, stringDefaultValue);
636 setDefaultValue(property, Long.valueOf(stringDefaultValue));
639 setDefaultValue(property, stringDefaultValue);
642 //we should never get here. getDefaultValue always gives us string
643 setDefaultValue(property, defaultValue.toString());
650 private static String processBinaryType(final ObjectNode property) {
651 property.put(FORMAT_KEY, "byte");
655 private static String processEnumType(final EnumTypeDefinition enumLeafType, final ObjectNode property) {
656 final List<EnumPair> enumPairs = enumLeafType.getValues();
657 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
658 for (final EnumPair enumPair : enumPairs) {
659 enumNames.add(new TextNode(enumPair.getName()));
662 property.set(ENUM_KEY, enumNames);
663 enumLeafType.getDefaultValue().ifPresent(v -> setDefaultValue(property, ((String) v)));
664 setExampleValue(property, enumLeafType.getValues().iterator().next().getName());
668 private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
669 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
670 final EffectiveModelContext schemaContext) {
671 final String definitionName;
672 if (isImported(leafTypeDef)) {
673 definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
675 final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
676 definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
678 property.put(REF_KEY, COMPONENTS_PREFIX + definitionName);
682 private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
683 final Map<String, Schema> definitions, final DefinitionNames definitionNames,
684 final EffectiveModelContext context) {
685 final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
686 final String identityName = idNode.getQName().getLocalName();
687 if (!definitionNames.isListedNode(idNode)) {
688 final Schema identityObj = buildIdentityObject(idNode, context);
689 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
690 final String name = identityName + discriminator;
691 definitions.put(name, identityObj);
694 return identityName + definitionNames.getDiscriminator(idNode);
698 private static Schema buildIdentityObject(final IdentitySchemaNode idNode,
699 final EffectiveModelContext context) {
700 final Schema.Builder schemaBuilder = new Schema.Builder();
701 final String identityName = idNode.getQName().getLocalName();
702 LOG.debug("Processing Identity: {}", identityName);
704 schemaBuilder.title(identityName);
705 schemaBuilder.description(idNode.getDescription().orElse(""));
707 final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
709 final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
710 enumPayload.add(identityName);
711 populateEnumWithDerived(derivedIds, enumPayload, context);
712 schemaBuilder.schemaEnum(enumPayload);
713 schemaBuilder.type(STRING_TYPE);
714 return schemaBuilder.build();
717 private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
718 return !leafTypeDef.getQName().getModule().equals(topLevelModule.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));
734 private static String processStringType(final TypeDefinition<?> stringType, final ObjectNode property,
735 final String nodeName) {
736 StringTypeDefinition type = (StringTypeDefinition) stringType;
737 Optional<LengthConstraint> lengthConstraints = ((StringTypeDefinition) stringType).getLengthConstraint();
738 while (lengthConstraints.isEmpty() && type.getBaseType() != null) {
739 type = type.getBaseType();
740 lengthConstraints = type.getLengthConstraint();
743 if (lengthConstraints.isPresent()) {
744 final Range<Integer> range = lengthConstraints.orElseThrow().getAllowedRanges().span();
745 putIfNonNull(property, MIN_LENGTH_KEY, range.lowerEndpoint());
746 putIfNonNull(property, MAX_LENGTH_KEY, range.upperEndpoint());
749 if (type.getPatternConstraints().iterator().hasNext()) {
750 final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
751 String regex = pattern.getRegularExpressionString();
752 // Escape special characters to prevent issues inside Automaton.
753 regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
754 for (final var charClass : PREDEFINED_CHARACTER_CLASSES.entrySet()) {
755 regex = regex.replaceAll(charClass.getKey(), charClass.getValue());
757 String defaultValue = "";
759 final RegExp regExp = new RegExp(regex);
760 defaultValue = regExp.toAutomaton().getShortestExample(true);
761 } catch (IllegalArgumentException ex) {
762 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
764 setDefaultValue(property, defaultValue);
766 setDefaultValue(property, "Some " + nodeName);
771 private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
772 final ObjectNode property) {
773 final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
774 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
776 if (isHexadecimalOrOctal(leafTypeDef)) {
780 if (leafTypeDef instanceof DecimalTypeDefinition) {
781 maybeLower.ifPresent(number -> setDefaultValue(property, ((Decimal64) number).decimalValue()));
784 if (leafTypeDef instanceof Uint8TypeDefinition
785 || leafTypeDef instanceof Uint16TypeDefinition
786 || leafTypeDef instanceof Int8TypeDefinition
787 || leafTypeDef instanceof Int16TypeDefinition
788 || leafTypeDef instanceof Int32TypeDefinition) {
790 property.put(FORMAT_KEY, INT32_FORMAT);
791 maybeLower.ifPresent(number -> setDefaultValue(property, Integer.valueOf(number.toString())));
792 } else if (leafTypeDef instanceof Uint32TypeDefinition
793 || leafTypeDef instanceof Int64TypeDefinition) {
795 property.put(FORMAT_KEY, INT64_FORMAT);
796 maybeLower.ifPresent(number -> setDefaultValue(property, Long.valueOf(number.toString())));
799 setDefaultValue(property, 0);
804 private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
805 final Optional<?> optDefaultValue = typeDef.getDefaultValue();
806 if (optDefaultValue.isPresent()) {
807 final String defaultValue = (String) optDefaultValue.orElseThrow();
808 return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
813 private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
814 final EffectiveModelContext schemaContext) {
815 // create example instance-identifier to the first container of node's module if exists or leave it empty
816 final var module = schemaContext.findModule(node.getQName().getModule());
817 if (module.isPresent()) {
818 final var container = module.orElseThrow().getChildNodes().stream()
819 .filter(n -> n instanceof ContainerSchemaNode)
821 container.ifPresent(c -> setDefaultValue(property, String.format("/%s:%s", module.orElseThrow().getPrefix(),
822 c.getQName().getLocalName())));
828 private static String processUnionType(final UnionTypeDefinition unionType) {
829 boolean isStringTakePlace = false;
830 boolean isNumberTakePlace = false;
831 boolean isBooleanTakePlace = false;
832 for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
833 if (!isStringTakePlace) {
834 if (typeDef instanceof StringTypeDefinition
835 || typeDef instanceof BitsTypeDefinition
836 || typeDef instanceof BinaryTypeDefinition
837 || typeDef instanceof IdentityrefTypeDefinition
838 || typeDef instanceof EnumTypeDefinition
839 || typeDef instanceof LeafrefTypeDefinition
840 || typeDef instanceof UnionTypeDefinition) {
841 isStringTakePlace = true;
842 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
843 isNumberTakePlace = true;
844 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
845 isBooleanTakePlace = true;
849 if (isStringTakePlace) {
852 if (isBooleanTakePlace) {
853 if (isNumberTakePlace) {
861 private static ObjectNode buildXmlParameter(final SchemaNode node) {
862 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
863 final QName qName = node.getQName();
864 xml.put(NAME_KEY, qName.getLocalName());
865 xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
869 private static void putIfNonNull(final ObjectNode property, final String key, final Number number) {
870 if (key != null && number != null) {
871 if (number instanceof Double) {
872 property.put(key, (Double) number);
873 } else if (number instanceof Float) {
874 property.put(key, (Float) number);
875 } else if (number instanceof Integer) {
876 property.put(key, (Integer) number);
877 } else if (number instanceof Short) {
878 property.put(key, (Short) number);
879 } else if (number instanceof Long) {
880 property.put(key, (Long) number);
885 private static void putIfNonNull(final ObjectNode property, final String key, final String value) {
886 if (key != null && value != null) {
887 property.put(key, value);
891 private static void setRequiredIfNotEmpty(final Schema.Builder nodeBuilder, final ArrayNode required) {
892 if (required.size() > 0) {
893 nodeBuilder.required(required);
897 private static void setExampleValue(final ObjectNode property, final String value) {
898 property.put(EXAMPLE_KEY, value);
901 private static void setDefaultValue(final ObjectNode property, final String value) {
902 property.put(DEFAULT_KEY, value);
905 private static void setDefaultValue(final ObjectNode property, final Integer value) {
906 property.put(DEFAULT_KEY, value);
909 private static void setDefaultValue(final ObjectNode property, final Long value) {
910 property.put(DEFAULT_KEY, value);
913 private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
914 property.put(DEFAULT_KEY, value);
917 private static void setDefaultValue(final ObjectNode property, final Boolean value) {
918 property.put(DEFAULT_KEY, value);