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