Sahil Lele

ONOS-2485 Autogenerate swagger JSON files from WebResource classes

Change-Id: If3efcd22ce04b4579bf0d3359684b252d981913e
...@@ -51,6 +51,8 @@ ...@@ -51,6 +51,8 @@
51 <module>features</module> 51 <module>features</module>
52 <module>tools/package/archetypes</module> 52 <module>tools/package/archetypes</module>
53 <module>tools/package/branding</module> 53 <module>tools/package/branding</module>
54 + <!-- FIXME remove before release -->
55 + <module>tools/package/maven-plugin</module>
54 <module>ovsdb</module> 56 <module>ovsdb</module>
55 </modules> 57 </modules>
56 58
...@@ -94,6 +96,14 @@ ...@@ -94,6 +96,14 @@
94 </repository> 96 </repository>
95 </repositories> 97 </repositories>
96 98
99 + <!--- FIXME Needed for onos-maven-plugin. Remove before official release -->
100 + <pluginRepositories>
101 + <pluginRepository>
102 + <id>snapshots</id>
103 + <url>https://oss.sonatype.org/content/repositories/snapshots</url>
104 + </pluginRepository>
105 + </pluginRepositories>
106 +
97 <dependencyManagement> 107 <dependencyManagement>
98 <dependencies> 108 <dependencies>
99 <dependency> 109 <dependency>
...@@ -584,7 +594,7 @@ ...@@ -584,7 +594,7 @@
584 <plugin> 594 <plugin>
585 <groupId>org.onosproject</groupId> 595 <groupId>org.onosproject</groupId>
586 <artifactId>onos-maven-plugin</artifactId> 596 <artifactId>onos-maven-plugin</artifactId>
587 - <version>1.4</version> 597 + <version>1.5-SNAPSHOT</version>
588 <executions> 598 <executions>
589 <execution> 599 <execution>
590 <id>cfg</id> 600 <id>cfg</id>
...@@ -594,6 +604,13 @@ ...@@ -594,6 +604,13 @@
594 </goals> 604 </goals>
595 </execution> 605 </execution>
596 <execution> 606 <execution>
607 + <id>swagger</id>
608 + <phase>generate-resources</phase>
609 + <goals>
610 + <goal>swagger</goal>
611 + </goals>
612 + </execution>
613 + <execution>
597 <id>app</id> 614 <id>app</id>
598 <phase>package</phase> 615 <phase>package</phase>
599 <goals> 616 <goals>
......
...@@ -75,6 +75,16 @@ ...@@ -75,6 +75,16 @@
75 <version>3.4</version> 75 <version>3.4</version>
76 <scope>provided</scope> 76 <scope>provided</scope>
77 </dependency> 77 </dependency>
78 + <dependency>
79 + <groupId>com.fasterxml.jackson.core</groupId>
80 + <artifactId>jackson-databind</artifactId>
81 + <version>2.4.2</version>
82 + </dependency>
83 + <dependency>
84 + <groupId>com.fasterxml.jackson.core</groupId>
85 + <artifactId>jackson-annotations</artifactId>
86 + <version>2.4.2</version>
87 + </dependency>
78 </dependencies> 88 </dependencies>
79 89
80 <build> 90 <build>
......
1 +/*
2 + * Copyright 2015 Open Networking Laboratory
3 + *
4 + * Licensed under the Apache License, Version 2.0 (the "License");
5 + * you may not use this file except in compliance with the License.
6 + * You may obtain a copy of the License at
7 + *
8 + * http://www.apache.org/licenses/LICENSE-2.0
9 + *
10 + * Unless required by applicable law or agreed to in writing, software
11 + * distributed under the License is distributed on an "AS IS" BASIS,
12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 + * See the License for the specific language governing permissions and
14 + * limitations under the License.
15 + */
16 +package org.onosproject.maven;
17 +
18 +import com.fasterxml.jackson.databind.ObjectMapper;
19 +import com.fasterxml.jackson.databind.node.ArrayNode;
20 +import com.fasterxml.jackson.databind.node.ObjectNode;
21 +import com.thoughtworks.qdox.JavaProjectBuilder;
22 +import com.thoughtworks.qdox.model.DocletTag;
23 +import com.thoughtworks.qdox.model.JavaAnnotation;
24 +import com.thoughtworks.qdox.model.JavaClass;
25 +import com.thoughtworks.qdox.model.JavaMethod;
26 +import com.thoughtworks.qdox.model.JavaParameter;
27 +import com.thoughtworks.qdox.model.JavaType;
28 +import org.apache.maven.plugin.AbstractMojo;
29 +import org.apache.maven.plugin.MojoExecutionException;
30 +import org.apache.maven.plugins.annotations.LifecyclePhase;
31 +import org.apache.maven.plugins.annotations.Mojo;
32 +import org.apache.maven.plugins.annotations.Parameter;
33 +
34 +import java.io.File;
35 +import java.io.FileWriter;
36 +import java.io.IOException;
37 +import java.io.PrintWriter;
38 +import java.util.HashMap;
39 +import java.util.Map;
40 +import java.util.Optional;
41 +
42 +/**
43 + * Produces ONOS Swagger api-doc.
44 + */
45 +@Mojo(name = "swagger", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
46 +public class OnosSwaggerMojo extends AbstractMojo {
47 + private final ObjectMapper mapper = new ObjectMapper();
48 +
49 + private static final String PATH = "javax.ws.rs.Path";
50 + private static final String PATHPARAM = "javax.ws.rs.PathParam";
51 + private static final String QUERYPARAM = "javax.ws.rs.QueryParam";
52 + private static final String POST = "javax.ws.rs.POST";
53 + private static final String GET = "javax.ws.rs.GET";
54 + private static final String PUT = "javax.ws.rs.PUT";
55 + private static final String DELETE = "javax.ws.rs.DELETE";
56 + private static final String PRODUCES = "javax.ws.rs.Produces";
57 + private static final String CONSUMES = "javax.ws.rs.Consumes";
58 + private static final String JSON = "MediaType.APPLICATION_JSON";
59 +
60 + /**
61 + * The directory where the generated catalogue file will be put.
62 + */
63 + @Parameter(defaultValue = "${basedir}")
64 + protected File srcDirectory;
65 +
66 + /**
67 + * The directory where the generated catalogue file will be put.
68 + */
69 + @Parameter(defaultValue = "${project.build.outputDirectory}")
70 + protected File dstDirectory;
71 +
72 + @Override
73 + public void execute() throws MojoExecutionException {
74 + getLog().info("Generating ONOS REST api documentation...");
75 + try {
76 + JavaProjectBuilder builder = new JavaProjectBuilder();
77 + builder.addSourceTree(new File(srcDirectory, "src/main/java"));
78 +
79 + ObjectNode root = initializeRoot();
80 +
81 + ArrayNode tags = mapper.createArrayNode();
82 + root.set("tags", tags);
83 +
84 + ObjectNode paths = mapper.createObjectNode();
85 + root.set("paths", paths);
86 + builder.getClasses().forEach(javaClass -> {
87 + processClass(javaClass, paths, tags);
88 + //writeCatalog(root); // write out this api json file
89 + });
90 + writeCatalog(root); // write out this api json file
91 + } catch (Exception e) {
92 + e.printStackTrace();
93 + throw e;
94 + }
95 + }
96 +
97 + // initializes top level root with Swagger required specifications
98 + private ObjectNode initializeRoot() {
99 + ObjectNode root = mapper.createObjectNode();
100 + root.put("swagger", "2.0");
101 + ObjectNode info = mapper.createObjectNode();
102 + root.set("info", info);
103 + info.put("title", "ONOS API");
104 + info.put("description", "Move your networking forward with ONOS");
105 + info.put("version", "1.0.0");
106 +
107 + root.put("host", "http://localhost:8181/onos");
108 + root.put("basePath", "/v1");
109 +
110 + ArrayNode produces = mapper.createArrayNode();
111 + produces.add("application/json");
112 + root.set("produces", produces);
113 +
114 + ArrayNode consumes = mapper.createArrayNode();
115 + consumes.add("application/json");
116 + root.set("consumes", consumes);
117 +
118 + return root;
119 + }
120 +
121 + // Checks whether javaClass has a path tag associated with it and if it does
122 + // processes its methods and creates a tag for the class on the root
123 + void processClass(JavaClass javaClass, ObjectNode paths, ArrayNode tags) {
124 + Optional<JavaAnnotation> optional =
125 + javaClass.getAnnotations().stream().filter(a -> a.getType().getName().equals(PATH)).findAny();
126 + JavaAnnotation annotation = optional.isPresent() ? optional.get() : null;
127 + // if the class does not have a Path tag then ignore it
128 + if (annotation == null) {
129 + return;
130 + }
131 +
132 + String resourcePath = getPath(annotation), pathName = ""; //returns empty string if something goes wrong
133 +
134 + // creating tag for this class on the root
135 + ObjectNode tagObject = mapper.createObjectNode();
136 + if (resourcePath != null && resourcePath.length() > 1) {
137 + pathName = resourcePath.substring(1);
138 + tagObject.put("name", pathName); //tagObject.put("name", resourcePath.substring(1));
139 + }
140 + if (javaClass.getComment() != null) {
141 + tagObject.put("description", javaClass.getComment());
142 + }
143 + tags.add(tagObject);
144 +
145 + //creating tag to add to all methods from this class
146 + ArrayNode tagArray = mapper.createArrayNode();
147 + tagArray.add(pathName);
148 +
149 + processAllMethods(javaClass, resourcePath, paths, tagArray);
150 + }
151 +
152 + // Checks whether a class's methods are REST methods and then places all the
153 + // methods under a specific path into the paths node
154 + private void processAllMethods(JavaClass javaClass, String resourcePath,
155 + ObjectNode paths, ArrayNode tagArray) {
156 + // map of the path to its methods represented by an ObjectNode
157 + Map<String, ObjectNode> pathMap = new HashMap<>();
158 +
159 + javaClass.getMethods().forEach(javaMethod -> {
160 + javaMethod.getAnnotations().forEach(annotation -> {
161 + String name = annotation.getType().getName();
162 + if (name.equals(POST) || name.equals(GET) || name.equals(DELETE) || name.equals(PUT)) {
163 + // substring(12) removes "javax.ws.rs."
164 + String method = annotation.getType().toString().substring(12).toLowerCase();
165 + processRestMethod(javaMethod, method, pathMap, resourcePath, tagArray);
166 + }
167 + });
168 + });
169 +
170 + // for each path add its methods to the path node
171 + for (Map.Entry<String, ObjectNode> entry : pathMap.entrySet()) {
172 + paths.set(entry.getKey(), entry.getValue());
173 + }
174 +
175 +
176 + }
177 +
178 + private void processRestMethod(JavaMethod javaMethod, String method,
179 + Map<String, ObjectNode> pathMap, String resourcePath,
180 + ArrayNode tagArray) {
181 + String fullPath = resourcePath, consumes = "", produces = "",
182 + comment = javaMethod.getComment();
183 + for (JavaAnnotation annotation : javaMethod.getAnnotations()) {
184 + String name = annotation.getType().getName();
185 + if (name.equals(PATH)) {
186 + fullPath += getPath(annotation);
187 + }
188 + if (name.equals(CONSUMES)) {
189 + consumes = getIOType(annotation);
190 + }
191 + if (name.equals(PRODUCES)) {
192 + produces = getIOType(annotation);
193 + }
194 + }
195 + ObjectNode methodNode = mapper.createObjectNode();
196 + methodNode.set("tags", tagArray);
197 +
198 + addSummaryDescriptions(methodNode, comment);
199 + processParameters(javaMethod, methodNode);
200 +
201 + processConsumesProduces(methodNode, "consumes", consumes);
202 + processConsumesProduces(methodNode, "produces", produces);
203 +
204 + addResponses(methodNode);
205 +
206 + ObjectNode operations = pathMap.get(fullPath);
207 + if (operations == null) {
208 + operations = mapper.createObjectNode();
209 + operations.set(method, methodNode);
210 + pathMap.put(fullPath, operations);
211 + } else {
212 + operations.set(method, methodNode);
213 + }
214 + }
215 +
216 + private void processConsumesProduces(ObjectNode methodNode, String type, String io) {
217 + if (!io.equals("")) {
218 + ArrayNode array = mapper.createArrayNode();
219 + methodNode.set(type, array);
220 + array.add(io);
221 + }
222 + }
223 +
224 + private void addSummaryDescriptions(ObjectNode methodNode, String comment) {
225 + String summary = "", description;
226 + if (comment != null) {
227 + if (comment.contains(".")) {
228 + int periodIndex = comment.indexOf(".");
229 + summary = comment.substring(0, periodIndex);
230 + description = comment.length() > periodIndex + 1 ?
231 + comment.substring(periodIndex + 1).trim() : "";
232 + } else {
233 + description = comment;
234 + }
235 + methodNode.put("summary", summary);
236 + methodNode.put("description", description);
237 + }
238 + }
239 +
240 + // temporary solution to add responses to a method
241 + // TODO Provide annotations in the web resources for responses and parse them
242 + private void addResponses(ObjectNode methodNode) {
243 + ObjectNode responses = mapper.createObjectNode();
244 + methodNode.set("responses", responses);
245 +
246 + ObjectNode success = mapper.createObjectNode();
247 + success.put("description", "successful operation");
248 + responses.set("200", success);
249 +
250 + ObjectNode defaultObj = mapper.createObjectNode();
251 + defaultObj.put("description", "Unexpected error");
252 + responses.set("default", defaultObj);
253 + }
254 +
255 + // for now only checks if the annotations has a value of JSON and
256 + // returns the string that Swagger requires
257 + private String getIOType(JavaAnnotation annotation) {
258 + if (annotation.getNamedParameter("value").toString().equals(JSON)) {
259 + return "application/json";
260 + }
261 + return "";
262 + }
263 +
264 + // if the annotation has a Path tag, returns the value with a
265 + // preceding backslash, else returns empty string
266 + private String getPath(JavaAnnotation annotation) {
267 + String path = annotation.getNamedParameter("value").toString();
268 + if (path == null) {
269 + return "";
270 + }
271 + path = path.substring(1, path.length() - 1); // removing end quotes
272 + path = "/" + path;
273 + if (path.charAt(path.length()-1) == '/') {
274 + return path.substring(0, path.length() - 1);
275 + }
276 + return path;
277 + }
278 +
279 + // processes parameters of javaMethod and enters the proper key-values into the methodNode
280 + private void processParameters(JavaMethod javaMethod, ObjectNode methodNode) {
281 + ArrayNode parameters = mapper.createArrayNode();
282 + methodNode.set("parameters", parameters);
283 + boolean required = true;
284 +
285 + for (JavaParameter javaParameter: javaMethod.getParameters()) {
286 + ObjectNode individualParameterNode = mapper.createObjectNode();
287 + Optional<JavaAnnotation> optional = javaParameter.getAnnotations().stream().filter(
288 + annotation -> annotation.getType().getName().equals(PATHPARAM) ||
289 + annotation.getType().getName().equals(QUERYPARAM)).findAny();
290 + JavaAnnotation pathType = optional.isPresent() ? optional.get() : null;
291 +
292 + String annotationName = javaParameter.getName();
293 +
294 +
295 + if (pathType != null) { //the parameter is a path or query parameter
296 + individualParameterNode.put("name",
297 + pathType.getNamedParameter("value").toString().replace("\"", ""));
298 + if (pathType.getType().getName().equals(PATHPARAM)) {
299 + individualParameterNode.put("in", "path");
300 + } else if (pathType.getType().getName().equals(QUERYPARAM)) {
301 + individualParameterNode.put("in", "query");
302 + }
303 + individualParameterNode.put("type", getType(javaParameter.getType()));
304 + } else { // the parameter is a body parameter
305 + individualParameterNode.put("name", annotationName);
306 + individualParameterNode.put("in", "body");
307 +
308 + // TODO add actual hardcoded schemas and a type
309 + // body parameters must have a schema associated with them
310 + ArrayNode schema = mapper.createArrayNode();
311 + individualParameterNode.set("schema", schema);
312 + }
313 + for (DocletTag p : javaMethod.getTagsByName("param")) {
314 + if (p.getValue().contains(annotationName)) {
315 + try {
316 + String description = p.getValue().split(" ", 2)[1].trim();
317 + if (description.contains("optional")) {
318 + required = false;
319 + }
320 + individualParameterNode.put("description", description);
321 + } catch (Exception e) {
322 + e.printStackTrace();
323 + }
324 + }
325 + }
326 + individualParameterNode.put("required", required);
327 + parameters.add(individualParameterNode);
328 + }
329 + }
330 +
331 + // returns the Swagger specified strings for the type of a parameter
332 + private String getType(JavaType javaType) {
333 + String type = javaType.getFullyQualifiedName();
334 + String value;
335 + if (type.equals(String.class.getName())) {
336 + value = "string";
337 + } else if (type.equals("int")) {
338 + value = "integer";
339 + } else if (type.equals(boolean.class.getName())) {
340 + value = "boolean";
341 + } else if (type.equals(long.class.getName())) {
342 + value = "number";
343 + } else {
344 + value = "";
345 + }
346 + return value;
347 + }
348 +
349 + // Takes the top root node and prints it SwaggerConfigFile JSON file
350 + // at onos/web/api/target/classes/SwaggerConfig.
351 + private void writeCatalog(ObjectNode root) {
352 + File dir = new File(dstDirectory, "SwaggerConfig");
353 + //File dir = new File(dstDirectory, javaClass.getPackageName().replace('.', '/'));
354 + dir.mkdirs();
355 +
356 + File swaggerCfg = new File(dir, "SwaggerConfigFile" + ".json");
357 + try (FileWriter fw = new FileWriter(swaggerCfg);
358 + PrintWriter pw = new PrintWriter(fw)) {
359 + pw.println(root.toString());
360 + } catch (IOException e) {
361 + System.err.println("Unable to write catalog for ");
362 + e.printStackTrace();
363 + }
364 + }
365 +
366 + // Prints "nickname" based on method and path for a REST method
367 + // Useful while log debugging
368 + private String setNickname(String method, String path) {
369 + if (!path.equals("")) {
370 + return (method + path.replace('/', '_').replace("{","").replace("}","")).toLowerCase();
371 + } else {
372 + return method.toLowerCase();
373 + }
374 + }
375 +}
...@@ -59,4 +59,13 @@ ...@@ -59,4 +59,13 @@
59 <web.context>/onos/v1</web.context> 59 <web.context>/onos/v1</web.context>
60 </properties> 60 </properties>
61 61
62 + <build>
63 + <plugins>
64 + <plugin>
65 + <groupId>org.onosproject</groupId>
66 + <artifactId>onos-maven-plugin</artifactId>
67 + </plugin>
68 + </plugins>
69 + </build>
70 +
62 </project> 71 </project>
......
...@@ -240,9 +240,8 @@ public class NetworkConfigWebResource extends AbstractWebResource { ...@@ -240,9 +240,8 @@ public class NetworkConfigWebResource extends AbstractWebResource {
240 */ 240 */
241 @DELETE 241 @DELETE
242 @Path("{subjectKey}/{subject}") 242 @Path("{subjectKey}/{subject}")
243 - @Consumes(MediaType.APPLICATION_JSON)
244 @SuppressWarnings("unchecked") 243 @SuppressWarnings("unchecked")
245 - public Response upload(@PathParam("subjectKey") String subjectKey, 244 + public Response delete(@PathParam("subjectKey") String subjectKey,
246 @PathParam("subject") String subject) { 245 @PathParam("subject") String subject) {
247 NetworkConfigService service = get(NetworkConfigService.class); 246 NetworkConfigService service = get(NetworkConfigService.class);
248 Object s = service.getSubjectFactory(subjectKey).createSubject(subject); 247 Object s = service.getSubjectFactory(subjectKey).createSubject(subject);
...@@ -261,9 +260,8 @@ public class NetworkConfigWebResource extends AbstractWebResource { ...@@ -261,9 +260,8 @@ public class NetworkConfigWebResource extends AbstractWebResource {
261 */ 260 */
262 @DELETE 261 @DELETE
263 @Path("{subjectKey}/{subject}/{configKey}") 262 @Path("{subjectKey}/{subject}/{configKey}")
264 - @Consumes(MediaType.APPLICATION_JSON)
265 @SuppressWarnings("unchecked") 263 @SuppressWarnings("unchecked")
266 - public Response upload(@PathParam("subjectKey") String subjectKey, 264 + public Response delete(@PathParam("subjectKey") String subjectKey,
267 @PathParam("subject") String subject, 265 @PathParam("subject") String subject,
268 @PathParam("configKey") String configKey) { 266 @PathParam("configKey") String configKey) {
269 NetworkConfigService service = get(NetworkConfigService.class); 267 NetworkConfigService service = get(NetworkConfigService.class);
...@@ -279,9 +277,8 @@ public class NetworkConfigWebResource extends AbstractWebResource { ...@@ -279,9 +277,8 @@ public class NetworkConfigWebResource extends AbstractWebResource {
279 * @return empty response 277 * @return empty response
280 */ 278 */
281 @DELETE 279 @DELETE
282 - @Consumes(MediaType.APPLICATION_JSON)
283 @SuppressWarnings("unchecked") 280 @SuppressWarnings("unchecked")
284 - public Response upload() { 281 + public Response delete() {
285 NetworkConfigService service = get(NetworkConfigService.class); 282 NetworkConfigService service = get(NetworkConfigService.class);
286 service.getSubjectClasses().forEach(subjectClass -> { 283 service.getSubjectClasses().forEach(subjectClass -> {
287 service.getSubjects(subjectClass).forEach(subject -> { 284 service.getSubjects(subjectClass).forEach(subject -> {
...@@ -303,9 +300,8 @@ public class NetworkConfigWebResource extends AbstractWebResource { ...@@ -303,9 +300,8 @@ public class NetworkConfigWebResource extends AbstractWebResource {
303 */ 300 */
304 @DELETE 301 @DELETE
305 @Path("{subjectKey}/") 302 @Path("{subjectKey}/")
306 - @Consumes(MediaType.APPLICATION_JSON)
307 @SuppressWarnings("unchecked") 303 @SuppressWarnings("unchecked")
308 - public Response upload(@PathParam("subjectKey") String subjectKey) { 304 + public Response delete(@PathParam("subjectKey") String subjectKey) {
309 NetworkConfigService service = get(NetworkConfigService.class); 305 NetworkConfigService service = get(NetworkConfigService.class);
310 service.getSubjects(service.getSubjectFactory(subjectKey).getClass()).forEach(subject -> { 306 service.getSubjects(service.getSubjectFactory(subjectKey).getClass()).forEach(subject -> {
311 service.getConfigs(subject).forEach(config -> { 307 service.getConfigs(subject).forEach(config -> {
......