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.netconf.sal.rest.doc.impl;
10 import static org.opendaylight.netconf.sal.rest.doc.impl.BaseYangSwaggerGenerator.MODULE_NAME_SUFFIX;
11 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.CONFIG;
12 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.NAME_KEY;
13 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.POST_SUFFIX;
14 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.TOP;
15 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.XML_KEY;
16 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.XML_SUFFIX;
17 import static org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.getAppropriateModelPrefix;
19 import com.fasterxml.jackson.databind.JsonNode;
20 import com.fasterxml.jackson.databind.node.ArrayNode;
21 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
22 import com.fasterxml.jackson.databind.node.ObjectNode;
23 import com.fasterxml.jackson.databind.node.TextNode;
24 import com.google.common.collect.Range;
25 import com.google.common.collect.RangeSet;
26 import com.mifmif.common.regex.Generex;
27 import java.io.IOException;
28 import java.math.BigDecimal;
29 import java.util.Collection;
30 import java.util.Iterator;
31 import java.util.List;
33 import java.util.Optional;
35 import java.util.regex.Pattern;
36 import java.util.stream.Collectors;
37 import org.opendaylight.netconf.sal.rest.doc.impl.ApiDocServiceImpl.OAversion;
38 import org.opendaylight.yangtools.yang.common.Decimal64;
39 import org.opendaylight.yangtools.yang.common.QName;
40 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
41 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
42 import org.opendaylight.yangtools.yang.model.api.AnydataSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode;
44 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
45 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
47 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
48 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
49 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
51 import org.opendaylight.yangtools.yang.model.api.ElementCountConstraint;
52 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
54 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
55 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.MandatoryAware;
57 import org.opendaylight.yangtools.yang.model.api.Module;
58 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
59 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
60 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
61 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
62 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
63 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
64 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition.Bit;
65 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
66 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
67 import org.opendaylight.yangtools.yang.model.api.type.EmptyTypeDefinition;
68 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
69 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition.EnumPair;
70 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
71 import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
72 import org.opendaylight.yangtools.yang.model.api.type.Int16TypeDefinition;
73 import org.opendaylight.yangtools.yang.model.api.type.Int32TypeDefinition;
74 import org.opendaylight.yangtools.yang.model.api.type.Int64TypeDefinition;
75 import org.opendaylight.yangtools.yang.model.api.type.Int8TypeDefinition;
76 import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
77 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
78 import org.opendaylight.yangtools.yang.model.api.type.PatternConstraint;
79 import org.opendaylight.yangtools.yang.model.api.type.RangeConstraint;
80 import org.opendaylight.yangtools.yang.model.api.type.RangeRestrictedTypeDefinition;
81 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
82 import org.opendaylight.yangtools.yang.model.api.type.Uint16TypeDefinition;
83 import org.opendaylight.yangtools.yang.model.api.type.Uint32TypeDefinition;
84 import org.opendaylight.yangtools.yang.model.api.type.Uint64TypeDefinition;
85 import org.opendaylight.yangtools.yang.model.api.type.Uint8TypeDefinition;
86 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
87 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
88 import org.slf4j.Logger;
89 import org.slf4j.LoggerFactory;
92 * Generates JSON Schema for data defined in YANG. This class is not thread-safe.
94 public class DefinitionGenerator {
96 private static final Logger LOG = LoggerFactory.getLogger(DefinitionGenerator.class);
98 private static final String UNIQUE_ITEMS_KEY = "uniqueItems";
99 private static final String MAX_ITEMS = "maxItems";
100 private static final String MIN_ITEMS = "minItems";
101 private static final String MAX_LENGTH_KEY = "maxLength";
102 private static final String MIN_LENGTH_KEY = "minLength";
103 private static final String REQUIRED_KEY = "required";
104 private static final String REF_KEY = "$ref";
105 private static final String ITEMS_KEY = "items";
106 private static final String TYPE_KEY = "type";
107 private static final String PROPERTIES_KEY = "properties";
108 private static final String DESCRIPTION_KEY = "description";
109 private static final String ARRAY_TYPE = "array";
110 private static final String ENUM_KEY = "enum";
111 private static final String TITLE_KEY = "title";
112 private static final String DEFAULT_KEY = "default";
113 private static final String FORMAT_KEY = "format";
114 private static final String NAMESPACE_KEY = "namespace";
115 public static final String INPUT = "input";
116 public static final String INPUT_SUFFIX = "_input";
117 public static final String OUTPUT = "output";
118 public static final String OUTPUT_SUFFIX = "_output";
119 private static final String STRING_TYPE = "string";
120 private static final String OBJECT_TYPE = "object";
121 private static final String NUMBER_TYPE = "number";
122 private static final String INTEGER_TYPE = "integer";
123 private static final String INT32_FORMAT = "int32";
124 private static final String INT64_FORMAT = "int64";
125 private static final String BOOLEAN_TYPE = "boolean";
126 // Special characters used in automaton inside Generex.
127 // See https://www.brics.dk/automaton/doc/dk/brics/automaton/RegExp.html
128 private static final Pattern AUTOMATON_SPECIAL_CHARACTERS = Pattern.compile("[@&\"<>#~]");
130 private Module topLevelModule;
132 public DefinitionGenerator() {
136 * Creates Json definitions from provided module according to swagger spec.
138 * @param module - Yang module to be converted
139 * @param schemaContext - SchemaContext of all Yang files used by Api Doc
140 * @param definitionNames - Store for definition names
141 * @return ObjectNode containing data used for creating examples and definitions in Api Doc
142 * @throws IOException if I/O operation fails
146 public ObjectNode convertToJsonSchema(final Module module, final EffectiveModelContext schemaContext,
147 final ObjectNode definitions, final DefinitionNames definitionNames,
148 final OAversion oaversion, final boolean isForSingleModule)
150 topLevelModule = module;
152 processIdentities(module, definitions, definitionNames, schemaContext);
153 processContainersAndLists(module, definitions, definitionNames, schemaContext, oaversion);
154 processRPCs(module, definitions, definitionNames, schemaContext, oaversion);
156 if (isForSingleModule) {
157 processModule(module, definitions, definitionNames, schemaContext, oaversion);
163 public ObjectNode convertToJsonSchema(final Module module, final EffectiveModelContext schemaContext,
164 final DefinitionNames definitionNames, final OAversion oaversion,
165 final boolean isForSingleModule)
167 final ObjectNode definitions = JsonNodeFactory.instance.objectNode();
168 if (isForSingleModule) {
169 definitionNames.addUnlinkedName(module.getName() + MODULE_NAME_SUFFIX);
171 return convertToJsonSchema(module, schemaContext, definitions, definitionNames, oaversion, isForSingleModule);
174 private void processModule(final Module module, final ObjectNode definitions, final DefinitionNames definitionNames,
175 final EffectiveModelContext schemaContext, final OAversion oaversion) {
176 final ObjectNode definition = JsonNodeFactory.instance.objectNode();
177 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
178 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
179 final String moduleName = module.getName();
180 final String definitionName = moduleName + MODULE_NAME_SUFFIX;
181 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
182 for (final DataSchemaNode node : module.getChildNodes()) {
183 stack.enterSchemaTree(node.getQName());
184 final String localName = node.getQName().getLocalName();
185 if (node.isConfiguration()) {
186 if (node instanceof ContainerSchemaNode || node instanceof ListSchemaNode) {
187 for (final DataSchemaNode childNode : ((DataNodeContainer) node).getChildNodes()) {
188 final ObjectNode childNodeProperties = JsonNodeFactory.instance.objectNode();
190 final String ref = getAppropriateModelPrefix(oaversion)
191 + moduleName + CONFIG
193 + definitionNames.getDiscriminator(node);
195 if (node instanceof ListSchemaNode) {
196 childNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
197 final ObjectNode items = JsonNodeFactory.instance.objectNode();
198 items.put(REF_KEY, ref);
199 childNodeProperties.set(ITEMS_KEY, items);
200 childNodeProperties.put(DESCRIPTION_KEY, childNode.getDescription().orElse(""));
201 childNodeProperties.put(TITLE_KEY, localName + CONFIG);
204 Description can't be added, because nothing allowed alongside $ref.
205 allOf is not an option, because ServiceNow can't parse it.
207 childNodeProperties.put(REF_KEY, ref);
209 //add module name prefix to property name, when ServiceNow can process colons
210 properties.set(localName, childNodeProperties);
212 } else if (node instanceof LeafSchemaNode) {
214 Add module name prefix to property name, when ServiceNow can process colons(second parameter
217 processLeafNode((LeafSchemaNode) node, localName, properties, required, stack,
218 definitions, definitionNames, oaversion);
223 definition.put(TITLE_KEY, definitionName);
224 definition.put(TYPE_KEY, OBJECT_TYPE);
225 definition.set(PROPERTIES_KEY, properties);
226 definition.put(DESCRIPTION_KEY, module.getDescription().orElse(""));
227 setRequiredIfNotEmpty(definition, required);
229 definitions.set(definitionName, definition);
232 private void processContainersAndLists(final Module module, final ObjectNode definitions,
233 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext, final OAversion oaversion)
235 final String moduleName = module.getName();
236 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
237 for (final DataSchemaNode childNode : module.getChildNodes()) {
238 stack.enterSchemaTree(childNode.getQName());
239 // For every container and list in the module
240 if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
241 if (childNode.isConfiguration()) {
242 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
243 true, stack, oaversion);
245 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
246 false, stack, oaversion);
247 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack, oaversion);
253 private void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
254 final ObjectNode definitions, final DefinitionNames definitionNames,
255 final SchemaInferenceStack stack, final OAversion oaversion)
257 for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
258 stack.enterSchemaTree(actionDef.getQName());
259 processOperations(actionDef, moduleName, definitions, definitionNames, stack, oaversion);
264 private void processRPCs(final Module module, final ObjectNode definitions, final DefinitionNames definitionNames,
265 final EffectiveModelContext schemaContext, final OAversion oaversion) throws IOException {
266 final String moduleName = module.getName();
267 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
268 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
269 stack.enterSchemaTree(rpcDefinition.getQName());
270 processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack, oaversion);
275 private void processOperations(final OperationDefinition operationDef, final String parentName,
276 final ObjectNode definitions, final DefinitionNames definitionNames,
277 final SchemaInferenceStack stack, final OAversion oaversion)
279 final String operationName = operationDef.getQName().getLocalName();
280 processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
281 definitionNames, stack, oaversion);
282 processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
283 definitionNames, stack, oaversion);
286 private void processOperationInputOutput(final ContainerLike container, final String operationName,
287 final String parentName, final boolean isInput,
288 final ObjectNode definitions, final DefinitionNames definitionNames,
289 final SchemaInferenceStack stack, final OAversion oaversion)
291 stack.enterSchemaTree(container.getQName());
292 if (!container.getChildNodes().isEmpty()) {
293 final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
294 final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
295 processChildren(childSchema, container.getChildNodes(), parentName, definitions, definitionNames,
296 false, stack, oaversion);
298 childSchema.put(TYPE_KEY, OBJECT_TYPE);
299 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
300 xml.put(NAME_KEY, isInput ? INPUT : OUTPUT);
301 childSchema.set(XML_KEY, xml);
302 childSchema.put(TITLE_KEY, filename);
303 final String discriminator =
304 definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
305 definitions.set(filename + discriminator, childSchema);
307 processTopData(filename, discriminator, definitions, container, oaversion);
312 private static ObjectNode processTopData(final String filename, final String discriminator,
313 final ObjectNode definitions, final SchemaNode schemaNode, final OAversion oaversion) {
314 final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
315 final String name = filename + discriminator;
316 final String ref = getAppropriateModelPrefix(oaversion) + name;
317 final String topName = filename + TOP;
319 if (schemaNode instanceof ListSchemaNode) {
320 dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
321 final ObjectNode items = JsonNodeFactory.instance.objectNode();
322 items.put(REF_KEY, ref);
323 dataNodeProperties.set(ITEMS_KEY, items);
324 dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
327 Description can't be added, because nothing allowed alongside $ref.
328 allOf is not an option, because ServiceNow can't parse it.
330 dataNodeProperties.put(REF_KEY, ref);
333 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
335 Add module name prefix to property name, when needed, when ServiceNow can process colons,
336 use RestDocGenUtil#resolveNodesName for creating property name
338 properties.set(schemaNode.getQName().getLocalName(), dataNodeProperties);
339 final ObjectNode finalChildSchema = JsonNodeFactory.instance.objectNode();
340 finalChildSchema.put(TYPE_KEY, OBJECT_TYPE);
341 finalChildSchema.set(PROPERTIES_KEY, properties);
342 finalChildSchema.put(TITLE_KEY, topName);
345 definitions.set(topName + discriminator, finalChildSchema);
347 return dataNodeProperties;
351 * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
352 * @param module The module from which the identity stmt will be processed
353 * @param definitions The ObjectNode in which the parsed identity will be put as a 'model' obj
354 * @param definitionNames Store for definition names
356 private static void processIdentities(final Module module, final ObjectNode definitions,
357 final DefinitionNames definitionNames, final EffectiveModelContext context) {
358 final String moduleName = module.getName();
359 final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
360 LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
362 for (final IdentitySchemaNode idNode : idNodes) {
363 final ObjectNode identityObj = buildIdentityObject(idNode, context);
364 final String idName = idNode.getQName().getLocalName();
365 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
366 final String name = idName + discriminator;
367 definitions.set(name, identityObj);
371 private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
372 final ArrayNode enumPayload, final EffectiveModelContext context) {
373 for (final IdentitySchemaNode derivedId : derivedIds) {
374 enumPayload.add(derivedId.getQName().getLocalName());
375 populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
379 private ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
380 final ObjectNode definitions, final DefinitionNames definitionNames,
381 final boolean isConfig, final SchemaInferenceStack stack,
382 final OAversion oaversion) throws IOException {
383 if (dataNode instanceof ListSchemaNode || dataNode instanceof ContainerSchemaNode) {
384 final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
385 final SchemaNode schemaNode = (SchemaNode) dataNode;
386 final String localName = schemaNode.getQName().getLocalName();
387 final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
388 final String nameAsParent = parentName + "_" + localName;
389 final ObjectNode properties =
390 processChildren(childSchema, containerChildren, parentName + "_" + localName, definitions,
391 definitionNames, isConfig, stack, oaversion);
393 final String nodeName = parentName + (isConfig ? CONFIG : "") + "_" + localName;
394 final String postNodeName = parentName + CONFIG + "_" + localName + POST_SUFFIX;
395 final String postXmlNodeName = postNodeName + XML_SUFFIX;
396 final String parentNameConfigLocalName = parentName + CONFIG + "_" + localName;
398 final String description = schemaNode.getDescription().orElse("");
399 final String discriminator;
401 if (!definitionNames.isListedNode(schemaNode)) {
402 final List<String> names = List.of(parentNameConfigLocalName,
403 parentNameConfigLocalName + TOP,
408 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
410 discriminator = definitionNames.getDiscriminator(schemaNode);
414 final ObjectNode postSchema = createPostJsonSchema(schemaNode, properties, postNodeName, description);
415 String truePostNodeName = postNodeName + discriminator;
416 definitions.set(truePostNodeName, postSchema);
418 final ObjectNode postXmlSchema = JsonNodeFactory.instance.objectNode();
419 postXmlSchema.put(REF_KEY, getAppropriateModelPrefix(oaversion) + truePostNodeName);
420 definitions.set(postXmlNodeName + discriminator, postXmlSchema);
423 childSchema.put(TYPE_KEY, OBJECT_TYPE);
424 childSchema.set(PROPERTIES_KEY, properties);
425 childSchema.put(TITLE_KEY, nodeName);
426 childSchema.put(DESCRIPTION_KEY, description);
428 final String defName = nodeName + discriminator;
429 childSchema.set(XML_KEY, buildXmlParameter(schemaNode));
430 definitions.set(defName, childSchema);
432 return processTopData(nodeName, discriminator, definitions, schemaNode, oaversion);
437 private static ObjectNode createPostJsonSchema(final SchemaNode dataNode, final ObjectNode properties,
438 final String postNodeName, final String description) {
439 final ObjectNode postSchema = JsonNodeFactory.instance.objectNode();
440 final ObjectNode postItemProperties;
441 if (dataNode instanceof ListSchemaNode) {
442 postItemProperties = createListItemProperties(properties, (ListSchemaNode) dataNode);
444 postItemProperties = properties.deepCopy();
446 postSchema.put(TYPE_KEY, OBJECT_TYPE);
447 postSchema.set(PROPERTIES_KEY, postItemProperties);
448 postSchema.put(TITLE_KEY, postNodeName);
449 postSchema.put(DESCRIPTION_KEY, description);
450 postSchema.set(XML_KEY, buildXmlParameter(dataNode));
454 private static ObjectNode createListItemProperties(final ObjectNode properties, final ListSchemaNode listNode) {
455 final ObjectNode postListItemProperties = JsonNodeFactory.instance.objectNode();
456 final List<QName> keyDefinition = listNode.getKeyDefinition();
457 final Set<String> keys = listNode.getChildNodes().stream()
458 .filter(node -> keyDefinition.contains(node.getQName()))
459 .map(node -> node.getQName().getLocalName())
460 .collect(Collectors.toSet());
462 Iterator<Map.Entry<String, JsonNode>> it = properties.fields();
463 while (it.hasNext()) {
464 Map.Entry<String, JsonNode> property = it.next();
465 if (!keys.contains(property.getKey())) {
466 postListItemProperties.set(property.getKey(), property.getValue());
470 return postListItemProperties;
474 * Processes the nodes.
476 private ObjectNode processChildren(
477 final ObjectNode parentNode, final Collection<? extends DataSchemaNode> nodes, final String parentName,
478 final ObjectNode definitions, final DefinitionNames definitionNames, final boolean isConfig,
479 final SchemaInferenceStack stack, final OAversion oaversion) throws IOException {
480 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
481 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
482 for (final DataSchemaNode node : nodes) {
483 stack.enterSchemaTree(node.getQName());
484 if (!isConfig || node.isConfiguration()) {
486 Add module name prefix to property name, when needed, when ServiceNow can process colons,
487 use RestDocGenUtil#resolveNodesName for creating property name
489 final String propertyName = node.getQName().getLocalName();
490 final ObjectNode property;
491 if (node instanceof LeafSchemaNode leaf) {
492 processLeafNode(leaf, propertyName, properties, required, stack, definitions, definitionNames,
494 } else if (node instanceof AnyxmlSchemaNode anyxml) {
495 processAnyXMLNode(anyxml, propertyName, properties, required);
496 } else if (node instanceof AnydataSchemaNode anydata) {
497 processAnydataNode(anydata, propertyName, properties, required);
499 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
500 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions,
501 definitionNames, isConfig, stack, oaversion);
503 processActionNodeContainer(node, parentName, definitions, definitionNames, stack,
506 } else if (node instanceof LeafListSchemaNode leafList) {
507 property = processLeafListNode(leafList, stack, definitions, definitionNames, oaversion);
509 } else if (node instanceof ChoiceSchemaNode choice) {
510 for (final CaseSchemaNode variant : choice.getCases()) {
511 stack.enterSchemaTree(variant.getQName());
512 processChoiceNode(variant.getChildNodes(), parentName, definitions, definitionNames,
513 isConfig, stack, properties, oaversion);
517 // FIXME dangerous statement here! Try to rework without continue.
520 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
522 properties.set(propertyName, property);
527 parentNode.set(PROPERTIES_KEY, properties);
528 setRequiredIfNotEmpty(parentNode, required);
532 private ObjectNode processLeafListNode(final LeafListSchemaNode listNode, final SchemaInferenceStack stack,
533 final ObjectNode definitions, final DefinitionNames definitionNames,
534 final OAversion oaversion) {
535 final ObjectNode props = JsonNodeFactory.instance.objectNode();
536 props.put(TYPE_KEY, ARRAY_TYPE);
538 final ObjectNode itemsVal = JsonNodeFactory.instance.objectNode();
539 final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
540 processElementCount(optConstraint, props);
542 processTypeDef(listNode.getType(), listNode, itemsVal, stack, definitions, definitionNames, oaversion);
543 props.set(ITEMS_KEY, itemsVal);
545 props.put(DESCRIPTION_KEY, listNode.getDescription().orElse(""));
550 private void processChoiceNode(
551 final Iterable<? extends DataSchemaNode> nodes, final String parentName, final ObjectNode definitions,
552 final DefinitionNames definitionNames, final boolean isConfig,
553 final SchemaInferenceStack stack, final ObjectNode properties, final OAversion oaversion)
555 for (final DataSchemaNode node : nodes) {
556 stack.enterSchemaTree(node.getQName());
558 Add module name prefix to property name, when needed, when ServiceNow can process colons,
559 use RestDocGenUtil#resolveNodesName for creating property name
561 final String name = node.getQName().getLocalName();
562 final ObjectNode property;
565 Ignore mandatoriness(passing unreferenced arrayNode to process...Node), because choice produces multiple
568 if (node instanceof LeafSchemaNode leaf) {
569 processLeafNode(leaf, name, properties, JsonNodeFactory.instance.arrayNode(), stack, definitions,
570 definitionNames, oaversion);
571 } else if (node instanceof AnyxmlSchemaNode anyxml) {
572 processAnyXMLNode(anyxml, name, properties, JsonNodeFactory.instance.arrayNode());
573 } else if (node instanceof AnydataSchemaNode anydata) {
574 processAnydataNode(anydata, name, properties, JsonNodeFactory.instance.arrayNode());
576 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
577 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions,
578 definitionNames, isConfig, stack, oaversion);
580 processActionNodeContainer(node, parentName, definitions, definitionNames, stack,
583 } else if (node instanceof LeafListSchemaNode leafList) {
584 property = processLeafListNode(leafList, stack, definitions, definitionNames, oaversion);
586 } else if (node instanceof ChoiceSchemaNode choice) {
587 for (final CaseSchemaNode variant : choice.getCases()) {
588 processChoiceNode(variant.getChildNodes(), parentName, definitions, definitionNames, isConfig,
589 stack, properties, oaversion);
593 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
595 properties.set(name, property);
601 private static void processElementCount(final Optional<ElementCountConstraint> constraint, final ObjectNode props) {
602 if (constraint.isPresent()) {
603 final ElementCountConstraint constr = constraint.get();
604 final Integer minElements = constr.getMinElements();
605 if (minElements != null) {
606 props.put(MIN_ITEMS, minElements);
608 final Integer maxElements = constr.getMaxElements();
609 if (maxElements != null) {
610 props.put(MAX_ITEMS, maxElements);
615 private static void processMandatory(final MandatoryAware node, final String nodeName, final ArrayNode required) {
616 if (node.isMandatory()) {
617 required.add(nodeName);
621 private ObjectNode processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
622 final ObjectNode properties, final ArrayNode required,
623 final SchemaInferenceStack stack, final ObjectNode definitions,
624 final DefinitionNames definitionNames, final OAversion oaversion) {
625 final ObjectNode property = JsonNodeFactory.instance.objectNode();
627 final String leafDescription = leafNode.getDescription().orElse("");
629 Description can't be added, because nothing allowed alongside $ref.
630 allOf is not an option, because ServiceNow can't parse it.
632 if (!(leafNode.getType() instanceof IdentityrefTypeDefinition)) {
633 property.put(DESCRIPTION_KEY, leafDescription);
636 processTypeDef(leafNode.getType(), leafNode, property, stack, definitions, definitionNames, oaversion);
637 properties.set(jsonLeafName, property);
638 property.set(XML_KEY, buildXmlParameter(leafNode));
639 processMandatory(leafNode, jsonLeafName, required);
644 private static ObjectNode processAnydataNode(final AnydataSchemaNode leafNode, final String name,
645 final ObjectNode properties, final ArrayNode required) {
646 final ObjectNode property = JsonNodeFactory.instance.objectNode();
648 final String leafDescription = leafNode.getDescription().orElse("");
649 property.put(DESCRIPTION_KEY, leafDescription);
651 final String localName = leafNode.getQName().getLocalName();
652 setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
653 property.put(TYPE_KEY, STRING_TYPE);
654 property.set(XML_KEY, buildXmlParameter(leafNode));
655 processMandatory(leafNode, name, required);
656 properties.set(name, property);
661 private static ObjectNode processAnyXMLNode(final AnyxmlSchemaNode leafNode, final String name,
662 final ObjectNode properties, final ArrayNode required) {
663 final ObjectNode property = JsonNodeFactory.instance.objectNode();
665 final String leafDescription = leafNode.getDescription().orElse("");
666 property.put(DESCRIPTION_KEY, leafDescription);
668 final String localName = leafNode.getQName().getLocalName();
669 setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
670 property.put(TYPE_KEY, STRING_TYPE);
671 property.set(XML_KEY, buildXmlParameter(leafNode));
672 processMandatory(leafNode, name, required);
673 properties.set(name, property);
678 private String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
679 final ObjectNode property, final SchemaInferenceStack stack,
680 final ObjectNode definitions, final DefinitionNames definitionNames,
681 final OAversion oaversion) {
682 final String jsonType;
683 if (leafTypeDef instanceof BinaryTypeDefinition) {
684 jsonType = processBinaryType(property);
686 } else if (leafTypeDef instanceof BitsTypeDefinition) {
687 jsonType = processBitsType((BitsTypeDefinition) leafTypeDef, property);
689 } else if (leafTypeDef instanceof EnumTypeDefinition) {
690 jsonType = processEnumType((EnumTypeDefinition) leafTypeDef, property);
692 } else if (leafTypeDef instanceof IdentityrefTypeDefinition) {
693 jsonType = processIdentityRefType((IdentityrefTypeDefinition) leafTypeDef, property, definitions,
694 definitionNames, oaversion, stack.getEffectiveModelContext());
696 } else if (leafTypeDef instanceof StringTypeDefinition) {
697 jsonType = processStringType(leafTypeDef, property, node.getQName().getLocalName());
699 } else if (leafTypeDef instanceof UnionTypeDefinition) {
700 jsonType = processUnionType((UnionTypeDefinition) leafTypeDef);
702 } else if (leafTypeDef instanceof EmptyTypeDefinition) {
703 jsonType = OBJECT_TYPE;
704 } else if (leafTypeDef instanceof LeafrefTypeDefinition) {
705 return processTypeDef(stack.resolveLeafref((LeafrefTypeDefinition) leafTypeDef), node, property,
706 stack, definitions, definitionNames, oaversion);
707 } else if (leafTypeDef instanceof BooleanTypeDefinition) {
708 jsonType = BOOLEAN_TYPE;
709 setDefaultValue(property, true);
710 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
711 jsonType = processNumberType((RangeRestrictedTypeDefinition<?, ?>) leafTypeDef, property);
712 } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
713 jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
715 jsonType = STRING_TYPE;
717 if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
718 putIfNonNull(property, TYPE_KEY, jsonType);
719 if (leafTypeDef.getDefaultValue().isPresent()) {
720 final Object defaultValue = leafTypeDef.getDefaultValue().get();
721 if (defaultValue instanceof String stringDefaultValue) {
722 if (leafTypeDef instanceof BooleanTypeDefinition) {
723 setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
724 } else if (leafTypeDef instanceof DecimalTypeDefinition
725 || leafTypeDef instanceof Uint64TypeDefinition) {
726 setDefaultValue(property, new BigDecimal(stringDefaultValue));
727 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
728 //uint8,16,32 int8,16,32,64
729 if (isHexadecimalOrOctal((RangeRestrictedTypeDefinition<?, ?>)leafTypeDef)) {
730 setDefaultValue(property, stringDefaultValue);
732 setDefaultValue(property, Long.valueOf(stringDefaultValue));
735 setDefaultValue(property, stringDefaultValue);
738 //we should never get here. getDefaultValue always gives us string
739 setDefaultValue(property, defaultValue.toString());
746 private static String processBinaryType(final ObjectNode property) {
747 property.put(FORMAT_KEY, "byte");
751 private static String processEnumType(final EnumTypeDefinition enumLeafType,
752 final ObjectNode property) {
753 final List<EnumPair> enumPairs = enumLeafType.getValues();
754 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
755 for (final EnumPair enumPair : enumPairs) {
756 enumNames.add(new TextNode(enumPair.getName()));
759 property.set(ENUM_KEY, enumNames);
760 setDefaultValue(property, enumLeafType.getValues().iterator().next().getName());
764 private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
765 final ObjectNode definitions, final DefinitionNames definitionNames,
766 final OAversion oaversion, final EffectiveModelContext schemaContext) {
767 final String definitionName;
768 if (isImported(leafTypeDef)) {
769 definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
771 final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
772 definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
774 property.put(REF_KEY, getAppropriateModelPrefix(oaversion) + definitionName);
778 private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
779 final ObjectNode definitions, final DefinitionNames definitionNames,
780 final EffectiveModelContext context) {
781 final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
782 final String identityName = idNode.getQName().getLocalName();
783 if (!definitionNames.isListedNode(idNode)) {
784 final ObjectNode identityObj = buildIdentityObject(idNode, context);
785 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
786 final String name = identityName + discriminator;
787 definitions.set(name, identityObj);
790 return identityName + definitionNames.getDiscriminator(idNode);
794 private static ObjectNode buildIdentityObject(final IdentitySchemaNode idNode,
795 final EffectiveModelContext context) {
796 final ObjectNode identityObj = JsonNodeFactory.instance.objectNode();
797 final String identityName = idNode.getQName().getLocalName();
798 LOG.debug("Processing Identity: {}", identityName);
800 identityObj.put(TITLE_KEY, identityName);
801 identityObj.put(DESCRIPTION_KEY, idNode.getDescription().orElse(""));
803 final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
805 final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
806 enumPayload.add(identityName);
807 populateEnumWithDerived(derivedIds, enumPayload, context);
808 identityObj.set(ENUM_KEY, enumPayload);
809 identityObj.put(TYPE_KEY, STRING_TYPE);
813 private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
814 return !leafTypeDef.getQName().getModule().equals(topLevelModule.getQNameModule());
817 private static String processBitsType(final BitsTypeDefinition bitsType,
818 final ObjectNode property) {
819 property.put(MIN_ITEMS, 0);
820 property.put(UNIQUE_ITEMS_KEY, true);
821 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
822 final Collection<? extends Bit> bits = bitsType.getBits();
823 for (final Bit bit : bits) {
824 enumNames.add(new TextNode(bit.getName()));
826 property.set(ENUM_KEY, enumNames);
827 property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
831 private static String processStringType(final TypeDefinition<?> stringType, final ObjectNode property,
832 final String nodeName) {
833 StringTypeDefinition type = (StringTypeDefinition) stringType;
834 Optional<LengthConstraint> lengthConstraints = ((StringTypeDefinition) stringType).getLengthConstraint();
835 while (lengthConstraints.isEmpty() && type.getBaseType() != null) {
836 type = type.getBaseType();
837 lengthConstraints = type.getLengthConstraint();
840 if (lengthConstraints.isPresent()) {
841 final Range<Integer> range = lengthConstraints.get().getAllowedRanges().span();
842 putIfNonNull(property, MIN_LENGTH_KEY, range.lowerEndpoint());
843 putIfNonNull(property, MAX_LENGTH_KEY, range.upperEndpoint());
846 if (type.getPatternConstraints().iterator().hasNext()) {
847 final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
848 String regex = pattern.getJavaPatternString();
849 regex = regex.substring(1, regex.length() - 1);
850 // Escape special characters to prevent issues inside Generex.
851 regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
852 String defaultValue = "";
854 final Generex generex = new Generex(regex);
855 defaultValue = generex.random();
856 } catch (IllegalArgumentException ex) {
857 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
859 setDefaultValue(property, defaultValue);
861 setDefaultValue(property, "Some " + nodeName);
866 private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
867 final ObjectNode property) {
868 final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
869 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
871 if (isHexadecimalOrOctal(leafTypeDef)) {
875 if (leafTypeDef instanceof DecimalTypeDefinition) {
876 maybeLower.ifPresent(number -> setDefaultValue(property, ((Decimal64) number).decimalValue()));
879 if (leafTypeDef instanceof Uint8TypeDefinition
880 || leafTypeDef instanceof Uint16TypeDefinition
881 || leafTypeDef instanceof Int8TypeDefinition
882 || leafTypeDef instanceof Int16TypeDefinition
883 || leafTypeDef instanceof Int32TypeDefinition) {
885 property.put(FORMAT_KEY, INT32_FORMAT);
886 maybeLower.ifPresent(number -> setDefaultValue(property, Integer.valueOf(number.toString())));
887 } else if (leafTypeDef instanceof Uint32TypeDefinition
888 || leafTypeDef instanceof Int64TypeDefinition) {
890 property.put(FORMAT_KEY, INT64_FORMAT);
891 maybeLower.ifPresent(number -> setDefaultValue(property, Long.valueOf(number.toString())));
894 setDefaultValue(property, 0);
899 private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
900 final Optional<?> optDefaultValue = typeDef.getDefaultValue();
901 if (optDefaultValue.isPresent()) {
902 final String defaultValue = (String)optDefaultValue.get();
903 return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
908 private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
909 final EffectiveModelContext schemaContext) {
910 // create example instance-identifier to the first container of node's module if exists or leave it empty
911 final var module = schemaContext.findModule(node.getQName().getModule());
912 if (module.isPresent()) {
913 final var container = module.get().getChildNodes().stream()
914 .filter(n -> n instanceof ContainerSchemaNode)
916 container.ifPresent(c -> setDefaultValue(property, String.format("/%s:%s", module.get().getPrefix(),
917 c.getQName().getLocalName())));
923 private static String processUnionType(final UnionTypeDefinition unionType) {
924 boolean isStringTakePlace = false;
925 boolean isNumberTakePlace = false;
926 boolean isBooleanTakePlace = false;
927 for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
928 if (!isStringTakePlace) {
929 if (typeDef instanceof StringTypeDefinition
930 || typeDef instanceof BitsTypeDefinition
931 || typeDef instanceof BinaryTypeDefinition
932 || typeDef instanceof IdentityrefTypeDefinition
933 || typeDef instanceof EnumTypeDefinition
934 || typeDef instanceof LeafrefTypeDefinition
935 || typeDef instanceof UnionTypeDefinition) {
936 isStringTakePlace = true;
937 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
938 isNumberTakePlace = true;
939 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
940 isBooleanTakePlace = true;
944 if (isStringTakePlace) {
947 if (isBooleanTakePlace) {
948 if (isNumberTakePlace) {
956 private static ObjectNode buildXmlParameter(final SchemaNode node) {
957 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
958 final QName qName = node.getQName();
959 xml.put(NAME_KEY, qName.getLocalName());
960 xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
964 private static void putIfNonNull(final ObjectNode property, final String key, final Number number) {
965 if (key != null && number != null) {
966 if (number instanceof Double) {
967 property.put(key, (Double) number);
968 } else if (number instanceof Float) {
969 property.put(key, (Float) number);
970 } else if (number instanceof Integer) {
971 property.put(key, (Integer) number);
972 } else if (number instanceof Short) {
973 property.put(key, (Short) number);
974 } else if (number instanceof Long) {
975 property.put(key, (Long) number);
980 private static void putIfNonNull(final ObjectNode property, final String key, final String value) {
981 if (key != null && value != null) {
982 property.put(key, value);
986 private static void setRequiredIfNotEmpty(final ObjectNode node, final ArrayNode required) {
987 if (required.size() > 0) {
988 node.set(REQUIRED_KEY, required);
992 private static void setDefaultValue(final ObjectNode property, final String value) {
993 property.put(DEFAULT_KEY, value);
996 private static void setDefaultValue(final ObjectNode property, final Integer value) {
997 property.put(DEFAULT_KEY, value);
1000 private static void setDefaultValue(final ObjectNode property, final Long value) {
1001 property.put(DEFAULT_KEY, value);
1004 private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
1005 property.put(DEFAULT_KEY, value);
1008 private static void setDefaultValue(final ObjectNode property, final Boolean value) {
1009 property.put(DEFAULT_KEY, value);