ONOS-2486 Adding swagger-based REST API documentation.
Change-Id: I237d973d73549ad30ddc638c1c201f024d344c70
diff --git a/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosSwaggerMojo.java b/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosSwaggerMojo.java
index 6803c82..ea84745 100644
--- a/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosSwaggerMojo.java
+++ b/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosSwaggerMojo.java
@@ -18,6 +18,8 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
import com.thoughtworks.qdox.JavaProjectBuilder;
import com.thoughtworks.qdox.model.DocletTag;
import com.thoughtworks.qdox.model.JavaAnnotation;
@@ -30,6 +32,7 @@
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
import java.io.File;
import java.io.FileWriter;
@@ -39,16 +42,22 @@
import java.util.Map;
import java.util.Optional;
+import static com.google.common.base.Strings.isNullOrEmpty;
+
/**
* Produces ONOS Swagger api-doc.
*/
-@Mojo(name = "swagger", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
+@Mojo(name = "swagger", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class OnosSwaggerMojo extends AbstractMojo {
private final ObjectMapper mapper = new ObjectMapper();
+ private static final String JSON_FILE = "swagger.json";
+ private static final String GEN_SRC = "generated-sources";
+ private static final String REG_SRC = "registrator.javat";
+
private static final String PATH = "javax.ws.rs.Path";
- private static final String PATHPARAM = "javax.ws.rs.PathParam";
- private static final String QUERYPARAM = "javax.ws.rs.QueryParam";
+ private static final String PATH_PARAM = "javax.ws.rs.PathParam";
+ private static final String QUERY_PARAM = "javax.ws.rs.QueryParam";
private static final String POST = "javax.ws.rs.POST";
private static final String GET = "javax.ws.rs.GET";
private static final String PUT = "javax.ws.rs.PUT";
@@ -66,30 +75,74 @@
/**
* The directory where the generated catalogue file will be put.
*/
- @Parameter(defaultValue = "${project.build.outputDirectory}")
+ @Parameter(defaultValue = "${project.build.directory}")
protected File dstDirectory;
+ /**
+ * REST API web-context
+ */
+ @Parameter(defaultValue = "${web.context}")
+ protected String webContext;
+
+ /**
+ * REST API version
+ */
+ @Parameter(defaultValue = "${api.version}")
+ protected String apiVersion;
+
+ /**
+ * REST API description
+ */
+ @Parameter(defaultValue = "${api.description}")
+ protected String apiDescription;
+
+ /**
+ * REST API title
+ */
+ @Parameter(defaultValue = "${api.title}")
+ protected String apiTitle;
+
+ /**
+ * REST API title
+ */
+ @Parameter(defaultValue = "${api.package}")
+ protected String apiPackage;
+
+ /**
+ * Maven project
+ */
+ @Parameter(defaultValue = "${project}")
+ protected MavenProject project;
+
+
@Override
public void execute() throws MojoExecutionException {
- getLog().info("Generating ONOS REST api documentation...");
try {
JavaProjectBuilder builder = new JavaProjectBuilder();
builder.addSourceTree(new File(srcDirectory, "src/main/java"));
ObjectNode root = initializeRoot();
-
ArrayNode tags = mapper.createArrayNode();
- root.set("tags", tags);
-
ObjectNode paths = mapper.createObjectNode();
+
+ root.set("tags", tags);
root.set("paths", paths);
- builder.getClasses().forEach(javaClass -> {
- processClass(javaClass, paths, tags);
- //writeCatalog(root); // write out this api json file
- });
- writeCatalog(root); // write out this api json file
+
+ builder.getClasses().forEach(jc -> processClass(jc, paths, tags));
+
+ if (paths.size() > 0) {
+ getLog().info("Generating ONOS REST API documentation...");
+ genCatalog(root);
+
+ if (!isNullOrEmpty(apiPackage)) {
+ genRegistrator();
+ }
+ }
+
+ project.addCompileSourceRoot(new File(dstDirectory, GEN_SRC).getPath());
+
} catch (Exception e) {
- e.printStackTrace();
+ getLog().warn("Unable to generate ONOS REST API documentation", e);
throw e;
}
}
@@ -100,12 +153,11 @@
root.put("swagger", "2.0");
ObjectNode info = mapper.createObjectNode();
root.set("info", info);
- info.put("title", "ONOS API");
- info.put("description", "Move your networking forward with ONOS");
- info.put("version", "1.0.0");
- root.put("host", "http://localhost:8181/onos");
- root.put("basePath", "/v1");
+ root.put("basePath", webContext);
+ info.put("version", apiVersion);
+ info.put("title", apiTitle);
+ info.put("description", apiDescription);
ArrayNode produces = mapper.createArrayNode();
produces.add("application/json");
@@ -121,34 +173,41 @@
// Checks whether javaClass has a path tag associated with it and if it does
// processes its methods and creates a tag for the class on the root
void processClass(JavaClass javaClass, ObjectNode paths, ArrayNode tags) {
- Optional<JavaAnnotation> optional =
- javaClass.getAnnotations().stream().filter(a -> a.getType().getName().equals(PATH)).findAny();
- JavaAnnotation annotation = optional.isPresent() ? optional.get() : null;
- // if the class does not have a Path tag then ignore it
+ // If the class does not have a Path tag then ignore it
+ JavaAnnotation annotation = getPathAnnotation(javaClass);
if (annotation == null) {
return;
}
- String resourcePath = getPath(annotation), pathName = ""; //returns empty string if something goes wrong
-
- // creating tag for this class on the root
- ObjectNode tagObject = mapper.createObjectNode();
- if (resourcePath != null && resourcePath.length() > 1) {
- pathName = resourcePath.substring(1);
- tagObject.put("name", pathName); //tagObject.put("name", resourcePath.substring(1));
+ String path = getPath(annotation);
+ if (path == null) {
+ return;
}
+
+ String resourcePath = "/" + path;
+ String tagPath = path.isEmpty() ? "/" : path;
+
+ // Create tag node for this class.
+ ObjectNode tagObject = mapper.createObjectNode();
+ tagObject.put("name", tagPath);
if (javaClass.getComment() != null) {
- tagObject.put("description", javaClass.getComment());
+ tagObject.put("description", shortText(javaClass.getComment()));
}
tags.add(tagObject);
- //creating tag to add to all methods from this class
+ // Create an array node add to all methods from this class.
ArrayNode tagArray = mapper.createArrayNode();
- tagArray.add(pathName);
+ tagArray.add(tagPath);
processAllMethods(javaClass, resourcePath, paths, tagArray);
}
+ private JavaAnnotation getPathAnnotation(JavaClass javaClass) {
+ Optional<JavaAnnotation> optional = javaClass.getAnnotations()
+ .stream().filter(a -> a.getType().getName().equals(PATH)).findAny();
+ return optional.isPresent() ? optional.get() : null;
+ }
+
// Checks whether a class's methods are REST methods and then places all the
// methods under a specific path into the paths node
private void processAllMethods(JavaClass javaClass, String resourcePath,
@@ -176,14 +235,15 @@
}
private void processRestMethod(JavaMethod javaMethod, String method,
- Map<String, ObjectNode> pathMap, String resourcePath,
- ArrayNode tagArray) {
+ Map<String, ObjectNode> pathMap,
+ String resourcePath, ArrayNode tagArray) {
String fullPath = resourcePath, consumes = "", produces = "",
comment = javaMethod.getComment();
for (JavaAnnotation annotation : javaMethod.getAnnotations()) {
String name = annotation.getType().getName();
if (name.equals(PATH)) {
- fullPath += getPath(annotation);
+ fullPath = resourcePath + "/" + getPath(annotation);
+ fullPath = fullPath.replaceFirst("^//", "/");
}
if (name.equals(CONSUMES)) {
consumes = getIOType(annotation);
@@ -237,7 +297,7 @@
}
}
- // temporary solution to add responses to a method
+ // Temporary solution to add responses to a method
// TODO Provide annotations in the web resources for responses and parse them
private void addResponses(ObjectNode methodNode) {
ObjectNode responses = mapper.createObjectNode();
@@ -252,8 +312,8 @@
responses.set("default", defaultObj);
}
- // for now only checks if the annotations has a value of JSON and
- // returns the string that Swagger requires
+ // Checks if the annotations has a value of JSON and returns the string
+ // that Swagger requires
private String getIOType(JavaAnnotation annotation) {
if (annotation.getNamedParameter("value").toString().equals(JSON)) {
return "application/json";
@@ -261,32 +321,24 @@
return "";
}
- // if the annotation has a Path tag, returns the value with a
- // preceding backslash, else returns empty string
+ // If the annotation has a Path tag, returns the value with leading and
+ // trailing double quotes and slash removed.
private String getPath(JavaAnnotation annotation) {
String path = annotation.getNamedParameter("value").toString();
- if (path == null) {
- return "";
- }
- path = path.substring(1, path.length() - 1); // removing end quotes
- path = "/" + path;
- if (path.charAt(path.length()-1) == '/') {
- return path.substring(0, path.length() - 1);
- }
- return path;
+ return path == null ? null : path.replaceAll("(^[\\\"/]*|[/\\\"]*$)", "");
}
- // processes parameters of javaMethod and enters the proper key-values into the methodNode
+ // Processes parameters of javaMethod and enters the proper key-values into the methodNode
private void processParameters(JavaMethod javaMethod, ObjectNode methodNode) {
ArrayNode parameters = mapper.createArrayNode();
methodNode.set("parameters", parameters);
boolean required = true;
- for (JavaParameter javaParameter: javaMethod.getParameters()) {
+ for (JavaParameter javaParameter : javaMethod.getParameters()) {
ObjectNode individualParameterNode = mapper.createObjectNode();
Optional<JavaAnnotation> optional = javaParameter.getAnnotations().stream().filter(
- annotation -> annotation.getType().getName().equals(PATHPARAM) ||
- annotation.getType().getName().equals(QUERYPARAM)).findAny();
+ annotation -> annotation.getType().getName().equals(PATH_PARAM) ||
+ annotation.getType().getName().equals(QUERY_PARAM)).findAny();
JavaAnnotation pathType = optional.isPresent() ? optional.get() : null;
String annotationName = javaParameter.getName();
@@ -295,9 +347,9 @@
if (pathType != null) { //the parameter is a path or query parameter
individualParameterNode.put("name",
pathType.getNamedParameter("value").toString().replace("\"", ""));
- if (pathType.getType().getName().equals(PATHPARAM)) {
+ if (pathType.getType().getName().equals(PATH_PARAM)) {
individualParameterNode.put("in", "path");
- } else if (pathType.getType().getName().equals(QUERYPARAM)) {
+ } else if (pathType.getType().getName().equals(QUERY_PARAM)) {
individualParameterNode.put("in", "query");
}
individualParameterNode.put("type", getType(javaParameter.getType()));
@@ -328,7 +380,7 @@
}
}
- // returns the Swagger specified strings for the type of a parameter
+ // Returns the Swagger specified strings for the type of a parameter
private String getType(JavaType javaType) {
String type = javaType.getFullyQualifiedName();
String value;
@@ -346,30 +398,54 @@
return value;
}
- // Takes the top root node and prints it SwaggerConfigFile JSON file
- // at onos/web/api/target/classes/SwaggerConfig.
- private void writeCatalog(ObjectNode root) {
- File dir = new File(dstDirectory, "SwaggerConfig");
- //File dir = new File(dstDirectory, javaClass.getPackageName().replace('.', '/'));
- dir.mkdirs();
-
- File swaggerCfg = new File(dir, "SwaggerConfigFile" + ".json");
- try (FileWriter fw = new FileWriter(swaggerCfg);
- PrintWriter pw = new PrintWriter(fw)) {
- pw.println(root.toString());
- } catch (IOException e) {
- System.err.println("Unable to write catalog for ");
- e.printStackTrace();
+ // Writes the swagger.json file using the supplied JSON root.
+ private void genCatalog(ObjectNode root) {
+ File swaggerCfg = new File(dstDirectory, JSON_FILE);
+ if (dstDirectory.exists() || dstDirectory.mkdirs()) {
+ try (FileWriter fw = new FileWriter(swaggerCfg);
+ PrintWriter pw = new PrintWriter(fw)) {
+ pw.println(root.toString());
+ } catch (IOException e) {
+ getLog().warn("Unable to write " + JSON_FILE);
+ }
+ } else {
+ getLog().warn("Unable to create " + dstDirectory);
}
}
- // Prints "nickname" based on method and path for a REST method
- // Useful while log debugging
+ // Generates the registrator Java component.
+ private void genRegistrator() {
+ File dir = new File(dstDirectory, GEN_SRC);
+ File reg = new File(dir, apiPackage.replaceAll("\\.", "/") + "/ApiDocRegistrator.java");
+ File pkg = reg.getParentFile();
+ if (pkg.exists() || pkg.mkdirs()) {
+ try {
+ String src = new String(ByteStreams.toByteArray(getClass().getResourceAsStream(REG_SRC)));
+ src = src.replace("${api.package}", apiPackage)
+ .replace("${web.context}", webContext)
+ .replace("${api.title}", apiTitle)
+ .replace("${api.description}", apiTitle);
+ Files.write(src.getBytes(), reg);
+ } catch (IOException e) {
+ getLog().warn("Unable to write " + reg);
+ }
+ } else {
+ getLog().warn("Unable to create " + reg);
+ }
+ }
+
+ // Returns "nickname" based on method and path for a REST method
private String setNickname(String method, String path) {
if (!path.equals("")) {
- return (method + path.replace('/', '_').replace("{","").replace("}","")).toLowerCase();
+ return (method + path.replace('/', '_').replace("{", "").replace("}", "")).toLowerCase();
} else {
return method.toLowerCase();
}
}
+
+ private String shortText(String comment) {
+ int i = comment.indexOf('.');
+ return i > 0 ? comment.substring(0, i) : comment;
+ }
+
}