blob: 6803c8277442ed5739a3ea5879937aa22a7b0c05 [file] [log] [blame]
Sahil Lele372d1f32015-07-31 15:01:41 -07001/*
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 */
16package org.onosproject.maven;
17
18import com.fasterxml.jackson.databind.ObjectMapper;
19import com.fasterxml.jackson.databind.node.ArrayNode;
20import com.fasterxml.jackson.databind.node.ObjectNode;
21import com.thoughtworks.qdox.JavaProjectBuilder;
22import com.thoughtworks.qdox.model.DocletTag;
23import com.thoughtworks.qdox.model.JavaAnnotation;
24import com.thoughtworks.qdox.model.JavaClass;
25import com.thoughtworks.qdox.model.JavaMethod;
26import com.thoughtworks.qdox.model.JavaParameter;
27import com.thoughtworks.qdox.model.JavaType;
28import org.apache.maven.plugin.AbstractMojo;
29import org.apache.maven.plugin.MojoExecutionException;
30import org.apache.maven.plugins.annotations.LifecyclePhase;
31import org.apache.maven.plugins.annotations.Mojo;
32import org.apache.maven.plugins.annotations.Parameter;
33
34import java.io.File;
35import java.io.FileWriter;
36import java.io.IOException;
37import java.io.PrintWriter;
38import java.util.HashMap;
39import java.util.Map;
40import java.util.Optional;
41
42/**
43 * Produces ONOS Swagger api-doc.
44 */
45@Mojo(name = "swagger", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
46public 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}