ONOS-2485 Autogenerate swagger JSON files from WebResource classes

Change-Id: If3efcd22ce04b4579bf0d3359684b252d981913e
diff --git a/pom.xml b/pom.xml
index f33f95b..4ea2f20 100644
--- a/pom.xml
+++ b/pom.xml
@@ -51,6 +51,8 @@
         <module>features</module>
         <module>tools/package/archetypes</module>
         <module>tools/package/branding</module>
+        <!-- FIXME remove before release -->
+        <module>tools/package/maven-plugin</module>
         <module>ovsdb</module>
     </modules>
 
@@ -94,6 +96,14 @@
         </repository>
     </repositories>
 
+    <!--- FIXME Needed for onos-maven-plugin. Remove before official release -->
+    <pluginRepositories>
+        <pluginRepository>
+            <id>snapshots</id>
+            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
+        </pluginRepository>
+    </pluginRepositories>
+
     <dependencyManagement>
         <dependencies>
             <dependency>
@@ -584,7 +594,7 @@
                 <plugin>
                     <groupId>org.onosproject</groupId>
                     <artifactId>onos-maven-plugin</artifactId>
-                    <version>1.4</version>
+                    <version>1.5-SNAPSHOT</version>
                     <executions>
                         <execution>
                             <id>cfg</id>
@@ -594,6 +604,13 @@
                             </goals>
                         </execution>
                         <execution>
+                            <id>swagger</id>
+                            <phase>generate-resources</phase>
+                            <goals>
+                                <goal>swagger</goal>
+                            </goals>
+                        </execution>
+                        <execution>
                             <id>app</id>
                             <phase>package</phase>
                             <goals>
diff --git a/tools/package/maven-plugin/pom.xml b/tools/package/maven-plugin/pom.xml
index fe53cba..c90860b 100644
--- a/tools/package/maven-plugin/pom.xml
+++ b/tools/package/maven-plugin/pom.xml
@@ -75,6 +75,16 @@
             <version>3.4</version>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.4.2</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+            <version>2.4.2</version>
+        </dependency>
     </dependencies>
 
     <build>
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
new file mode 100644
index 0000000..6803c82
--- /dev/null
+++ b/tools/package/maven-plugin/src/main/java/org/onosproject/maven/OnosSwaggerMojo.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright 2015 Open Networking Laboratory
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.maven;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.thoughtworks.qdox.JavaProjectBuilder;
+import com.thoughtworks.qdox.model.DocletTag;
+import com.thoughtworks.qdox.model.JavaAnnotation;
+import com.thoughtworks.qdox.model.JavaClass;
+import com.thoughtworks.qdox.model.JavaMethod;
+import com.thoughtworks.qdox.model.JavaParameter;
+import com.thoughtworks.qdox.model.JavaType;
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Produces ONOS Swagger api-doc.
+ */
+@Mojo(name = "swagger", defaultPhase = LifecyclePhase.GENERATE_RESOURCES)
+public class OnosSwaggerMojo extends AbstractMojo {
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    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 POST = "javax.ws.rs.POST";
+    private static final String GET = "javax.ws.rs.GET";
+    private static final String PUT = "javax.ws.rs.PUT";
+    private static final String DELETE = "javax.ws.rs.DELETE";
+    private static final String PRODUCES = "javax.ws.rs.Produces";
+    private static final String CONSUMES = "javax.ws.rs.Consumes";
+    private static final String JSON = "MediaType.APPLICATION_JSON";
+
+    /**
+     * The directory where the generated catalogue file will be put.
+     */
+    @Parameter(defaultValue = "${basedir}")
+    protected File srcDirectory;
+
+    /**
+     * The directory where the generated catalogue file will be put.
+     */
+    @Parameter(defaultValue = "${project.build.outputDirectory}")
+    protected File dstDirectory;
+
+    @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("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
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw e;
+        }
+    }
+
+    // initializes top level root with Swagger required specifications
+    private ObjectNode initializeRoot() {
+        ObjectNode root = mapper.createObjectNode();
+        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");
+
+        ArrayNode produces = mapper.createArrayNode();
+        produces.add("application/json");
+        root.set("produces", produces);
+
+        ArrayNode consumes = mapper.createArrayNode();
+        consumes.add("application/json");
+        root.set("consumes", consumes);
+
+        return root;
+    }
+
+    // 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 (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));
+        }
+        if (javaClass.getComment() != null) {
+            tagObject.put("description", javaClass.getComment());
+        }
+        tags.add(tagObject);
+
+        //creating tag to add to all methods from this class
+        ArrayNode tagArray = mapper.createArrayNode();
+        tagArray.add(pathName);
+
+        processAllMethods(javaClass, resourcePath, paths, tagArray);
+    }
+
+    // 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,
+                                   ObjectNode paths, ArrayNode tagArray) {
+        // map of the path to its methods represented by an ObjectNode
+        Map<String, ObjectNode> pathMap = new HashMap<>();
+
+        javaClass.getMethods().forEach(javaMethod -> {
+            javaMethod.getAnnotations().forEach(annotation -> {
+                String name = annotation.getType().getName();
+                if (name.equals(POST) || name.equals(GET) || name.equals(DELETE) || name.equals(PUT)) {
+                    // substring(12) removes "javax.ws.rs."
+                    String method = annotation.getType().toString().substring(12).toLowerCase();
+                    processRestMethod(javaMethod, method, pathMap, resourcePath, tagArray);
+                }
+            });
+        });
+
+        // for each path add its methods to the path node
+        for (Map.Entry<String, ObjectNode> entry : pathMap.entrySet()) {
+            paths.set(entry.getKey(), entry.getValue());
+        }
+
+
+    }
+
+    private void processRestMethod(JavaMethod javaMethod, String method,
+                                   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);
+            }
+            if (name.equals(CONSUMES)) {
+                consumes = getIOType(annotation);
+            }
+            if (name.equals(PRODUCES)) {
+                produces = getIOType(annotation);
+            }
+        }
+        ObjectNode methodNode = mapper.createObjectNode();
+        methodNode.set("tags", tagArray);
+
+        addSummaryDescriptions(methodNode, comment);
+        processParameters(javaMethod, methodNode);
+
+        processConsumesProduces(methodNode, "consumes", consumes);
+        processConsumesProduces(methodNode, "produces", produces);
+
+        addResponses(methodNode);
+
+        ObjectNode operations = pathMap.get(fullPath);
+        if (operations == null) {
+            operations = mapper.createObjectNode();
+            operations.set(method, methodNode);
+            pathMap.put(fullPath, operations);
+        } else {
+            operations.set(method, methodNode);
+        }
+    }
+
+    private void processConsumesProduces(ObjectNode methodNode, String type, String io) {
+        if (!io.equals("")) {
+            ArrayNode array = mapper.createArrayNode();
+            methodNode.set(type, array);
+            array.add(io);
+        }
+    }
+
+    private void addSummaryDescriptions(ObjectNode methodNode, String comment) {
+        String summary = "", description;
+        if (comment != null) {
+            if (comment.contains(".")) {
+                int periodIndex = comment.indexOf(".");
+                summary = comment.substring(0, periodIndex);
+                description = comment.length() > periodIndex + 1 ?
+                        comment.substring(periodIndex + 1).trim() : "";
+            } else {
+                description = comment;
+            }
+            methodNode.put("summary", summary);
+            methodNode.put("description", description);
+        }
+    }
+
+    // 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();
+        methodNode.set("responses", responses);
+
+        ObjectNode success = mapper.createObjectNode();
+        success.put("description", "successful operation");
+        responses.set("200", success);
+
+        ObjectNode defaultObj = mapper.createObjectNode();
+        defaultObj.put("description", "Unexpected error");
+        responses.set("default", defaultObj);
+    }
+
+    // for now only 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";
+        }
+        return "";
+    }
+
+    // if the annotation has a Path tag, returns the value with a
+    // preceding backslash, else returns empty string
+    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;
+    }
+
+    // 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()) {
+            ObjectNode individualParameterNode = mapper.createObjectNode();
+            Optional<JavaAnnotation> optional = javaParameter.getAnnotations().stream().filter(
+                    annotation -> annotation.getType().getName().equals(PATHPARAM) ||
+                            annotation.getType().getName().equals(QUERYPARAM)).findAny();
+            JavaAnnotation pathType = optional.isPresent() ? optional.get() : null;
+
+            String annotationName = javaParameter.getName();
+
+
+            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)) {
+                    individualParameterNode.put("in", "path");
+                } else if (pathType.getType().getName().equals(QUERYPARAM)) {
+                    individualParameterNode.put("in", "query");
+                }
+                individualParameterNode.put("type", getType(javaParameter.getType()));
+            } else { // the parameter is a body parameter
+                individualParameterNode.put("name", annotationName);
+                individualParameterNode.put("in", "body");
+
+                // TODO add actual hardcoded schemas and a type
+                // body parameters must have a schema associated with them
+                ArrayNode schema = mapper.createArrayNode();
+                individualParameterNode.set("schema", schema);
+            }
+            for (DocletTag p : javaMethod.getTagsByName("param")) {
+                if (p.getValue().contains(annotationName)) {
+                    try {
+                        String description = p.getValue().split(" ", 2)[1].trim();
+                        if (description.contains("optional")) {
+                            required = false;
+                        }
+                        individualParameterNode.put("description", description);
+                    } catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+            individualParameterNode.put("required", required);
+            parameters.add(individualParameterNode);
+        }
+    }
+
+    // returns the Swagger specified strings for the type of a parameter
+    private String getType(JavaType javaType) {
+        String type = javaType.getFullyQualifiedName();
+        String value;
+        if (type.equals(String.class.getName())) {
+            value = "string";
+        } else if (type.equals("int")) {
+            value = "integer";
+        } else if (type.equals(boolean.class.getName())) {
+            value = "boolean";
+        } else if (type.equals(long.class.getName())) {
+            value = "number";
+        } else {
+            value = "";
+        }
+        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();
+        }
+    }
+
+    // Prints "nickname" based on method and path for a REST method
+    // Useful while log debugging
+    private String setNickname(String method, String path) {
+        if (!path.equals("")) {
+            return (method + path.replace('/', '_').replace("{","").replace("}","")).toLowerCase();
+        } else {
+            return method.toLowerCase();
+        }
+    }
+}
diff --git a/web/api/pom.xml b/web/api/pom.xml
index 56e9b47..7f56c7b 100644
--- a/web/api/pom.xml
+++ b/web/api/pom.xml
@@ -59,4 +59,13 @@
         <web.context>/onos/v1</web.context>
     </properties>
 
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.onosproject</groupId>
+                <artifactId>onos-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
 </project>
diff --git a/web/api/src/main/java/org/onosproject/rest/resources/NetworkConfigWebResource.java b/web/api/src/main/java/org/onosproject/rest/resources/NetworkConfigWebResource.java
index 2dc778ba..4e2c790 100644
--- a/web/api/src/main/java/org/onosproject/rest/resources/NetworkConfigWebResource.java
+++ b/web/api/src/main/java/org/onosproject/rest/resources/NetworkConfigWebResource.java
@@ -240,9 +240,8 @@
      */
     @DELETE
     @Path("{subjectKey}/{subject}")
-    @Consumes(MediaType.APPLICATION_JSON)
     @SuppressWarnings("unchecked")
-    public Response upload(@PathParam("subjectKey") String subjectKey,
+    public Response delete(@PathParam("subjectKey") String subjectKey,
                            @PathParam("subject") String subject) {
         NetworkConfigService service = get(NetworkConfigService.class);
         Object s = service.getSubjectFactory(subjectKey).createSubject(subject);
@@ -261,9 +260,8 @@
      */
     @DELETE
     @Path("{subjectKey}/{subject}/{configKey}")
-    @Consumes(MediaType.APPLICATION_JSON)
     @SuppressWarnings("unchecked")
-    public Response upload(@PathParam("subjectKey") String subjectKey,
+    public Response delete(@PathParam("subjectKey") String subjectKey,
                            @PathParam("subject") String subject,
                            @PathParam("configKey") String configKey) {
         NetworkConfigService service = get(NetworkConfigService.class);
@@ -279,9 +277,8 @@
      * @return empty response
      */
     @DELETE
-    @Consumes(MediaType.APPLICATION_JSON)
     @SuppressWarnings("unchecked")
-    public Response upload() {
+    public Response delete() {
         NetworkConfigService service = get(NetworkConfigService.class);
         service.getSubjectClasses().forEach(subjectClass -> {
             service.getSubjects(subjectClass).forEach(subject -> {
@@ -303,9 +300,8 @@
      */
     @DELETE
     @Path("{subjectKey}/")
-    @Consumes(MediaType.APPLICATION_JSON)
     @SuppressWarnings("unchecked")
-    public Response upload(@PathParam("subjectKey") String subjectKey) {
+    public Response delete(@PathParam("subjectKey") String subjectKey) {
         NetworkConfigService service = get(NetworkConfigService.class);
         service.getSubjects(service.getSubjectFactory(subjectKey).getClass()).forEach(subject -> {
             service.getConfigs(subject).forEach(config -> {