com.sitewhere.web.rest.documentation.RestDocumentationGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.sitewhere.web.rest.documentation.RestDocumentationGenerator.java

Source

/*
 * Copyright (c) SiteWhere, LLC. All rights reserved. http://www.sitewhere.com
 *
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package com.sitewhere.web.rest.documentation;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;

import org.apache.commons.io.IOUtils;
import org.pegdown.PegDownProcessor;
import org.reflections.Reflections;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.sitewhere.common.MarshalUtils;
import com.sitewhere.server.SiteWhereServer;
import com.sitewhere.spi.SiteWhereException;
import com.sitewhere.spi.system.IVersion;
import com.sitewhere.version.VersionHelper;
import com.sitewhere.web.rest.annotations.Concerns;
import com.sitewhere.web.rest.annotations.Concerns.ConcernType;
import com.sitewhere.web.rest.annotations.Documented;
import com.sitewhere.web.rest.annotations.DocumentedController;
import com.sitewhere.web.rest.annotations.Example;
import com.sitewhere.web.rest.documentation.ParsedParameter.ParameterType;
import com.thoughtworks.paranamer.BytecodeReadingParanamer;
import com.thoughtworks.paranamer.Paranamer;
import com.wordnik.swagger.annotations.Api;
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiParam;

/**
 * Introspects the REST controllers to pull out documentation and generate it
 * into an HTML file.
 * 
 * @author Derek
 */
public class RestDocumentationGenerator {

    public static void main(String[] args) {
        if (args.length < 2) {
            throw new RuntimeException("Missing arguments needed to create REST documentation.");
        }

        System.out.println("Generating SiteWhere REST documentation...");
        try {
            // Required since some internal operations require a user to be
            // logged in.
            try {
                SecurityContextHolder.getContext().setAuthentication(SiteWhereServer.getSystemAuthentication());
            } catch (SiteWhereException e) {
                throw new RuntimeException("Unable to set system user.", e);
            }

            File resources = new File(args[0]);
            if (!resources.exists()) {
                throw new SiteWhereException("Unable to find REST documentation resources folder.");
            }
            List<ParsedController> controllers = parseControllers(resources);
            generateRestDocumentation(controllers, resources, args[1]);
        } catch (SiteWhereException e) {
            System.err.println("Unable to generate SiteWhere REST documentation.");
            e.printStackTrace(System.err);
        }
        System.out.println("Finished generating SiteWhere REST documentation...");
    }

    /**
     * Parse all controllers using the {@link DocumentedController} annotation.
     * 
     * @param resourcesFolder
     * @return
     * @throws SiteWhereException
     */
    protected static List<ParsedController> parseControllers(File resourcesFolder) throws SiteWhereException {
        Reflections reflections = new Reflections("com.sitewhere.web.rest.controllers");
        Set<Class<?>> controllers = reflections.getTypesAnnotatedWith(DocumentedController.class);
        List<ParsedController> results = new ArrayList<ParsedController>();
        for (Class<?> controller : controllers) {
            results.add(parseController(controller, resourcesFolder));
        }
        Collections.sort(results, new Comparator<ParsedController>() {

            @Override
            public int compare(ParsedController o1, ParsedController o2) {
                if (o1.isGlobal() && !o2.isGlobal()) {
                    return -1;
                } else if (!o1.isGlobal() && o2.isGlobal()) {
                    return 1;
                }
                return o1.getName().compareTo(o2.getName());
            }
        });
        int methodCount = 0;
        for (ParsedController controller : results) {
            methodCount += controller.getMethods().size();
        }
        System.out.println(
                "Found " + controllers.size() + " documented controllers containing " + methodCount + " methods.");
        return results;
    }

    /**
     * Generate documentation.
     * 
     * @param controllers
     * @param resourcesFolder
     * @param outputFolder
     * @throws SiteWhereException
     */
    protected static void generateRestDocumentation(List<ParsedController> controllers, File resourcesFolder,
            String outputFolder) throws SiteWhereException {
        System.out.println("Writing REST documentation to: " + outputFolder);

        File header = new File(resourcesFolder, "header.htm");
        if (!header.exists()) {
            throw new SiteWhereException("Unable to find header file: " + header.getAbsolutePath());
        }

        String completeDoc = readFile(header);
        completeDoc += generateNavigation(controllers);

        completeDoc += "<div class=\"col-md-8\">\n";
        for (ParsedController controller : controllers) {
            String controllerDoc = generateControllerDocumentation(controller);
            completeDoc += controllerDoc;
        }
        completeDoc += "</div>\n";

        File footer = new File(resourcesFolder, "footer.htm");
        if (!footer.exists()) {
            throw new SiteWhereException("Unable to find footer file: " + footer.getAbsolutePath());
        }

        IVersion version = VersionHelper.getVersion();
        completeDoc += readFile(footer).replace("${sitewhere.version}",
                "SiteWhere " + version.getVersionIdentifier() + " " + version.getEditionIdentifier());

        File output = new File(outputFolder);
        if (!output.exists()) {
            output.mkdir();
        }
        File single = new File(output, "single.html");
        try {
            FileOutputStream out = new FileOutputStream(single);
            out.write(completeDoc.getBytes());
            out.close();
        } catch (IOException e) {
            throw new SiteWhereException("Unable to write REST documentation file.", e);
        }
    }

    /**
     * Add the navigation panel.
     * 
     * @param controllers
     * @return
     */
    protected static String generateNavigation(List<ParsedController> controllers) {
        String html = "<nav id=\"rest-navigation\" class=\"col-md-4 bs-docs-sidenav\">\n";
        html += "<ul class=\"nav nav-list affix\">\n";

        boolean setActive = false;
        for (ParsedController controller : controllers) {
            String globalClass = (controller.isGlobal() ? " style=\"font-style: normal;font-weight: bold;\" " : "");
            html += "<li" + ((!setActive) ? " class=\"active\"" : "") + "><a href=\"#" + controller.getResource()
                    + "\"" + globalClass + "><i class=\"icon-chevron-right\"></i> " + controller.getName()
                    + "</a>\n";
            html += "<ul class=\"nav\">\n";
            for (ParsedMethod method : controller.getMethods()) {
                html += "<li><a href=\"#" + method.getName() + "\"><i class=\"icon-chevron-right\"></i> "
                        + method.getSummary() + "</a>\n";
            }
            html += "</ul>\n";
            html += "</li>";
            setActive = true;
        }

        html += "</ul>\n";
        html += "</nav>\n";
        return html;
    }

    /**
     * Generate documentation for a single controller.
     * 
     * @param controller
     * @return
     */
    protected static String generateControllerDocumentation(ParsedController controller) {
        String html = "<a id=\"" + controller.getResource() + "\">&nbsp;</a>\n";
        html += controller.getDescription();
        for (ParsedMethod method : controller.getMethods()) {
            RequestMethodColors colors = getRequestMethodColors(method);
            String methodHtml = "<a id=\"" + method.getName() + "\" style=\"display:block;\">&nbsp;</a>\n"
                    + method.getDescription();
            if (!controller.isGlobal()) {
                methodHtml += createUriBlock(method, colors) + "\n";

                MethodParameterBreakdown breakdown = MethodParameterBreakdown.parse(method);
                methodHtml += createParametersBlock(breakdown) + "\n";
                for (ParsedExample example : method.getExamples()) {
                    String exampleHtml = "";
                    if (example.getDescription() != null) {
                        exampleHtml += "<div>" + example.getDescription() + "</div>\n";
                    }
                    if (example.getJson() != null) {
                        exampleHtml += "<pre><code class='json'>" + example.getJson()
                                + "</code><span class=\"code-tag\">" + example.getStage().toString()
                                + "</span></pre>\n";
                    }
                    methodHtml += exampleHtml;
                }
            }
            html += methodHtml;
        }
        return html;
    }

    /**
     * Create block that displays REST service URI.
     * 
     * @param method
     * @param colors
     * @return
     */
    protected static String createUriBlock(ParsedMethod method, RequestMethodColors colors) {
        String uri = "<h3>Request URI</h3><div style=\"background-color: " + colors.getBgColor()
                + "; border: 1px solid " + colors.getBrdColor()
                + "; font-size: 13px; margin: 20px 0px;\"><span style=\"width: 70px; background-color: "
                + colors.getTagColor()
                + "; color: #fff; text-align: center; display: inline-block; margin-right: 15px; padding: 5px;\">"
                + colors.getTagName()
                + "</span><span style=\"width: 100%; font-family: courier; font-size: 12px; color: #000;\">"
                + method.getBaseUri() + method.getRelativeUri() + "</span></div>";

        int pathParamsCount = 0;
        String table = "<h3>Path Parameters</h3><table class=\"param-table\"><thead><tr style=\"background-color: "
                + colors.getTagColor() + "; color: #fff;\"><th>Name</th>" + "<th>Description</th></thead><tbody>";
        for (ParsedParameter param : method.getParameters()) {
            if (param.getType() == ParameterType.Path) {
                table += "<tr style=\"background-color: " + colors.getBgColor()
                        + "\"><td style=\"border: 1px solid " + colors.getBrdColor() + ";\">" + param.getName()
                        + "</td><td>" + param.getDescription() + "</td></tr>";
                pathParamsCount++;
            }
        }
        table += "</table>";

        if (pathParamsCount > 0) {
            uri += table;
        }

        return uri;
    }

    /**
     * Find colors associated with request method.
     * 
     * @param method
     * @return
     */
    protected static RequestMethodColors getRequestMethodColors(ParsedMethod method) {
        String tagName, tagColor, bgColor, brdColor;
        switch (method.getRequestMethod()) {
        case GET: {
            tagName = "GET";
            tagColor = "#10A54A";
            bgColor = "#E7F6EC";
            brdColor = "C3E8D1";
            break;
        }
        case POST: {
            tagName = "POST";
            tagColor = "#0F6AB4";
            bgColor = "#E7F0F7";
            brdColor = "#C3D9EC";
            break;
        }
        case PUT: {
            tagName = "PUT";
            tagColor = "#DDAA44";
            bgColor = "#F9F2E9";
            brdColor = "#F0E0CA";
            break;
        }
        case DELETE: {
            tagName = "DELETE";
            tagColor = "#A41E22";
            bgColor = "#F5E8E8";
            brdColor = "#E8C6C7";
            break;
        }
        default: {
            tagName = "???";
            tagColor = "#999";
            bgColor = "#ccc";
            brdColor = "#aaa";
            break;
        }
        }
        RequestMethodColors colors = new RequestMethodColors();
        colors.setTagName(tagName);
        colors.setTagColor(tagColor);
        colors.setBgColor(bgColor);
        colors.setBrdColor(brdColor);
        return colors;
    }

    /**
     * Holder object for colors associated with request method;
     * 
     * @author Derek
     */
    protected static class RequestMethodColors {
        private String tagName;
        private String tagColor;
        private String bgColor;
        private String brdColor;

        public String getTagName() {
            return tagName;
        }

        public void setTagName(String tagName) {
            this.tagName = tagName;
        }

        public String getTagColor() {
            return tagColor;
        }

        public void setTagColor(String tagColor) {
            this.tagColor = tagColor;
        }

        public String getBgColor() {
            return bgColor;
        }

        public void setBgColor(String bgColor) {
            this.bgColor = bgColor;
        }

        public String getBrdColor() {
            return brdColor;
        }

        public void setBrdColor(String brdColor) {
            this.brdColor = brdColor;
        }
    }

    /**
     * Create the table of parameters.
     * 
     * @param method
     * @return
     */
    protected static String createParametersBlock(MethodParameterBreakdown breakdown) {
        String block = "";
        if (breakdown.getNonConcernParameters().size() > 0) {
            String table = "<h3>Request Parameters</h3><table class=\"param-table\"><thead><tr><th style=\"width: 25%\">Name</th>"
                    + "<th style=\"width: 50%\">Description</th><th>Required</th></thead><tbody>";

            for (ParsedParameter param : breakdown.getNonConcernParameters()) {
                table += "<tr><td>" + param.getName() + "</td><td>" + param.getDescription() + "</td><td>"
                        + param.isRequired() + "</td></tr>";
            }

            table += "</tbody></table>";
            block += table;
        }
        for (ConcernType type : breakdown.getConcernParameters().keySet()) {
            List<ParsedParameter> params = breakdown.getConcernParameters().get(type);
            String table = "<h3><a href=\"#" + type.getLink() + "\">" + type.getTitle()
                    + "</a> Request Parameters</h3><table class=\"param-table\"><thead><tr><th style=\"width: 25%\">Name</th>"
                    + "<th style=\"width: 50%\">Description</th><th>Required</th></thead><tbody>";

            for (ParsedParameter param : params) {
                table += "<tr><td>" + param.getName() + "</td><td>" + param.getDescription() + "</td><td>"
                        + param.isRequired() + "</td></tr>";
            }

            table += "</tbody></table>";
            block += table;
        }
        return block;
    }

    /**
     * Parse information for a given controller.
     * 
     * @param controller
     * @param resourcesFolder
     * @return
     * @throws SiteWhereException
     */
    protected static ParsedController parseController(Class<?> controller, File resourcesFolder)
            throws SiteWhereException {
        ParsedController parsed = new ParsedController();

        Api api = controller.getAnnotation(Api.class);
        if (api == null) {
            throw new SiteWhereException("Swagger Api annotation missing on documented controller.");
        }
        parsed.setResource(api.value());

        DocumentedController doc = controller.getAnnotation(DocumentedController.class);
        parsed.setName(doc.name());
        parsed.setGlobal(doc.global());

        System.out.println("Processing controller: " + parsed.getName() + " (" + parsed.getResource() + ")");

        RequestMapping mapping = controller.getAnnotation(RequestMapping.class);
        if (mapping == null) {
            throw new SiteWhereException(
                    "Spring RequestMapping annotation missing on documented controller: " + controller.getName());
        }
        parsed.setBaseUri("/sitewhere/api" + mapping.value()[0]);

        // Verify controller markdown file.
        File markdownFile = new File(resourcesFolder, parsed.getResource() + ".md");
        if (!markdownFile.exists()) {
            throw new SiteWhereException("Controller markdown file missing: " + markdownFile.getAbsolutePath());
        }

        // Verify controller resources folder.
        File resources = new File(resourcesFolder, parsed.getResource());
        if (!resources.exists()) {
            throw new SiteWhereException("Controller markdown folder missing: " + resources.getAbsolutePath());
        }

        try {
            PegDownProcessor processor = new PegDownProcessor();
            String markdown = readFile(markdownFile);
            parsed.setDescription(processor.markdownToHtml(markdown));
        } catch (IOException e) {
            throw new SiteWhereException("Unable to read markdown from: " + markdownFile.getAbsolutePath(), e);
        }

        Method[] methods = controller.getMethods();
        for (Method method : methods) {
            if (method.getAnnotation(Documented.class) != null) {
                ParsedMethod parsedMethod = parseMethod(parsed.getBaseUri(), method, resources);
                parsed.getMethods().add(parsedMethod);
            }
        }
        Collections.sort(parsed.getMethods(), new Comparator<ParsedMethod>() {

            @Override
            public int compare(ParsedMethod o1, ParsedMethod o2) {
                return o1.getSummary().compareTo(o2.getSummary());
            }
        });
        return parsed;
    }

    /**
     * Generate documentation for a documented method.
     * 
     * @param baseUri
     * @param method
     * @return
     * @throws SiteWhereException
     */
    protected static ParsedMethod parseMethod(String baseUri, Method method, File resources)
            throws SiteWhereException {
        ParsedMethod parsed = new ParsedMethod();
        parsed.setName(method.getName());
        parsed.setBaseUri(baseUri);

        ApiOperation op = method.getAnnotation(ApiOperation.class);
        if (op == null) {
            throw new SiteWhereException(
                    "Spring ApiOperation annotation missing on documented method: " + method.getName());
        }
        parsed.setSummary(op.value());

        RequestMapping mapping = method.getAnnotation(RequestMapping.class);
        if (mapping == null) {
            throw new SiteWhereException(
                    "Spring RequestMapping annotation missing on documented method: " + method.getName());
        }

        // Find URI mapping.
        String[] mappings = mapping.value();
        parsed.setRelativeUri("/");
        if (mappings.length > 0) {
            parsed.setRelativeUri(mappings[0]);
        }

        // Find request method.
        RequestMethod[] methods = mapping.method();
        if (methods.length == 0) {
            throw new SiteWhereException("No request methods configured.");
        }
        parsed.setRequestMethod(methods[0]);

        Documented documented = method.getAnnotation(Documented.class);
        String markdownFilename = documented.description();
        if (markdownFilename.length() == 0) {
            markdownFilename = method.getName() + ".md";
        }

        // Parse method-level markdown description.
        File markdownFile = new File(resources, markdownFilename);
        if (!markdownFile.exists()) {
            throw new SiteWhereException("Method markdown file missing: " + markdownFile.getAbsolutePath());
        }
        PegDownProcessor processor = new PegDownProcessor();
        String markdown = readFile(markdownFile);
        parsed.setDescription(processor.markdownToHtml(markdown));

        // Parse parameters.
        List<ParsedParameter> params = parseParameters(method);
        Collections.sort(params, new Comparator<ParsedParameter>() {

            @Override
            public int compare(ParsedParameter o1, ParsedParameter o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });
        parsed.setParameters(params);

        parseExamples(method, parsed, resources);

        return parsed;
    }

    /**
     * Parse method parameters.
     * 
     * @param method
     * @return
     * @throws SiteWhereException
     */
    protected static List<ParsedParameter> parseParameters(Method method) throws SiteWhereException {
        List<ParsedParameter> parsed = new ArrayList<ParsedParameter>();

        Paranamer paranamer = new BytecodeReadingParanamer();
        String[] paramNames = paranamer.lookupParameterNames(method);

        if (paramNames.length > 0) {
            int i = 0;
            for (Annotation[] annotations : method.getParameterAnnotations()) {
                RequestParam request = null;
                PathVariable path = null;
                ApiParam api = null;
                Concerns concerns = null;
                for (Annotation annotation : annotations) {
                    if (annotation instanceof RequestParam) {
                        request = (RequestParam) annotation;
                    } else if (annotation instanceof PathVariable) {
                        path = (PathVariable) annotation;
                    } else if (annotation instanceof ApiParam) {
                        api = (ApiParam) annotation;
                    } else if (annotation instanceof Concerns) {
                        concerns = (Concerns) annotation;
                    }
                }
                if (request != null) {
                    ParsedParameter param = new ParsedParameter();
                    param.setType(ParameterType.Request);
                    param.setName(paramNames[i]);
                    param.setRequired(request.required());
                    if (api != null) {
                        param.setRequired(api.required());
                        param.setDescription(api.value());
                    }
                    if (concerns != null) {
                        param.getConcerns().addAll(Arrays.asList(concerns.values()));
                    }
                    parsed.add(param);
                } else if (path != null) {
                    ParsedParameter param = new ParsedParameter();
                    param.setType(ParameterType.Path);
                    param.setName(paramNames[i]);
                    param.setRequired(true);
                    if (api != null) {
                        param.setDescription(api.value());
                    }
                    if (concerns != null) {
                        param.getConcerns().addAll(Arrays.asList(concerns.values()));
                    }
                    parsed.add(param);
                }
                i++;
            }
        }

        return parsed;
    }

    /**
     * Parse examples for method.
     * 
     * @param method
     * @param parsed
     * @param resources
     * @throws SiteWhereException
     */
    protected static void parseExamples(Method method, ParsedMethod parsedMethod, File resources)
            throws SiteWhereException {
        Documented documented = method.getAnnotation(Documented.class);
        Example[] examples = documented.examples();
        File examplesResources = new File(resources, "examples");
        for (Example example : examples) {
            ParsedExample parsed = new ParsedExample();
            String mdFilename = example.description();
            if (mdFilename != null) {
                File markdownFile = new File(examplesResources, mdFilename);
                if (markdownFile.exists()) {
                    PegDownProcessor processor = new PegDownProcessor();
                    String markdown = readFile(markdownFile);
                    parsed.setDescription(processor.markdownToHtml(markdown));
                }
            }
            Class<?> jsonObj = example.json();
            if (jsonObj != null) {
                try {
                    Object instance = jsonObj.newInstance();
                    try {
                        Method generate = jsonObj.getMethod("generate");
                        instance = generate.invoke(instance);
                    } catch (NoSuchMethodException e) {
                        // Fall through if method is not implemented.
                    } catch (SecurityException e) {
                        throw new SiteWhereException("Error executing generator method.", e);
                    } catch (IllegalArgumentException e) {
                        throw new SiteWhereException("Error executing generator method.", e);
                    } catch (InvocationTargetException e) {
                        throw new SiteWhereException("Error executing generator method.", e);
                    }
                    String marshaled = MarshalUtils.marshalJsonAsPrettyString(instance);
                    parsed.setJson(marshaled);
                } catch (InstantiationException e) {
                    throw new SiteWhereException("Unable to create instance of JSON example object.", e);
                } catch (IllegalAccessException e) {
                    throw new SiteWhereException("Unable to create instance of JSON example object.", e);
                }
            }
            parsed.setStage(example.stage());
            parsedMethod.getExamples().add(parsed);
        }
    }

    /**
     * Read contents of a file into a String.
     * 
     * @param file
     * @return
     * @throws SiteWhereException
     */
    protected static String readFile(File file) throws SiteWhereException {
        try {
            FileInputStream in = new FileInputStream(file);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            IOUtils.copy(in, out);
            IOUtils.closeQuietly(in);
            IOUtils.closeQuietly(out);
            return out.toString();
        } catch (IOException e) {
            throw new SiteWhereException("Unable to read content from: " + file.getAbsolutePath());
        }
    }
}