org.rakam.client.builder.document.SlateDocumentGenerator.java Source code

Java tutorial

Introduction

Here is the source code for org.rakam.client.builder.document.SlateDocumentGenerator.java

Source

package org.rakam.client.builder.document;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.google.common.base.CaseFormat;
import com.google.common.base.Throwables;
import com.google.common.collect.*;
import com.google.common.io.Resources;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template;
import io.github.robwin.markup.builder.markdown.MarkdownBuilder;
import io.swagger.codegen.ClientOptInput;
import io.swagger.codegen.CodegenConfig;
import io.swagger.codegen.CodegenOperation;
import io.swagger.codegen.DefaultGenerator;
import io.swagger.codegen.config.CodegenConfigurator;
import io.swagger.models.*;
import io.swagger.models.parameters.AbstractSerializableParameter;
import io.swagger.models.parameters.BodyParameter;
import io.swagger.models.parameters.FormParameter;
import io.swagger.models.parameters.Parameter;
import io.swagger.models.properties.*;
import org.apache.commons.lang3.StringUtils;
import org.rakam.client.utils.ParameterUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.immutableEntry;
import static java.lang.String.format;
import static org.rakam.client.utils.PropertyUtils.getType;

public class SlateDocumentGenerator {
    private static final Logger LOGGER = LoggerFactory.getLogger(SlateDocumentGenerator.class);
    private static final List supportedLanguages = ImmutableList.builder().add("java").add("python").add("php")
            .build();
    private static final ObjectMapper mapper = new ObjectMapper();
    private static final String TERMS_OF_SERVICE = "Terms of service: ";
    private static final String URI_SCHEME = "URI scheme";
    private static final String HOST = "Host: ";
    private static final String BASE_PATH = "BasePath: ";
    private static final String SCHEMES = "Schemes: ";
    public static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("<<([^>]+)>>");
    private final MarkdownBuilder markdownBuilder;
    private List<CodegenConfigurator> configurators = null;
    private Swagger swagger;
    private Set<String> definitions;
    private List<String> tagsOrder;

    private Map<OperationIdentifier, Map<String, String>> templates;

    @SuppressWarnings("unchecked")
    private Map<String, Supplier<String>> contentSuppliers = Lists.<Map.Entry<String, Supplier<String>>>newArrayList(
            immutableEntry("swagger_api_version", () -> swagger.getInfo().getVersion()),
            immutableEntry("languages",
                    () -> configurators.stream().map(CodegenConfigurator::getLang).map(lang -> "  - " + lang)
                            .reduce((x, y) -> x + "\n" + y).orElse("")),
            immutableEntry("swagger_base_path", () -> swagger.getBasePath()),
            immutableEntry("swagger_schemes",
                    () -> swagger.getSchemes().stream().map(Scheme::toString).reduce((x, y) -> x + ", " + y)
                            .orElse("")),
            immutableEntry("swagger_tags", () -> generateApiTags(new MarkdownBuilder()).toString()),
            immutableEntry("swagger_definitions", this::generateDefinitions)).stream()
            .collect(Collectors.toMap(Entry::getKey, Entry::getValue));

    public SlateDocumentGenerator(ImmutableList<CodegenConfigurator> configurators, String tagsOrder) {
        this.configurators = configurators;
        markdownBuilder = new MarkdownBuilder();
        definitions = new HashSet<>();
        this.tagsOrder = tagsOrder == null || tagsOrder.trim().isEmpty() ? null
                : Arrays.asList(tagsOrder.split("\\s*,\\s*"));
    }

    public MarkdownBuilder build() throws IOException {
        markdownBuilder.textLine("---");
        markdownBuilder.textLine("title: API Reference");
        markdownBuilder.textLine("language_tabs:");
        markdownBuilder.textLine("  - shell");
        configurators.stream().map(c -> c.getLang()).forEach(lang -> markdownBuilder.textLine("  - " + lang));

        markdownBuilder.textLine("toc_footers:");
        markdownBuilder.textLine(" - <a href='#'>Sign Up for a Developer Key</a>");

        markdownBuilder.textLine("includes:").textLine("    - errors");
        markdownBuilder.textLine("search: true");
        markdownBuilder.textLine("---");

        this.templates = generateExampleUsages();
        buildSlateDocument();
        new DefinitionsDocument(this.swagger, markdownBuilder).process(definitions);
        return markdownBuilder;
    }

    private String generateDefinitions() {
        MarkdownBuilder builder = new MarkdownBuilder();
        try {
            new DefinitionsDocument(swagger, builder).process(definitions);
        } catch (IOException e) {
            System.out.println("failed generating definitions:" + e.toString());
            e.printStackTrace();
            return "";
        }
        return builder.toString();
    }

    public void generateTo(InputStream template, OutputStream dest) throws IOException {
        this.templates = generateExampleUsages();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(template));
                PrintWriter writer = new PrintWriter(new OutputStreamWriter(dest))) {
            String line;
            while ((line = reader.readLine()) != null) {
                Matcher matcher = PLACEHOLDER_PATTERN.matcher(line);
                StringBuffer buffer = new StringBuffer();
                while (matcher.find()) {
                    String key = matcher.group(1);
                    System.out.println("replacing:" + key);
                    matcher.appendReplacement(buffer, generateContent(key));
                }
                matcher.appendTail(buffer);
                writer.println(buffer);
            }
        } catch (Exception e) {
            System.out.println("failed generating slate:" + e);
            e.printStackTrace();
        }
    }

    private String generateContent(String key) {
        Supplier<String> supplier = contentSuppliers.get(key);
        if (supplier == null) {
            System.out.println("template key " + key + " not supported!");
            return "";
        }
        return supplier.get();
    }

    private void buildSlateDocument() {
        Info info = swagger.getInfo();

        markdownBuilder.documentTitle("Introduction");

        markdownBuilder.textLine("We have language bindings in "
                + configurators.stream().map(c -> c.getLang()).collect(Collectors.joining(", "))
                + "! You can view code examples in the dark area to the right, and you can switch the programming language of the examples with the tabs in the top right.");

        if (info.getDescription() != null) {
            markdownBuilder.textLine(info.getDescription());
            markdownBuilder.newLine();
        }

        if (StringUtils.isNotBlank(info.getVersion())) {
            markdownBuilder.sectionTitleLevel2("Version");
            markdownBuilder.textLine("Version: " + info.getVersion());
            markdownBuilder.newLine();
        }

        Contact contact = info.getContact();
        if (contact != null) {
            markdownBuilder.sectionTitleLevel1("Contact Information");
            if (StringUtils.isNotBlank(contact.getName())) {
                markdownBuilder.textLine("Contact: " + contact.getName());
            }
            if (StringUtils.isNotBlank(contact.getEmail())) {
                markdownBuilder.textLine("Email: " + contact.getEmail());
            }
            markdownBuilder.newLine();
        }

        License license = info.getLicense();
        if (license != null
                && (StringUtils.isNotBlank(license.getName()) || StringUtils.isNotBlank(license.getUrl()))) {
            markdownBuilder.sectionTitleLevel2("License");
            if (StringUtils.isNotBlank(license.getName())) {
                markdownBuilder.textLine("License: " + license.getName()).newLine();
            }
            if (StringUtils.isNotBlank(license.getUrl())) {
                markdownBuilder.textLine("License url: " + license.getUrl());
            }
            markdownBuilder.newLine();
        }

        if (StringUtils.isNotBlank(info.getTermsOfService())) {
            markdownBuilder.textLine(TERMS_OF_SERVICE + info.getTermsOfService());
            markdownBuilder.newLine();
        }

        if (StringUtils.isNotBlank(swagger.getHost()) || StringUtils.isNotBlank(swagger.getBasePath())) {
            markdownBuilder.sectionTitleLevel2(URI_SCHEME);
            if (StringUtils.isNotBlank(swagger.getHost())) {
                markdownBuilder.textLine(HOST + swagger.getHost());
            }
            if (StringUtils.isNotBlank(swagger.getBasePath())) {
                markdownBuilder.textLine(BASE_PATH + swagger.getBasePath());
            }
            if (swagger.getSchemes() != null && !swagger.getSchemes().isEmpty()) {
                List<String> schemes = swagger.getSchemes().stream().map(Scheme::toString)
                        .collect(Collectors.toList());
                markdownBuilder.textLine(SCHEMES + StringUtils.join(schemes, ", "));
            }
            markdownBuilder.newLine();
        }

        generateApiTags(markdownBuilder);
    }

    private MarkdownBuilder generateApiTags(MarkdownBuilder markdownBuilder) {
        if (!swagger.getTags().isEmpty()) {
            Map<String, Tag> tags = swagger.getTags().stream()
                    .collect(Collectors.toMap(t -> t.getName().toLowerCase(), Function.identity()));
            Set<String> nonOrderedTags = new HashSet<>(tags.keySet());
            nonOrderedTags.removeAll(tagsOrder);
            Stream.concat(tagsOrder.stream(), nonOrderedTags.stream()).forEachOrdered(tagName -> {
                Tag tag = tags.get(tagName.toLowerCase());
                if (tag == null) {
                    LOGGER.warn("tag not found:" + tagName);
                } else {
                    String name = tag.getName();
                    String description = tag.getDescription();
                    markdownBuilder.documentTitle(
                            CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, name.replaceAll("-", " ")))
                            .newLine().textLine(description).newLine();
                    processOperation(markdownBuilder, name);
                }
            });
            markdownBuilder.newLine();
        }
        return markdownBuilder;
    }

    private void processOperation(MarkdownBuilder markdownBuilder, String path, String method,
            Operation operation) {
        try {
            markdownBuilder.sectionTitleLevel1(operation.getSummary());

            StringBuilder builder = new StringBuilder();
            builder.append("curl ").append('"').append(swagger.getHost() == null ? "" : swagger.getHost())
                    .append(path).append('"');
            if (operation.getSecurity() != null) {
                for (Map<String, List<String>> map : operation.getSecurity()) {
                    for (Entry<String, List<String>> entry : map.entrySet()) {
                        builder.append(" -H \"" + entry.getKey() + ": my" + entry.getKey() + '"');
                    }
                }
            }

            builder.append(" -X " + method);
            if (operation.getParameters().stream()
                    .anyMatch(p -> p instanceof FormParameter || p instanceof BodyParameter)) {
                builder.append(" -d @- << EOF \n" + toExampleJsonParameters(operation) + "\nEOF");
            }

            markdownBuilder.source(builder.toString(), "shell");

            for (Entry<String, String> entry : templates.get(new OperationIdentifier(path, method)).entrySet()) {
                markdownBuilder.source(entry.getValue(), entry.getKey());
            }

            // TODO: response object also have example property
            Response response = operation.getResponses().get("200");
            if (response != null) {
                markdownBuilder.textLine("> The above command returns JSON structured like this:").newLine();
                if (response.getSchema() == null) {
                    LOGGER.warn("missing response schema for :" + operation.getOperationId());
                }
                Object example = response.getSchema().getExample();
                if (example != null) {
                    markdownBuilder.source(example.toString(), "json");
                } else {
                    String value = getValue(response.getSchema());
                    String prettyJson = mapper.writerWithDefaultPrettyPrinter()
                            .writeValueAsString(mapper.readValue(value, Object.class));

                    getValue(response.getSchema());

                    markdownBuilder.source(prettyJson, "json");
                }
            }

            String description = trimNullableText(operation.getDescription());

            if (!description.isEmpty()) {
                markdownBuilder.paragraph(description);
            }

            markdownBuilder.sectionTitleLevel2("HTTP Request").textLine("`" + method + " " + path + "`");

            renderParameters(operation.getParameters(), markdownBuilder);

            markdownBuilder.sectionTitleLevel2("Responses for status codes");
            List<String> responses = new ArrayList<>();

            String headerRow = operation.getResponses().keySet().stream().collect(Collectors.joining("|"));
            responses.add(headerRow);

            String responseRow = operation.getResponses().values().stream().filter(e -> e.getSchema() != null) // some responses can be null
                    .map(e -> getType(e.getSchema(), definitions)).collect(Collectors.joining("|"));
            responses.add(responseRow);

            markdownBuilder.tableWithHeaderRow(responses);

        } catch (Exception e) {
            LOGGER.error(format("An error occurred while processing operation. %s %s. Skipping..",
                    method.toUpperCase(Locale.ENGLISH), path), e);
        }
    }

    private void renderParameters(List<Parameter> _parameters, MarkdownBuilder markdownBuilder) {
        Multimap<ParameterIn, String> parameterGroups = ArrayListMultimap.create();

        if (_parameters == null || _parameters.isEmpty()) {
            return;
        }

        _parameters.forEach(p -> {
            ParameterIn parameterIn;
            try {
                parameterIn = ParameterIn.valueOf(p.getIn().toUpperCase(Locale.ENGLISH));
            } catch (IllegalArgumentException e) {
                throw new UnsupportedOperationException(
                        format("Parameter type '%s' not supported yet.", p.getIn()));
            }

            parameterGroups.putAll(parameterIn, renderParameter(parameterIn, p));
        });

        parameterGroups.keySet().forEach(key -> {
            List<String> group = new ArrayList<>();
            group.add("Parameter|Required|Type|Description");
            group.addAll(parameterGroups.get(key));

            markdownBuilder.sectionTitleLevel2(key.getQuery() + " Parameters");
            markdownBuilder.tableWithHeaderRow(group);
        });
    }

    private List<String> renderParameter(ParameterIn parameterIn, Parameter p) {
        if (parameterIn.equals(ParameterIn.BODY)) {
            Model schema = ((BodyParameter) p).getSchema();
            if (schema instanceof RefModel) {
                schema = swagger.getDefinitions().get(((RefModel) schema).getSimpleRef());
            }

            Map<String, Property> properties;
            if (schema instanceof ArrayModel) {
                Property items = ((ArrayModel) schema).getItems();
                properties = ImmutableMap.of("array", items);
            } else {
                properties = schema.getProperties();
            }
            return properties.entrySet().stream().map(entry -> {
                if (entry == null)
                    System.out.println("entry was null:");
                if (entry.getValue() == null)
                    System.out.println("value was null:" + entry.getKey());
                return entry.getKey() + "|" + entry.getValue().getRequired() + "|"
                        + getType(entry.getValue(), definitions) + "|"
                        + trimNullableText(entry.getValue().getDescription());
            }).collect(Collectors.toList());
        } else {
            return newArrayList(p.getName() + "|" + p.getRequired() + "|" + ParameterUtils.getType(p, definitions)
                    + "|" + trimNullableText(p.getDescription()));
        }
    }

    private String toExampleJsonParameters(Map<String, Property> properties) {
        return "{" + properties.entrySet().stream()
                .map(e -> "\"" + e.getKey() + "\" : " + getValue(e.getValue()) + "\n")
                .collect(Collectors.joining(", ")) + "}";
    }

    private String toExampleJsonParameters(Operation operation) {
        if (operation.getParameters().size() == 1 && operation.getParameters().get(0).getIn().equals("body")) {
            Model model = ((BodyParameter) operation.getParameters().get(0)).getSchema();

            Map<String, Property> properties;
            if (model.getReference() != null) {
                String prefix = "#/definitions/";
                if (model.getReference().startsWith(prefix)) {
                    Model model1 = swagger.getDefinitions().get(model.getReference().substring(prefix.length()));
                    if (model1 instanceof ArrayModel) {
                        return prettyJson("[" + getValue(((ArrayModel) model1).getItems()) + "]");
                    }
                    properties = model1.getProperties();
                } else {
                    throw new IllegalStateException();
                }
            } else {
                properties = model.getProperties();
            }
            return prettyJson(toExampleJsonParameters(properties));
        }

        String jsonString = "{" + operation.getParameters().stream().filter(e -> e instanceof FormParameter)
                .map(e -> "\"" + e.getName() + "\" : " + getValue((AbstractSerializableParameter) e))
                .collect(Collectors.joining(", ")) + "}";

        return prettyJson(jsonString);
    }

    private static String prettyJson(String json) {
        try {
            return mapper.enable(SerializationFeature.INDENT_OUTPUT).writerWithDefaultPrettyPrinter()
                    .writeValueAsString(mapper.readValue(json, Object.class));
        } catch (IOException e) {
            throw new IllegalStateException("Example generator couldn't generate a valid JSON");
        }
    }

    private String getValue(Property value) {
        return getValue(value, null);
    }

    private String getValue(Property value, Property parent) {
        if (value.getExample() != null && value.getExample() != null) {
            return value.getExample().toString();
        }
        if (value instanceof StringProperty) {
            List<String> anEnum = ((StringProperty) value).getEnum();
            if (anEnum != null && !anEnum.isEmpty()) {
                try {
                    return mapper.writeValueAsString(anEnum.get(0));
                } catch (JsonProcessingException e) {
                    throw Throwables.propagate(e);
                }
            }
            return "\"str\"";
        } else if (value instanceof IntegerProperty || value instanceof LongProperty) {
            return "1";
        } else if (value instanceof DoubleProperty) {
            return "1.0";
        } else if (value instanceof DecimalProperty) {
            return "1.0";
        } else if (value instanceof DateProperty) {
            return "\"2015-01-20\"";
        } else if (value instanceof BooleanProperty) {
            return "true";
        } else if (value instanceof MapProperty) {
            return "{\"prop\": {}}";
        } else if (value instanceof RefProperty) {
            Model model = swagger.getDefinitions().get(((RefProperty) value).getSimpleRef());
            if (model.getProperties() == null) {
                LOGGER.warn("missing properties for " + value.getType() + " :" + parent.getName());
                return "{}";
            }
            return "{" + model.getProperties().entrySet().stream()
                    .map(e -> "\"" + e.getKey() + "\" : " + getValue(e.getValue(), parent) + "")
                    .collect(Collectors.joining(", ")) + "}";
        } else if (value instanceof ArrayProperty) {
            if (parent != null && parent.equals(value)) {
                return "[]";
            } else {
                return "[\n\t" + getValue(((ArrayProperty) value).getItems(), value) + "\n]";
            }
        } else if (value instanceof ObjectProperty) {
            return "\"object\"";
        } else if (value instanceof DateTimeProperty) {
            return "\"2016-03-03T10:15:30.00Z\"";
        } else if (value instanceof UUIDProperty) {
            return "\"4f884c73-7d2d-4c70-9e16-9685bda4263a\"";
        } else {
            throw new IllegalStateException("Value " + value + " is not supported.");
        }
    }

    private String getValue(AbstractSerializableParameter value) {
        switch (value.getType()) {
        case "date":
            return "\"2015-01-20\"";
        case "string":
            return "\"str\"";
        case "integer":
        case "long":
            return "0";
        case "double":
            return "0.0";
        case "boolean":
            return "false";
        case "map":
            return "{\"prop\": value}";
        case "array":
            return "[\n\t" + getValue(value.getItems()) + "\n]";
        default:
            return "";
        }
    }

    private String trimNullableText(String text) {
        if (text == null || text.equals("null")) {
            return "";
        }
        return text.trim();
    }

    private enum ParameterIn {
        PATH("Path"), BODY("Body"), HEADER("Header"), FORMDATA("Form"), QUERY("Query");

        private final String query;

        ParameterIn(String query) {
            this.query = query;
        }

        public String getQuery() {
            return query;
        }
    }

    private Map<OperationIdentifier, Map<String, String>> generateExampleUsages() throws IOException {
        Map<OperationIdentifier, Map<String, String>> templates = Maps.newHashMap();

        Map<String, Entry<CodegenConfig, DefaultGenerator>> languages = new HashMap<>();

        for (CodegenConfigurator configurator : configurators) {
            ClientOptInput clientOptInput = configurator.toClientOptInput();
            clientOptInput.getConfig().processOpts();

            DefaultGenerator defaultGenerator = new DefaultGenerator();
            defaultGenerator.opts(clientOptInput);

            if (!supportedLanguages.contains(configurator.getLang())) {
                throw new IllegalArgumentException(
                        format("Language %s is not supported at the moment.", configurator.getLang()));
            }

            languages.put(configurator.getLang(),
                    new AbstractMap.SimpleImmutableEntry<>(clientOptInput.getConfig(), defaultGenerator));

            if (swagger == null) {
                swagger = clientOptInput.getSwagger();
            }
        }

        for (Entry<String, Entry<CodegenConfig, DefaultGenerator>> entry : languages.entrySet()) {
            String language = entry.getKey();
            Entry<CodegenConfig, DefaultGenerator> value = entry.getValue();

            Map<String, List<CodegenOperation>> operations = value.getValue().processPaths(swagger.getPaths());
            for (String parentTag : operations.keySet()) {
                List<CodegenOperation> ops = operations.get(parentTag);
                for (CodegenOperation op : ops) {
                    Map<String, Object> operation = value.getValue().processOperations(value.getKey(), parentTag,
                            ImmutableList.of(op));

                    operation.put("modelPackage", value.getKey().modelPackage());
                    operation.put("classname", value.getKey().toApiName(parentTag));
                    operation.put("hostname", swagger.getHost());

                    for (String templateName : value.getKey().apiTemplateFiles().keySet()) {
                        String filename = value.getKey().apiFilename(templateName, parentTag);
                        if (!value.getKey().shouldOverwrite(filename) && new File(filename).exists()) {
                            continue;
                        }

                        String template;
                        URL resource = this.getClass().getClassLoader()
                                .getResource("templates/" + language + "_api_example.mustache");

                        template = Resources.toString(resource, StandardCharsets.UTF_8);

                        Template tmpl = Mustache.compiler()
                                .withLoader(name -> value.getValue().getTemplateReader(
                                        value.getKey().templateDir() + File.separator + name + ".mustache"))
                                .defaultValue("").compile(template);

                        templates.computeIfAbsent(new OperationIdentifier(op.path, op.httpMethod),
                                key -> Maps.newHashMap()).put(language, tmpl.execute(operation));
                    }
                }
            }
        }

        return templates;
    }

    public static class OperationIdentifier {
        public final String path;
        public final String httpMethod;

        public OperationIdentifier(String path, String httpMethod) {
            this.path = path;
            this.httpMethod = httpMethod;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (!(o instanceof OperationIdentifier)) {
                return false;
            }

            OperationIdentifier that = (OperationIdentifier) o;

            if (!path.equals(that.path)) {
                return false;
            }
            return httpMethod.equals(that.httpMethod);
        }

        @Override
        public int hashCode() {
            int result = path.hashCode();
            result = 31 * result + httpMethod.hashCode();
            return result;
        }

        @Override
        public String toString() {
            return "OperationIdentifier{" + "path='" + path + '\'' + ", httpMethod='" + httpMethod + '\'' + '}';
        }
    }

    private void processOperation(MarkdownBuilder markdownBuilder, String tag) {

        for (Entry<String, Path> entry : swagger.getPaths().entrySet()) {

            Path value = entry.getValue();
            if (value.getGet() != null && value.getGet().getTags().contains(tag)) {
                processOperation(markdownBuilder, entry.getKey(), "GET", value.getGet());
            }
            if (value.getPut() != null && value.getPut().getTags().contains(tag)) {
                processOperation(markdownBuilder, entry.getKey(), "PUT", value.getPut());
            }
            if (value.getPost() != null && value.getPost().getTags().contains(tag)) {
                processOperation(markdownBuilder, entry.getKey(), "POST", value.getPost());
            }
            if (value.getDelete() != null && value.getDelete().getTags().contains(tag)) {
                processOperation(markdownBuilder, entry.getKey(), "DELETE", value.getDelete());
            }
            if (value.getPatch() != null && value.getPatch().getTags().contains(tag)) {
                processOperation(markdownBuilder, entry.getKey(), "PATCH", value.getPatch());
            }
            if (value.getOptions() != null && value.getOptions().getTags().contains(tag)) {
                processOperation(markdownBuilder, entry.getKey(), "OPTIONS", value.getOptions());
            }
        }
    }
}