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);
213 if (node instanceof LeafSchemaNode) {
215 Add module name prefix to property name, when ServiceNow can process colons(second parameter
218 processLeafNode((LeafSchemaNode) node, localName, properties, required, stack,
219 definitions, definitionNames, oaversion);
225 definition.put(TITLE_KEY, definitionName);
226 definition.put(TYPE_KEY, OBJECT_TYPE);
227 definition.set(PROPERTIES_KEY, properties);
228 definition.put(DESCRIPTION_KEY, module.getDescription().orElse(""));
229 setRequiredIfNotEmpty(definition, required);
231 definitions.set(definitionName, definition);
234 private void processContainersAndLists(final Module module, final ObjectNode definitions,
235 final DefinitionNames definitionNames, final EffectiveModelContext schemaContext, final OAversion oaversion)
237 final String moduleName = module.getName();
238 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
239 for (final DataSchemaNode childNode : module.getChildNodes()) {
240 stack.enterSchemaTree(childNode.getQName());
241 // For every container and list in the module
242 if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
243 if (childNode.isConfiguration()) {
244 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
245 true, stack, oaversion);
247 processDataNodeContainer((DataNodeContainer) childNode, moduleName, definitions, definitionNames,
248 false, stack, oaversion);
249 processActionNodeContainer(childNode, moduleName, definitions, definitionNames, stack, oaversion);
255 private void processActionNodeContainer(final DataSchemaNode childNode, final String moduleName,
256 final ObjectNode definitions, final DefinitionNames definitionNames,
257 final SchemaInferenceStack stack, final OAversion oaversion)
259 for (final ActionDefinition actionDef : ((ActionNodeContainer) childNode).getActions()) {
260 stack.enterSchemaTree(actionDef.getQName());
261 processOperations(actionDef, moduleName, definitions, definitionNames, stack, oaversion);
266 private void processRPCs(final Module module, final ObjectNode definitions, final DefinitionNames definitionNames,
267 final EffectiveModelContext schemaContext, final OAversion oaversion) throws IOException {
268 final String moduleName = module.getName();
269 final SchemaInferenceStack stack = SchemaInferenceStack.of(schemaContext);
270 for (final RpcDefinition rpcDefinition : module.getRpcs()) {
271 stack.enterSchemaTree(rpcDefinition.getQName());
272 processOperations(rpcDefinition, moduleName, definitions, definitionNames, stack, oaversion);
277 private void processOperations(final OperationDefinition operationDef, final String parentName,
278 final ObjectNode definitions, final DefinitionNames definitionNames,
279 final SchemaInferenceStack stack, final OAversion oaversion)
281 final String operationName = operationDef.getQName().getLocalName();
282 processOperationInputOutput(operationDef.getInput(), operationName, parentName, true, definitions,
283 definitionNames, stack, oaversion);
284 processOperationInputOutput(operationDef.getOutput(), operationName, parentName, false, definitions,
285 definitionNames, stack, oaversion);
288 private void processOperationInputOutput(final ContainerLike container, final String operationName,
289 final String parentName, final boolean isInput,
290 final ObjectNode definitions, final DefinitionNames definitionNames,
291 final SchemaInferenceStack stack, final OAversion oaversion)
293 stack.enterSchemaTree(container.getQName());
294 if (!container.getChildNodes().isEmpty()) {
295 final String filename = parentName + "_" + operationName + (isInput ? INPUT_SUFFIX : OUTPUT_SUFFIX);
296 final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
297 processChildren(childSchema, container.getChildNodes(), parentName, definitions, definitionNames,
298 false, stack, oaversion);
300 childSchema.put(TYPE_KEY, OBJECT_TYPE);
301 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
302 xml.put(NAME_KEY, isInput ? INPUT : OUTPUT);
303 childSchema.set(XML_KEY, xml);
304 childSchema.put(TITLE_KEY, filename);
305 final String discriminator =
306 definitionNames.pickDiscriminator(container, List.of(filename, filename + TOP));
307 definitions.set(filename + discriminator, childSchema);
309 processTopData(filename, discriminator, definitions, container, oaversion);
314 private static ObjectNode processTopData(final String filename, final String discriminator,
315 final ObjectNode definitions, final SchemaNode schemaNode, final OAversion oaversion) {
316 final ObjectNode dataNodeProperties = JsonNodeFactory.instance.objectNode();
317 final String name = filename + discriminator;
318 final String ref = getAppropriateModelPrefix(oaversion) + name;
319 final String topName = filename + TOP;
321 if (schemaNode instanceof ListSchemaNode) {
322 dataNodeProperties.put(TYPE_KEY, ARRAY_TYPE);
323 final ObjectNode items = JsonNodeFactory.instance.objectNode();
324 items.put(REF_KEY, ref);
325 dataNodeProperties.set(ITEMS_KEY, items);
326 dataNodeProperties.put(DESCRIPTION_KEY, schemaNode.getDescription().orElse(""));
329 Description can't be added, because nothing allowed alongside $ref.
330 allOf is not an option, because ServiceNow can't parse it.
332 dataNodeProperties.put(REF_KEY, ref);
335 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
337 Add module name prefix to property name, when needed, when ServiceNow can process colons,
338 use RestDocGenUtil#resolveNodesName for creating property name
340 properties.set(schemaNode.getQName().getLocalName(), dataNodeProperties);
341 final ObjectNode finalChildSchema = JsonNodeFactory.instance.objectNode();
342 finalChildSchema.put(TYPE_KEY, OBJECT_TYPE);
343 finalChildSchema.set(PROPERTIES_KEY, properties);
344 finalChildSchema.put(TITLE_KEY, topName);
347 definitions.set(topName + discriminator, finalChildSchema);
349 return dataNodeProperties;
353 * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
354 * @param module The module from which the identity stmt will be processed
355 * @param definitions The ObjectNode in which the parsed identity will be put as a 'model' obj
356 * @param definitionNames Store for definition names
358 private static void processIdentities(final Module module, final ObjectNode definitions,
359 final DefinitionNames definitionNames, final EffectiveModelContext context) {
360 final String moduleName = module.getName();
361 final Collection<? extends IdentitySchemaNode> idNodes = module.getIdentities();
362 LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
364 for (final IdentitySchemaNode idNode : idNodes) {
365 final ObjectNode identityObj = buildIdentityObject(idNode, context);
366 final String idName = idNode.getQName().getLocalName();
367 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(idName));
368 final String name = idName + discriminator;
369 definitions.set(name, identityObj);
373 private static void populateEnumWithDerived(final Collection<? extends IdentitySchemaNode> derivedIds,
374 final ArrayNode enumPayload, final EffectiveModelContext context) {
375 for (final IdentitySchemaNode derivedId : derivedIds) {
376 enumPayload.add(derivedId.getQName().getLocalName());
377 populateEnumWithDerived(context.getDerivedIdentities(derivedId), enumPayload, context);
381 private ObjectNode processDataNodeContainer(final DataNodeContainer dataNode, final String parentName,
382 final ObjectNode definitions, final DefinitionNames definitionNames,
383 final boolean isConfig, final SchemaInferenceStack stack,
384 final OAversion oaversion) throws IOException {
385 if (dataNode instanceof ListSchemaNode || dataNode instanceof ContainerSchemaNode) {
386 final Collection<? extends DataSchemaNode> containerChildren = dataNode.getChildNodes();
387 final SchemaNode schemaNode = (SchemaNode) dataNode;
388 final String localName = schemaNode.getQName().getLocalName();
389 final ObjectNode childSchema = JsonNodeFactory.instance.objectNode();
390 final String nameAsParent = parentName + "_" + localName;
391 final ObjectNode properties =
392 processChildren(childSchema, containerChildren, parentName + "_" + localName, definitions,
393 definitionNames, isConfig, stack, oaversion);
395 final String nodeName = parentName + (isConfig ? CONFIG : "") + "_" + localName;
396 final String postNodeName = parentName + CONFIG + "_" + localName + POST_SUFFIX;
397 final String postXmlNodeName = postNodeName + XML_SUFFIX;
398 final String parentNameConfigLocalName = parentName + CONFIG + "_" + localName;
400 final String description = schemaNode.getDescription().orElse("");
401 final String discriminator;
403 if (!definitionNames.isListedNode(schemaNode)) {
404 final List<String> names = List.of(parentNameConfigLocalName,
405 parentNameConfigLocalName + TOP,
410 discriminator = definitionNames.pickDiscriminator(schemaNode, names);
412 discriminator = definitionNames.getDiscriminator(schemaNode);
416 final ObjectNode postSchema = createPostJsonSchema(schemaNode, properties, postNodeName, description);
417 String truePostNodeName = postNodeName + discriminator;
418 definitions.set(truePostNodeName, postSchema);
420 final ObjectNode postXmlSchema = JsonNodeFactory.instance.objectNode();
421 postXmlSchema.put(REF_KEY, getAppropriateModelPrefix(oaversion) + truePostNodeName);
422 definitions.set(postXmlNodeName + discriminator, postXmlSchema);
425 childSchema.put(TYPE_KEY, OBJECT_TYPE);
426 childSchema.set(PROPERTIES_KEY, properties);
427 childSchema.put(TITLE_KEY, nodeName);
428 childSchema.put(DESCRIPTION_KEY, description);
430 final String defName = nodeName + discriminator;
431 childSchema.set(XML_KEY, buildXmlParameter(schemaNode));
432 definitions.set(defName, childSchema);
434 return processTopData(nodeName, discriminator, definitions, schemaNode, oaversion);
439 private static ObjectNode createPostJsonSchema(final SchemaNode dataNode, final ObjectNode properties,
440 final String postNodeName, final String description) {
441 final ObjectNode postSchema = JsonNodeFactory.instance.objectNode();
442 final ObjectNode postItemProperties;
443 if (dataNode instanceof ListSchemaNode) {
444 postItemProperties = createListItemProperties(properties, (ListSchemaNode) dataNode);
446 postItemProperties = properties.deepCopy();
448 postSchema.put(TYPE_KEY, OBJECT_TYPE);
449 postSchema.set(PROPERTIES_KEY, postItemProperties);
450 postSchema.put(TITLE_KEY, postNodeName);
451 postSchema.put(DESCRIPTION_KEY, description);
452 postSchema.set(XML_KEY, buildXmlParameter(dataNode));
456 private static ObjectNode createListItemProperties(final ObjectNode properties, final ListSchemaNode listNode) {
457 final ObjectNode postListItemProperties = JsonNodeFactory.instance.objectNode();
458 final List<QName> keyDefinition = listNode.getKeyDefinition();
459 final Set<String> keys = listNode.getChildNodes().stream()
460 .filter(node -> keyDefinition.contains(node.getQName()))
461 .map(node -> node.getQName().getLocalName())
462 .collect(Collectors.toSet());
464 Iterator<Map.Entry<String, JsonNode>> it = properties.fields();
465 while (it.hasNext()) {
466 Map.Entry<String, JsonNode> property = it.next();
467 if (!keys.contains(property.getKey())) {
468 postListItemProperties.set(property.getKey(), property.getValue());
472 return postListItemProperties;
476 * Processes the nodes.
478 private ObjectNode processChildren(
479 final ObjectNode parentNode, final Collection<? extends DataSchemaNode> nodes, final String parentName,
480 final ObjectNode definitions, final DefinitionNames definitionNames, final boolean isConfig,
481 final SchemaInferenceStack stack, final OAversion oaversion) throws IOException {
482 final ObjectNode properties = JsonNodeFactory.instance.objectNode();
483 final ArrayNode required = JsonNodeFactory.instance.arrayNode();
484 for (final DataSchemaNode node : nodes) {
485 stack.enterSchemaTree(node.getQName());
486 if (!isConfig || node.isConfiguration()) {
488 Add module name prefix to property name, when needed, when ServiceNow can process colons,
489 use RestDocGenUtil#resolveNodesName for creating property name
491 final String propertyName = node.getQName().getLocalName();
492 final ObjectNode property;
493 if (node instanceof LeafSchemaNode) {
494 processLeafNode((LeafSchemaNode) node, propertyName, properties,
495 required, stack, definitions, definitionNames, oaversion);
496 } else if (node instanceof AnyxmlSchemaNode) {
497 processAnyXMLNode((AnyxmlSchemaNode) node, propertyName, properties,
499 } else if (node instanceof AnydataSchemaNode) {
500 processAnydataNode((AnydataSchemaNode) node, propertyName, properties, required);
502 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
503 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions,
504 definitionNames, isConfig, stack, oaversion);
506 processActionNodeContainer(node, parentName, definitions, definitionNames, stack,
509 } else if (node instanceof LeafListSchemaNode) {
510 property = processLeafListNode((LeafListSchemaNode) node, stack, definitions,
511 definitionNames, oaversion);
513 } else if (node instanceof ChoiceSchemaNode) {
514 for (final CaseSchemaNode variant : ((ChoiceSchemaNode) node).getCases()) {
515 stack.enterSchemaTree(variant.getQName());
516 processChoiceNode(variant.getChildNodes(), parentName, definitions, definitionNames,
517 isConfig, stack, properties, oaversion);
521 // FIXME dangerous statement here! Try to rework without continue.
524 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
526 properties.set(propertyName, property);
531 parentNode.set(PROPERTIES_KEY, properties);
532 setRequiredIfNotEmpty(parentNode, required);
536 private ObjectNode processLeafListNode(final LeafListSchemaNode listNode, final SchemaInferenceStack stack,
537 final ObjectNode definitions, final DefinitionNames definitionNames,
538 final OAversion oaversion) {
539 final ObjectNode props = JsonNodeFactory.instance.objectNode();
540 props.put(TYPE_KEY, ARRAY_TYPE);
542 final ObjectNode itemsVal = JsonNodeFactory.instance.objectNode();
543 final Optional<ElementCountConstraint> optConstraint = listNode.getElementCountConstraint();
544 processElementCount(optConstraint, props);
546 processTypeDef(listNode.getType(), listNode, itemsVal, stack, definitions, definitionNames, oaversion);
547 props.set(ITEMS_KEY, itemsVal);
549 props.put(DESCRIPTION_KEY, listNode.getDescription().orElse(""));
554 private void processChoiceNode(
555 final Iterable<? extends DataSchemaNode> nodes, final String parentName, final ObjectNode definitions,
556 final DefinitionNames definitionNames, final boolean isConfig,
557 final SchemaInferenceStack stack, final ObjectNode properties, final OAversion oaversion)
559 for (final DataSchemaNode node : nodes) {
560 stack.enterSchemaTree(node.getQName());
562 Add module name prefix to property name, when needed, when ServiceNow can process colons,
563 use RestDocGenUtil#resolveNodesName for creating property name
565 final String name = node.getQName().getLocalName();
566 final ObjectNode property;
569 Ignore mandatoriness(passing unreferenced arrayNode to process...Node), because choice produces multiple
572 if (node instanceof LeafSchemaNode) {
573 processLeafNode((LeafSchemaNode) node, name, properties,
574 JsonNodeFactory.instance.arrayNode(), stack, definitions, definitionNames, oaversion);
575 } else if (node instanceof AnyxmlSchemaNode) {
576 processAnyXMLNode((AnyxmlSchemaNode) node, name, properties,
577 JsonNodeFactory.instance.arrayNode());
578 } else if (node instanceof AnydataSchemaNode) {
579 processAnydataNode((AnydataSchemaNode) node, name, properties,
580 JsonNodeFactory.instance.arrayNode());
582 if (node instanceof ListSchemaNode || node instanceof ContainerSchemaNode) {
583 property = processDataNodeContainer((DataNodeContainer) node, parentName, definitions,
584 definitionNames, isConfig, stack, oaversion);
586 processActionNodeContainer(node, parentName, definitions, definitionNames, stack,
589 } else if (node instanceof LeafListSchemaNode) {
590 property = processLeafListNode((LeafListSchemaNode) node, stack, definitions,
591 definitionNames, oaversion);
593 } else if (node instanceof ChoiceSchemaNode) {
594 for (final CaseSchemaNode variant : ((ChoiceSchemaNode) node).getCases()) {
595 processChoiceNode(variant.getChildNodes(), parentName, definitions, definitionNames, isConfig,
596 stack, properties, oaversion);
600 throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
602 properties.set(name, property);
608 private static void processElementCount(final Optional<ElementCountConstraint> constraint, final ObjectNode props) {
609 if (constraint.isPresent()) {
610 final ElementCountConstraint constr = constraint.get();
611 final Integer minElements = constr.getMinElements();
612 if (minElements != null) {
613 props.put(MIN_ITEMS, minElements);
615 final Integer maxElements = constr.getMaxElements();
616 if (maxElements != null) {
617 props.put(MAX_ITEMS, maxElements);
622 private static void processMandatory(final MandatoryAware node, final String nodeName, final ArrayNode required) {
623 if (node.isMandatory()) {
624 required.add(nodeName);
628 private ObjectNode processLeafNode(final LeafSchemaNode leafNode, final String jsonLeafName,
629 final ObjectNode properties, final ArrayNode required,
630 final SchemaInferenceStack stack, final ObjectNode definitions,
631 final DefinitionNames definitionNames, final OAversion oaversion) {
632 final ObjectNode property = JsonNodeFactory.instance.objectNode();
634 final String leafDescription = leafNode.getDescription().orElse("");
636 Description can't be added, because nothing allowed alongside $ref.
637 allOf is not an option, because ServiceNow can't parse it.
639 if (!(leafNode.getType() instanceof IdentityrefTypeDefinition)) {
640 property.put(DESCRIPTION_KEY, leafDescription);
643 processTypeDef(leafNode.getType(), leafNode, property, stack, definitions, definitionNames, oaversion);
644 properties.set(jsonLeafName, property);
645 property.set(XML_KEY, buildXmlParameter(leafNode));
646 processMandatory(leafNode, jsonLeafName, required);
651 private static ObjectNode processAnydataNode(final AnydataSchemaNode leafNode, final String name,
652 final ObjectNode properties, final ArrayNode required) {
653 final ObjectNode property = JsonNodeFactory.instance.objectNode();
655 final String leafDescription = leafNode.getDescription().orElse("");
656 property.put(DESCRIPTION_KEY, leafDescription);
658 final String localName = leafNode.getQName().getLocalName();
659 setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
660 property.put(TYPE_KEY, STRING_TYPE);
661 property.set(XML_KEY, buildXmlParameter(leafNode));
662 processMandatory(leafNode, name, required);
663 properties.set(name, property);
668 private static ObjectNode processAnyXMLNode(final AnyxmlSchemaNode leafNode, final String name,
669 final ObjectNode properties, final ArrayNode required) {
670 final ObjectNode property = JsonNodeFactory.instance.objectNode();
672 final String leafDescription = leafNode.getDescription().orElse("");
673 property.put(DESCRIPTION_KEY, leafDescription);
675 final String localName = leafNode.getQName().getLocalName();
676 setDefaultValue(property, String.format("<%s> ... </%s>", localName, localName));
677 property.put(TYPE_KEY, STRING_TYPE);
678 property.set(XML_KEY, buildXmlParameter(leafNode));
679 processMandatory(leafNode, name, required);
680 properties.set(name, property);
685 private String processTypeDef(final TypeDefinition<?> leafTypeDef, final DataSchemaNode node,
686 final ObjectNode property, final SchemaInferenceStack stack,
687 final ObjectNode definitions, final DefinitionNames definitionNames,
688 final OAversion oaversion) {
689 final String jsonType;
690 if (leafTypeDef instanceof BinaryTypeDefinition) {
691 jsonType = processBinaryType(property);
693 } else if (leafTypeDef instanceof BitsTypeDefinition) {
694 jsonType = processBitsType((BitsTypeDefinition) leafTypeDef, property);
696 } else if (leafTypeDef instanceof EnumTypeDefinition) {
697 jsonType = processEnumType((EnumTypeDefinition) leafTypeDef, property);
699 } else if (leafTypeDef instanceof IdentityrefTypeDefinition) {
700 jsonType = processIdentityRefType((IdentityrefTypeDefinition) leafTypeDef, property, definitions,
701 definitionNames, oaversion, stack.getEffectiveModelContext());
703 } else if (leafTypeDef instanceof StringTypeDefinition) {
704 jsonType = processStringType(leafTypeDef, property, node.getQName().getLocalName());
706 } else if (leafTypeDef instanceof UnionTypeDefinition) {
707 jsonType = processUnionType((UnionTypeDefinition) leafTypeDef);
709 } else if (leafTypeDef instanceof EmptyTypeDefinition) {
710 jsonType = OBJECT_TYPE;
711 } else if (leafTypeDef instanceof LeafrefTypeDefinition) {
712 return processTypeDef(stack.resolveLeafref((LeafrefTypeDefinition) leafTypeDef), node, property,
713 stack, definitions, definitionNames, oaversion);
714 } else if (leafTypeDef instanceof BooleanTypeDefinition) {
715 jsonType = BOOLEAN_TYPE;
716 setDefaultValue(property, true);
717 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
718 jsonType = processNumberType((RangeRestrictedTypeDefinition<?, ?>) leafTypeDef, property);
719 } else if (leafTypeDef instanceof InstanceIdentifierTypeDefinition) {
720 jsonType = processInstanceIdentifierType(node, property, stack.getEffectiveModelContext());
722 jsonType = STRING_TYPE;
724 if (!(leafTypeDef instanceof IdentityrefTypeDefinition)) {
725 putIfNonNull(property, TYPE_KEY, jsonType);
726 if (leafTypeDef.getDefaultValue().isPresent()) {
727 final Object defaultValue = leafTypeDef.getDefaultValue().get();
728 if (defaultValue instanceof String) {
729 final String stringDefaultValue = (String) defaultValue;
730 if (leafTypeDef instanceof BooleanTypeDefinition) {
731 setDefaultValue(property, Boolean.valueOf(stringDefaultValue));
732 } else if (leafTypeDef instanceof DecimalTypeDefinition
733 || leafTypeDef instanceof Uint64TypeDefinition) {
734 setDefaultValue(property, new BigDecimal(stringDefaultValue));
735 } else if (leafTypeDef instanceof RangeRestrictedTypeDefinition) {
736 //uint8,16,32 int8,16,32,64
737 if (isHexadecimalOrOctal((RangeRestrictedTypeDefinition<?, ?>)leafTypeDef)) {
738 setDefaultValue(property, stringDefaultValue);
740 setDefaultValue(property, Long.valueOf(stringDefaultValue));
743 setDefaultValue(property, stringDefaultValue);
746 //we should never get here. getDefaultValue always gives us string
747 setDefaultValue(property, defaultValue.toString());
754 private static String processBinaryType(final ObjectNode property) {
755 property.put(FORMAT_KEY, "byte");
759 private static String processEnumType(final EnumTypeDefinition enumLeafType,
760 final ObjectNode property) {
761 final List<EnumPair> enumPairs = enumLeafType.getValues();
762 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
763 for (final EnumPair enumPair : enumPairs) {
764 enumNames.add(new TextNode(enumPair.getName()));
767 property.set(ENUM_KEY, enumNames);
768 setDefaultValue(property, enumLeafType.getValues().iterator().next().getName());
772 private String processIdentityRefType(final IdentityrefTypeDefinition leafTypeDef, final ObjectNode property,
773 final ObjectNode definitions, final DefinitionNames definitionNames,
774 final OAversion oaversion, final EffectiveModelContext schemaContext) {
775 final String definitionName;
776 if (isImported(leafTypeDef)) {
777 definitionName = addImportedIdentity(leafTypeDef, definitions, definitionNames, schemaContext);
779 final SchemaNode node = leafTypeDef.getIdentities().iterator().next();
780 definitionName = node.getQName().getLocalName() + definitionNames.getDiscriminator(node);
782 property.put(REF_KEY, getAppropriateModelPrefix(oaversion) + definitionName);
786 private static String addImportedIdentity(final IdentityrefTypeDefinition leafTypeDef,
787 final ObjectNode definitions, final DefinitionNames definitionNames,
788 final EffectiveModelContext context) {
789 final IdentitySchemaNode idNode = leafTypeDef.getIdentities().iterator().next();
790 final String identityName = idNode.getQName().getLocalName();
791 if (!definitionNames.isListedNode(idNode)) {
792 final ObjectNode identityObj = buildIdentityObject(idNode, context);
793 final String discriminator = definitionNames.pickDiscriminator(idNode, List.of(identityName));
794 final String name = identityName + discriminator;
795 definitions.set(name, identityObj);
798 return identityName + definitionNames.getDiscriminator(idNode);
802 private static ObjectNode buildIdentityObject(final IdentitySchemaNode idNode,
803 final EffectiveModelContext context) {
804 final ObjectNode identityObj = JsonNodeFactory.instance.objectNode();
805 final String identityName = idNode.getQName().getLocalName();
806 LOG.debug("Processing Identity: {}", identityName);
808 identityObj.put(TITLE_KEY, identityName);
809 identityObj.put(DESCRIPTION_KEY, idNode.getDescription().orElse(""));
811 final Collection<? extends IdentitySchemaNode> derivedIds = context.getDerivedIdentities(idNode);
813 final ArrayNode enumPayload = JsonNodeFactory.instance.arrayNode();
814 enumPayload.add(identityName);
815 populateEnumWithDerived(derivedIds, enumPayload, context);
816 identityObj.set(ENUM_KEY, enumPayload);
817 identityObj.put(TYPE_KEY, STRING_TYPE);
821 private boolean isImported(final IdentityrefTypeDefinition leafTypeDef) {
822 return !leafTypeDef.getQName().getModule().equals(topLevelModule.getQNameModule());
825 private static String processBitsType(final BitsTypeDefinition bitsType,
826 final ObjectNode property) {
827 property.put(MIN_ITEMS, 0);
828 property.put(UNIQUE_ITEMS_KEY, true);
829 final ArrayNode enumNames = new ArrayNode(JsonNodeFactory.instance);
830 final Collection<? extends Bit> bits = bitsType.getBits();
831 for (final Bit bit : bits) {
832 enumNames.add(new TextNode(bit.getName()));
834 property.set(ENUM_KEY, enumNames);
835 property.put(DEFAULT_KEY, enumNames.iterator().next() + " " + enumNames.get(enumNames.size() - 1));
839 private static String processStringType(final TypeDefinition<?> stringType, final ObjectNode property,
840 final String nodeName) {
841 StringTypeDefinition type = (StringTypeDefinition) stringType;
842 Optional<LengthConstraint> lengthConstraints = ((StringTypeDefinition) stringType).getLengthConstraint();
843 while (lengthConstraints.isEmpty() && type.getBaseType() != null) {
844 type = type.getBaseType();
845 lengthConstraints = type.getLengthConstraint();
848 if (lengthConstraints.isPresent()) {
849 final Range<Integer> range = lengthConstraints.get().getAllowedRanges().span();
850 putIfNonNull(property, MIN_LENGTH_KEY, range.lowerEndpoint());
851 putIfNonNull(property, MAX_LENGTH_KEY, range.upperEndpoint());
854 if (type.getPatternConstraints().iterator().hasNext()) {
855 final PatternConstraint pattern = type.getPatternConstraints().iterator().next();
856 String regex = pattern.getJavaPatternString();
857 regex = regex.substring(1, regex.length() - 1);
858 // Escape special characters to prevent issues inside Generex.
859 regex = AUTOMATON_SPECIAL_CHARACTERS.matcher(regex).replaceAll("\\\\$0");
860 String defaultValue = "";
862 final Generex generex = new Generex(regex);
863 defaultValue = generex.random();
864 } catch (IllegalArgumentException ex) {
865 LOG.warn("Cannot create example string for type: {} with regex: {}.", stringType.getQName(), regex);
867 setDefaultValue(property, defaultValue);
869 setDefaultValue(property, "Some " + nodeName);
874 private static String processNumberType(final RangeRestrictedTypeDefinition<?, ?> leafTypeDef,
875 final ObjectNode property) {
876 final Optional<Number> maybeLower = leafTypeDef.getRangeConstraint()
877 .map(RangeConstraint::getAllowedRanges).map(RangeSet::span).map(Range::lowerEndpoint);
879 if (isHexadecimalOrOctal(leafTypeDef)) {
883 if (leafTypeDef instanceof DecimalTypeDefinition) {
884 maybeLower.ifPresent(number -> setDefaultValue(property, ((Decimal64) number).decimalValue()));
887 if (leafTypeDef instanceof Uint8TypeDefinition
888 || leafTypeDef instanceof Uint16TypeDefinition
889 || leafTypeDef instanceof Int8TypeDefinition
890 || leafTypeDef instanceof Int16TypeDefinition
891 || leafTypeDef instanceof Int32TypeDefinition) {
893 property.put(FORMAT_KEY, INT32_FORMAT);
894 maybeLower.ifPresent(number -> setDefaultValue(property, Integer.valueOf(number.toString())));
895 } else if (leafTypeDef instanceof Uint32TypeDefinition
896 || leafTypeDef instanceof Int64TypeDefinition) {
898 property.put(FORMAT_KEY, INT64_FORMAT);
899 maybeLower.ifPresent(number -> setDefaultValue(property, Long.valueOf(number.toString())));
902 setDefaultValue(property, 0);
907 private static boolean isHexadecimalOrOctal(final RangeRestrictedTypeDefinition<?, ?> typeDef) {
908 final Optional<?> optDefaultValue = typeDef.getDefaultValue();
909 if (optDefaultValue.isPresent()) {
910 final String defaultValue = (String)optDefaultValue.get();
911 return defaultValue.startsWith("0") || defaultValue.startsWith("-0");
916 private static String processInstanceIdentifierType(final DataSchemaNode node, final ObjectNode property,
917 final EffectiveModelContext schemaContext) {
918 // create example instance-identifier to the first container of node's module if exists or leave it empty
919 final var module = schemaContext.findModule(node.getQName().getModule());
920 if (module.isPresent()) {
921 final var container = module.get().getChildNodes().stream()
922 .filter(n -> n instanceof ContainerSchemaNode)
924 container.ifPresent(c -> setDefaultValue(property, String.format("/%s:%s", module.get().getPrefix(),
925 c.getQName().getLocalName())));
931 private static String processUnionType(final UnionTypeDefinition unionType) {
932 boolean isStringTakePlace = false;
933 boolean isNumberTakePlace = false;
934 boolean isBooleanTakePlace = false;
935 for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
936 if (!isStringTakePlace) {
937 if (typeDef instanceof StringTypeDefinition
938 || typeDef instanceof BitsTypeDefinition
939 || typeDef instanceof BinaryTypeDefinition
940 || typeDef instanceof IdentityrefTypeDefinition
941 || typeDef instanceof EnumTypeDefinition
942 || typeDef instanceof LeafrefTypeDefinition
943 || typeDef instanceof UnionTypeDefinition) {
944 isStringTakePlace = true;
945 } else if (!isNumberTakePlace && typeDef instanceof RangeRestrictedTypeDefinition) {
946 isNumberTakePlace = true;
947 } else if (!isBooleanTakePlace && typeDef instanceof BooleanTypeDefinition) {
948 isBooleanTakePlace = true;
952 if (isStringTakePlace) {
955 if (isBooleanTakePlace) {
956 if (isNumberTakePlace) {
964 private static ObjectNode buildXmlParameter(final SchemaNode node) {
965 final ObjectNode xml = JsonNodeFactory.instance.objectNode();
966 final QName qName = node.getQName();
967 xml.put(NAME_KEY, qName.getLocalName());
968 xml.put(NAMESPACE_KEY, qName.getNamespace().toString());
972 private static void putIfNonNull(final ObjectNode property, final String key, final Number number) {
973 if (key != null && number != null) {
974 if (number instanceof Double) {
975 property.put(key, (Double) number);
976 } else if (number instanceof Float) {
977 property.put(key, (Float) number);
978 } else if (number instanceof Integer) {
979 property.put(key, (Integer) number);
980 } else if (number instanceof Short) {
981 property.put(key, (Short) number);
982 } else if (number instanceof Long) {
983 property.put(key, (Long) number);
988 private static void putIfNonNull(final ObjectNode property, final String key, final String value) {
989 if (key != null && value != null) {
990 property.put(key, value);
994 private static void setRequiredIfNotEmpty(final ObjectNode node, final ArrayNode required) {
995 if (required.size() > 0) {
996 node.set(REQUIRED_KEY, required);
1000 private static void setDefaultValue(final ObjectNode property, final String value) {
1001 property.put(DEFAULT_KEY, value);
1004 private static void setDefaultValue(final ObjectNode property, final Integer value) {
1005 property.put(DEFAULT_KEY, value);
1008 private static void setDefaultValue(final ObjectNode property, final Long value) {
1009 property.put(DEFAULT_KEY, value);
1012 private static void setDefaultValue(final ObjectNode property, final BigDecimal value) {
1013 property.put(DEFAULT_KEY, value);
1016 private static void setDefaultValue(final ObjectNode property, final Boolean value) {
1017 property.put(DEFAULT_KEY, value);