Java tutorial
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()); } } } }