Bug 6679 - api explorer creates false examples
[netconf.git] / restconf / sal-rest-docgen / src / main / java / org / opendaylight / netconf / sal / rest / doc / impl / ModelGenerator.java
1 /*
2  * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.netconf.sal.rest.doc.impl;
9
10 import static org.opendaylight.netconf.sal.rest.doc.util.RestDocgenUtil.resolveNodesName;
11
12 import com.google.common.base.Preconditions;
13 import java.io.IOException;
14 import java.util.ArrayList;
15 import java.util.List;
16 import java.util.Set;
17 import javax.annotation.concurrent.NotThreadSafe;
18 import org.json.JSONArray;
19 import org.json.JSONException;
20 import org.json.JSONObject;
21 import org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder;
22 import org.opendaylight.netconf.sal.rest.doc.model.builder.OperationBuilder.Post;
23 import org.opendaylight.yangtools.yang.model.api.AnyXmlSchemaNode;
24 import org.opendaylight.yangtools.yang.model.api.ChoiceCaseNode;
25 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
26 import org.opendaylight.yangtools.yang.model.api.ConstraintDefinition;
27 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
28 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
29 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
30 import org.opendaylight.yangtools.yang.model.api.IdentitySchemaNode;
31 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
32 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
33 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
34 import org.opendaylight.yangtools.yang.model.api.Module;
35 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
36 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
37 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
39 import org.opendaylight.yangtools.yang.model.api.type.BinaryTypeDefinition;
40 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition;
41 import org.opendaylight.yangtools.yang.model.api.type.BitsTypeDefinition.Bit;
42 import org.opendaylight.yangtools.yang.model.api.type.BooleanTypeDefinition;
43 import org.opendaylight.yangtools.yang.model.api.type.DecimalTypeDefinition;
44 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition;
45 import org.opendaylight.yangtools.yang.model.api.type.EnumTypeDefinition.EnumPair;
46 import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
47 import org.opendaylight.yangtools.yang.model.api.type.IntegerTypeDefinition;
48 import org.opendaylight.yangtools.yang.model.api.type.LengthConstraint;
49 import org.opendaylight.yangtools.yang.model.api.type.StringTypeDefinition;
50 import org.opendaylight.yangtools.yang.model.api.type.UnionTypeDefinition;
51 import org.opendaylight.yangtools.yang.model.api.type.UnsignedIntegerTypeDefinition;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 /**
56  * Generates JSON Schema for data defined in YANG.
57  */
58 @NotThreadSafe
59 public class ModelGenerator {
60
61     private static final Logger LOG = LoggerFactory.getLogger(ModelGenerator.class);
62
63     private static final String BASE_64 = "base64";
64     private static final String BINARY_ENCODING_KEY = "binaryEncoding";
65     private static final String MEDIA_KEY = "media";
66     private static final String ONE_OF_KEY = "oneOf";
67     private static final String UNIQUE_ITEMS_KEY = "uniqueItems";
68     private static final String MAX_ITEMS = "maxItems";
69     private static final String MIN_ITEMS = "minItems";
70     private static final String SCHEMA_URL = "http://json-schema.org/draft-04/schema";
71     private static final String SCHEMA_KEY = "$schema";
72     private static final String MAX_LENGTH_KEY = "maxLength";
73     private static final String MIN_LENGTH_KEY = "minLength";
74     private static final String REQUIRED_KEY = "required";
75     private static final String REF_KEY = "$ref";
76     private static final String ITEMS_KEY = "items";
77     private static final String TYPE_KEY = "type";
78     private static final String PROPERTIES_KEY = "properties";
79     private static final String DESCRIPTION_KEY = "description";
80     private static final String OBJECT_TYPE = "object";
81     private static final String ARRAY_TYPE = "array";
82     private static final String ENUM = "enum";
83     private static final String INTEGER = "integer";
84     private static final String NUMBER = "number";
85     private static final String BOOLEAN = "boolean";
86     private static final String STRING = "string";
87     private static final String ID_KEY = "id";
88     private static final String SUB_TYPES_KEY = "subTypes";
89
90     private Module topLevelModule;
91
92     public ModelGenerator() {
93     }
94
95     private static String jsonTypeFor(final TypeDefinition<?> type) {
96         if (type instanceof BooleanTypeDefinition) {
97             return BOOLEAN;
98         } else if (type instanceof DecimalTypeDefinition) {
99             return NUMBER;
100         } else if (type instanceof EnumTypeDefinition) {
101             return ENUM;
102         } else if (type instanceof IntegerTypeDefinition) {
103             return INTEGER;
104         } else if (type instanceof UnsignedIntegerTypeDefinition) {
105             return INTEGER;
106         } else if (type instanceof StringTypeDefinition) {
107             return STRING;
108         }
109
110         // TODO: Binary type
111         return null;
112     }
113
114     public JSONObject convertToJsonSchema(final Module module, final SchemaContext schemaContext) throws IOException, JSONException {
115         final JSONObject models = new JSONObject();
116         topLevelModule = module;
117         processModules(module, models);
118         processContainersAndLists(module, models, schemaContext);
119         processRPCs(module, models, schemaContext);
120         processIdentities(module, models);
121         return models;
122     }
123
124     private void processModules(final Module module, final JSONObject models) throws JSONException {
125         createConcreteModelForPost(models, module.getName() + BaseYangSwaggerGenerator.MODULE_NAME_SUFFIX,
126                 createPropertiesForPost(module, module.getName()));
127     }
128
129     private void processContainersAndLists(final Module module, final JSONObject models, final SchemaContext schemaContext)
130             throws IOException, JSONException {
131
132         final String moduleName = module.getName();
133
134         for (final DataSchemaNode childNode : module.getChildNodes()) {
135             // For every container and list in the module
136             if (childNode instanceof ContainerSchemaNode || childNode instanceof ListSchemaNode) {
137                 processDataNodeContainer((DataNodeContainer) childNode, moduleName, models, true, schemaContext);
138                 processDataNodeContainer((DataNodeContainer) childNode, moduleName, models, false, schemaContext);
139             }
140         }
141     }
142
143     /**
144      * Process the RPCs for a Module Spits out a file each of the name <rpcName>-input.json and <rpcName>-output.json
145      * for each RPC that contains input & output elements
146      *
147      * @param module
148      * @throws JSONException
149      * @throws IOException
150      */
151     private void processRPCs(final Module module, final JSONObject models, final SchemaContext schemaContext) throws JSONException,
152             IOException {
153         final Set<RpcDefinition> rpcs = module.getRpcs();
154         final String moduleName = module.getName();
155         for (final RpcDefinition rpc : rpcs) {
156             final ContainerSchemaNode input = rpc.getInput();
157             if (input != null) {
158                 final JSONObject properties = processChildren(input.getChildNodes(), moduleName, models, true, schemaContext);
159
160                 final String filename = "(" + rpc.getQName().getLocalName() + ")input";
161                 final JSONObject childSchema = getSchemaTemplate();
162                 childSchema.put(TYPE_KEY, OBJECT_TYPE);
163                 childSchema.put(PROPERTIES_KEY, properties);
164                 childSchema.put("id", filename);
165                 models.put(filename, childSchema);
166
167                 processTopData(filename, models, input);
168             }
169
170             final ContainerSchemaNode output = rpc.getOutput();
171             if (output != null) {
172                 final JSONObject properties = processChildren(output.getChildNodes(), moduleName, models, true, schemaContext);
173                 final String filename = "(" + rpc.getQName().getLocalName() + ")output";
174                 final JSONObject childSchema = getSchemaTemplate();
175                 childSchema.put(TYPE_KEY, OBJECT_TYPE);
176                 childSchema.put(PROPERTIES_KEY, properties);
177                 childSchema.put("id", filename);
178                 models.put(filename, childSchema);
179
180                 processTopData(filename, models, output);
181             }
182         }
183     }
184
185     private static JSONObject processTopData(final String filename, final JSONObject models, final SchemaNode schemaNode) {
186         final JSONObject items = new JSONObject();
187
188         items.put(REF_KEY, filename);
189         final JSONObject dataNodeProperties = new JSONObject();
190         dataNodeProperties.put(TYPE_KEY, schemaNode instanceof ListSchemaNode ? ARRAY_TYPE : OBJECT_TYPE);
191         dataNodeProperties.put(ITEMS_KEY, items);
192
193         dataNodeProperties.putOpt(DESCRIPTION_KEY, schemaNode.getDescription());
194         final JSONObject properties = new JSONObject();
195         properties.put(schemaNode.getQName().getLocalName(), dataNodeProperties);
196         final JSONObject finalChildSchema = getSchemaTemplate();
197         finalChildSchema.put(TYPE_KEY, OBJECT_TYPE);
198         finalChildSchema.put(PROPERTIES_KEY, properties);
199         finalChildSchema.put(ID_KEY, filename + OperationBuilder.TOP);
200         models.put(filename + OperationBuilder.TOP, finalChildSchema);
201
202         return dataNodeProperties;
203     }
204
205     /**
206      * Processes the 'identity' statement in a yang model and maps it to a 'model' in the Swagger JSON spec.
207      *
208      * @param module
209      *            The module from which the identity stmt will be processed
210      * @param models
211      *            The JSONObject in which the parsed identity will be put as a 'model' obj
212      */
213     private static void processIdentities(final Module module, final JSONObject models) throws JSONException {
214
215         final String moduleName = module.getName();
216         final Set<IdentitySchemaNode> idNodes = module.getIdentities();
217         LOG.debug("Processing Identities for module {} . Found {} identity statements", moduleName, idNodes.size());
218
219         for (final IdentitySchemaNode idNode : idNodes) {
220             final JSONObject identityObj = new JSONObject();
221             final String identityName = idNode.getQName().getLocalName();
222             LOG.debug("Processing Identity: {}", identityName);
223
224             identityObj.put(ID_KEY, identityName);
225             identityObj.put(DESCRIPTION_KEY, idNode.getDescription());
226
227             final JSONObject props = new JSONObject();
228             final IdentitySchemaNode baseId = idNode.getBaseIdentity();
229
230             if (baseId == null) {
231                 /**
232                  * This is a base identity. So lets see if it has sub types. If it does, then add them to the model
233                  * definition.
234                  */
235                 final Set<IdentitySchemaNode> derivedIds = idNode.getDerivedIdentities();
236
237                 if (derivedIds != null) {
238                     final JSONArray subTypes = new JSONArray();
239                     for (final IdentitySchemaNode derivedId : derivedIds) {
240                         subTypes.put(derivedId.getQName().getLocalName());
241                     }
242                     identityObj.put(SUB_TYPES_KEY, subTypes);
243                 }
244             } else {
245                 /**
246                  * This is a derived entity. Add it's base type & move on.
247                  */
248                 props.put(TYPE_KEY, baseId.getQName().getLocalName());
249             }
250
251             // Add the properties. For a base type, this will be an empty object as required by the Swagger spec.
252             identityObj.put(PROPERTIES_KEY, props);
253             models.put(identityName, identityObj);
254         }
255     }
256
257     private JSONObject processDataNodeContainer(final DataNodeContainer dataNode, final String parentName, final JSONObject models,
258                                                 final boolean isConfig, final SchemaContext schemaContext) throws JSONException, IOException {
259         if (dataNode instanceof ListSchemaNode || dataNode instanceof ContainerSchemaNode) {
260             Preconditions.checkArgument(dataNode instanceof SchemaNode, "Data node should be also schema node");
261             final Iterable<DataSchemaNode> containerChildren = dataNode.getChildNodes();
262             final String localName = ((SchemaNode) dataNode).getQName().getLocalName();
263             final JSONObject properties = processChildren(containerChildren, parentName + "/" + localName, models, isConfig, schemaContext);
264             final String nodeName = parentName + (isConfig ? OperationBuilder.CONFIG : OperationBuilder.OPERATIONAL)
265                     + localName;
266
267             final JSONObject childSchema = getSchemaTemplate();
268             childSchema.put(TYPE_KEY, OBJECT_TYPE);
269             childSchema.put(PROPERTIES_KEY, properties);
270
271             childSchema.put("id", nodeName);
272             models.put(nodeName, childSchema);
273
274             if (isConfig) {
275                 createConcreteModelForPost(models, localName,
276                         createPropertiesForPost(dataNode, parentName + "/" + localName));
277             }
278
279             return processTopData(nodeName, models, (SchemaNode) dataNode);
280         }
281         return null;
282     }
283
284     private static void createConcreteModelForPost(final JSONObject models, final String localName,
285             final JSONObject properties) throws JSONException {
286         final String nodePostName = OperationBuilder.CONFIG + localName + Post.METHOD_NAME;
287         final JSONObject postSchema = getSchemaTemplate();
288         postSchema.put(TYPE_KEY, OBJECT_TYPE);
289         postSchema.put("id", nodePostName);
290         postSchema.put(PROPERTIES_KEY, properties);
291         models.put(nodePostName, postSchema);
292     }
293
294     private JSONObject createPropertiesForPost(final DataNodeContainer dataNodeContainer, final String parentName)
295             throws JSONException {
296         final JSONObject properties = new JSONObject();
297         for (final DataSchemaNode childNode : dataNodeContainer.getChildNodes()) {
298             if (childNode instanceof ListSchemaNode || childNode instanceof ContainerSchemaNode) {
299                 final JSONObject items = new JSONObject();
300                 items.put(REF_KEY, parentName + "(config)" + childNode.getQName().getLocalName());
301                 final JSONObject property = new JSONObject();
302                 property.put(TYPE_KEY, childNode instanceof ListSchemaNode ? ARRAY_TYPE : OBJECT_TYPE);
303                 property.put(ITEMS_KEY, items);
304                 properties.put(childNode.getQName().getLocalName(), property);
305             } else if (childNode instanceof LeafSchemaNode) {
306                 final JSONObject property = processLeafNode((LeafSchemaNode) childNode);
307                 properties.put(childNode.getQName().getLocalName(), property);
308             }
309         }
310         return properties;
311     }
312
313     private JSONObject processChildren(final Iterable<DataSchemaNode> nodes, final String moduleName,
314                                        final JSONObject models, final SchemaContext schemaContext) throws JSONException, IOException {
315         return processChildren(nodes, moduleName, models, true, schemaContext);
316     }
317
318     /**
319      * Processes the nodes.
320      */
321     private JSONObject processChildren(final Iterable<DataSchemaNode> nodes, final String parentName, final JSONObject models,
322                                        final boolean isConfig, final SchemaContext schemaContext)
323             throws JSONException, IOException {
324         final JSONObject properties = new JSONObject();
325         for (final DataSchemaNode node : nodes) {
326             if (node.isConfiguration() == isConfig) {
327                 final String name = resolveNodesName(node, topLevelModule, schemaContext);
328                 final JSONObject property;
329                 if (node instanceof LeafSchemaNode) {
330                     property = processLeafNode((LeafSchemaNode) node);
331
332                 } else if (node instanceof ListSchemaNode) {
333                     property = processDataNodeContainer((ListSchemaNode) node, parentName, models, isConfig,
334                             schemaContext);
335
336                 } else if (node instanceof LeafListSchemaNode) {
337                     property = processLeafListNode((LeafListSchemaNode) node);
338
339                 } else if (node instanceof ChoiceSchemaNode) {
340                     property = processChoiceNode((ChoiceSchemaNode) node, parentName, models, schemaContext);
341
342                 } else if (node instanceof AnyXmlSchemaNode) {
343                     property = processAnyXMLNode((AnyXmlSchemaNode) node);
344
345                 } else if (node instanceof ContainerSchemaNode) {
346                     property = processDataNodeContainer((ContainerSchemaNode) node, parentName, models, isConfig,
347                             schemaContext);
348
349                 } else {
350                     throw new IllegalArgumentException("Unknown DataSchemaNode type: " + node.getClass());
351                 }
352                 property.putOpt(DESCRIPTION_KEY, node.getDescription());
353                 properties.put(name, property);
354             }
355         }
356         return properties;
357     }
358
359     private JSONObject processLeafListNode(final LeafListSchemaNode listNode) throws JSONException {
360         final JSONObject props = new JSONObject();
361         props.put(TYPE_KEY, ARRAY_TYPE);
362
363         final JSONObject itemsVal = new JSONObject();
364         processTypeDef(listNode.getType(), itemsVal);
365         props.put(ITEMS_KEY, itemsVal);
366
367         final ConstraintDefinition constraints = listNode.getConstraints();
368         processConstraints(constraints, props);
369
370         return props;
371     }
372
373     private JSONObject processChoiceNode(final ChoiceSchemaNode choiceNode, final String parentName, final JSONObject models,
374                                          final SchemaContext schemaContext) throws JSONException, IOException {
375
376         final Set<ChoiceCaseNode> cases = choiceNode.getCases();
377
378         final JSONArray choiceProps = new JSONArray();
379         for (final ChoiceCaseNode choiceCase : cases) {
380             final String choiceName = choiceCase.getQName().getLocalName();
381             final JSONObject choiceProp = processChildren(choiceCase.getChildNodes(), parentName, models, schemaContext);
382             final JSONObject choiceObj = new JSONObject();
383             choiceObj.put(choiceName, choiceProp);
384             choiceObj.put(TYPE_KEY, OBJECT_TYPE);
385             choiceProps.put(choiceObj);
386         }
387
388         final JSONObject oneOfProps = new JSONObject();
389         oneOfProps.put(ONE_OF_KEY, choiceProps);
390         oneOfProps.put(TYPE_KEY, OBJECT_TYPE);
391
392         return oneOfProps;
393     }
394
395     private static void processConstraints(final ConstraintDefinition constraints, final JSONObject props) throws JSONException {
396         final boolean isMandatory = constraints.isMandatory();
397         props.put(REQUIRED_KEY, isMandatory);
398
399         final Integer minElements = constraints.getMinElements();
400         final Integer maxElements = constraints.getMaxElements();
401         if (minElements != null) {
402             props.put(MIN_ITEMS, minElements);
403         }
404         if (maxElements != null) {
405             props.put(MAX_ITEMS, maxElements);
406         }
407     }
408
409     private JSONObject processLeafNode(final LeafSchemaNode leafNode) throws JSONException {
410         final JSONObject property = new JSONObject();
411
412         final String leafDescription = leafNode.getDescription();
413         property.put(DESCRIPTION_KEY, leafDescription);
414
415         processConstraints(leafNode.getConstraints(), property);
416         processTypeDef(leafNode.getType(), property);
417
418         return property;
419     }
420
421     private static JSONObject processAnyXMLNode(final AnyXmlSchemaNode leafNode) throws JSONException {
422         final JSONObject property = new JSONObject();
423
424         final String leafDescription = leafNode.getDescription();
425         property.put(DESCRIPTION_KEY, leafDescription);
426
427         processConstraints(leafNode.getConstraints(), property);
428
429         return property;
430     }
431
432     private void processTypeDef(final TypeDefinition<?> leafTypeDef, final JSONObject property) throws JSONException {
433         if (leafTypeDef instanceof BinaryTypeDefinition) {
434             processBinaryType((BinaryTypeDefinition) leafTypeDef, property);
435         } else if (leafTypeDef instanceof BitsTypeDefinition) {
436             processBitsType((BitsTypeDefinition) leafTypeDef, property);
437         } else if (leafTypeDef instanceof EnumTypeDefinition) {
438             processEnumType((EnumTypeDefinition) leafTypeDef, property);
439         } else if (leafTypeDef instanceof IdentityrefTypeDefinition) {
440             property.putOpt(TYPE_KEY,
441                     ((IdentityrefTypeDefinition) leafTypeDef).getIdentity().getQName().getLocalName());
442         } else if (leafTypeDef instanceof StringTypeDefinition) {
443             processStringType((StringTypeDefinition) leafTypeDef, property);
444         } else if (leafTypeDef instanceof UnionTypeDefinition) {
445             processUnionType((UnionTypeDefinition) leafTypeDef, property);
446         } else {
447             String jsonType = jsonTypeFor(leafTypeDef);
448             if (jsonType == null) {
449                 jsonType = "object";
450             }
451             property.putOpt(TYPE_KEY, jsonType);
452         }
453     }
454
455     private static void processBinaryType(final BinaryTypeDefinition binaryType, final JSONObject property) throws JSONException {
456         property.put(TYPE_KEY, STRING);
457         final JSONObject media = new JSONObject();
458         media.put(BINARY_ENCODING_KEY, BASE_64);
459         property.put(MEDIA_KEY, media);
460     }
461
462     private static void processEnumType(final EnumTypeDefinition enumLeafType, final JSONObject property) throws JSONException {
463         final List<EnumPair> enumPairs = enumLeafType.getValues();
464         final List<String> enumNames = new ArrayList<>();
465         for (final EnumPair enumPair : enumPairs) {
466             enumNames.add(enumPair.getName());
467         }
468         property.putOpt(ENUM, new JSONArray(enumNames));
469     }
470
471     private static void processBitsType(final BitsTypeDefinition bitsType, final JSONObject property) throws JSONException {
472         property.put(TYPE_KEY, ARRAY_TYPE);
473         property.put(MIN_ITEMS, 0);
474         property.put(UNIQUE_ITEMS_KEY, true);
475         final JSONArray enumValues = new JSONArray();
476
477         final List<Bit> bits = bitsType.getBits();
478         for (final Bit bit : bits) {
479             enumValues.put(bit.getName());
480         }
481         final JSONObject itemsValue = new JSONObject();
482         itemsValue.put(ENUM, enumValues);
483         property.put(ITEMS_KEY, itemsValue);
484     }
485
486     private static void processStringType(final StringTypeDefinition stringType, final JSONObject property) throws JSONException {
487         StringTypeDefinition type = stringType;
488         List<LengthConstraint> lengthConstraints = stringType.getLengthConstraints();
489         while (lengthConstraints.isEmpty() && type.getBaseType() != null) {
490             type = type.getBaseType();
491             lengthConstraints = type.getLengthConstraints();
492         }
493
494         // FIXME: json-schema is not expressive enough to capture min/max laternatives. We should find the true minimum
495         //        and true maximum implied by the constraints and use that.
496         for (final LengthConstraint lengthConstraint : lengthConstraints) {
497             final Number min = lengthConstraint.getMin();
498             final Number max = lengthConstraint.getMax();
499             property.putOpt(MIN_LENGTH_KEY, min);
500             property.putOpt(MAX_LENGTH_KEY, max);
501         }
502
503         property.put(TYPE_KEY, STRING);
504     }
505
506     private static void processUnionType(final UnionTypeDefinition unionType, final JSONObject property) throws JSONException {
507         final StringBuilder type = new StringBuilder();
508         for (final TypeDefinition<?> typeDef : unionType.getTypes()) {
509             if (type.length() > 0) {
510                 type.append(" or ");
511             }
512             type.append(jsonTypeFor(typeDef));
513         }
514
515         property.put(TYPE_KEY, type);
516     }
517
518     /**
519      * Helper method to generate a pre-filled JSON schema object.
520      */
521     private static JSONObject getSchemaTemplate() throws JSONException {
522         final JSONObject schemaJSON = new JSONObject();
523         schemaJSON.put(SCHEMA_KEY, SCHEMA_URL);
524
525         return schemaJSON;
526     }
527
528 }