/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.jooby.internal.raml; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.typesafe.config.Config; import org.jooby.MediaType; import org.jooby.spec.RouteParam; import org.jooby.spec.RouteParamType; import org.jooby.spec.RouteResponse; import org.jooby.spec.RouteSpec; import javax.inject.Inject; import javax.inject.Named; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; public class RamlBuilder { private static class Resource { private Set<Resource> children = new LinkedHashSet<>(); private String pattern; private List<RouteSpec> routes = new ArrayList<>(); private String mediaType; public Resource(final String pattern, final String mediaType) { this.pattern = pattern; this.mediaType = mediaType; } List<RouteSpec> routes() { List<RouteSpec> routes = new ArrayList<>(); routes.addAll(this.routes); for (Resource resource : children) { routes.addAll(resource.routes()); } return routes; } @Override public boolean equals(final Object obj) { if (obj instanceof Resource) { return pattern.equals(((Resource) obj).pattern); } return false; } @Override public int hashCode() { return pattern.hashCode(); } @Override public String toString() { return toString(0); } private String toString(final int level) { List<RouteSpec> routes = this.routes; String pattern = this.pattern; Set<Resource> children = this.children; List<RouteSpec> deep = routes(); if (deep.size() == 1) { routes = deep; List<String> snested = Splitter.on("/").trimResults().omitEmptyStrings() .splitToList(normalize(deep.get(0).pattern())); pattern = normalize("/" + snested.subList(level, snested.size()).stream().collect(Collectors.joining("/"))); children = Collections.emptySet(); } StringBuilder buff = new StringBuilder(); String fpattern = pattern; buff.append(indent(level)).append(fpattern).append(":\n"); Set<String> uriParamVisited = new HashSet<>(); Set<String> visitedPaths = new HashSet<>(); for (RouteSpec route : routes) { List<String> consumes = route.consumes().stream() .filter(t -> !t.equals("*/*")) .collect(Collectors.toList()); List<String> produces = route.produces().stream() .filter(t -> !t.equals("*/*")) .collect(Collectors.toList()); if (produces.isEmpty()) { produces.add(mediaType); } // uri params List<RouteParam> uriParams = route.params().stream() .filter(p -> p.paramType() == RouteParamType.PATH) .filter(p -> !uriParamVisited.contains(p.name())) .collect(Collectors.toList()); if (uriParams.size() > 0) { buff.append(indent(level + 1)).append("uriParameters:\n"); uriParams.forEach(p -> { uriParamVisited.add(p.name()); buff.append(param(p, level + 1)); }); } // method route.summary().ifPresent(doc -> { if (visitedPaths.add(fpattern)) { buff.append(indent(level + 1)).append("description: ") .append(Doc.parse(doc, level + 3)).append("\n"); } }); buff.append(indent(level + 1)).append(route.method().toLowerCase()).append(":\n"); route.doc().ifPresent( doc -> buff.append(indent(level + 2)).append("description: ") .append(Doc.parse(doc, level + 4)).append("\n")); // headers List<RouteParam> headers = route.params().stream() .filter(p -> p.paramType() == RouteParamType.HEADER) .collect(Collectors.toList()); if (headers.size() > 0) { buff.append(indent(level + 2)).append("headers:\n"); headers.forEach(p -> { buff.append(param(p, level + 2)); }); } // query params List<RouteParam> queryParams = route.params().stream() .filter(p -> p.paramType() == RouteParamType.QUERY) .collect(Collectors.toList()); if (queryParams.size() > 0) { buff.append(indent(level + 2)).append("queryParameters:\n"); queryParams.forEach(p -> { buff.append(param(p, level + 2)); }); } // body params boolean hasFiles = route.params().stream() .filter(p -> p.paramType() == RouteParamType.FILE) .findFirst() .isPresent(); List<RouteParam> bodyParams = route.params().stream() .filter( p -> p.paramType() == RouteParamType.BODY || p.paramType() == RouteParamType.FILE) .collect(Collectors.toList()); if (bodyParams.size() > 0) { buff.append(indent(level + 2)).append("body:\n"); bodyParams.forEach(p -> { List<String> cmtypes = consumes; if (hasFiles) { cmtypes = ImmutableList.of(MediaType.multipart.name()); } else if (cmtypes.isEmpty()) { cmtypes = ImmutableList.of(mediaType); } cmtypes.forEach(t -> buff.append(indent(level + 3)).append(t).append(":\n")); int foffset = 0; if (cmtypes.contains(MediaType.form.name()) || cmtypes.contains(MediaType.multipart.name())) { buff.append(indent(level + 4)).append("formParameters:\n"); foffset = 1; } buff.append(param(p, level + foffset + 3)); }); } // responses RouteResponse rsp = route.response(); if (rsp.type() != Object.class) { buff.append(indent(level + 2)).append("responses:\n"); buff.append(indent(level + 3)).append(rsp.statusCode()).append(":\n"); rsp.doc().ifPresent(doc -> buff.append(indent(level + 4)).append("description: ") .append(Doc.parse(doc, level + 4)).append("\n")); if (rsp.type() != void.class) { RamlType rspType = RamlType.parse(rsp.type()); buff.append(indent(level + 4)).append("body").append(":\n"); produces.stream().forEach(t -> buff.append(indent(level + 5)).append(t).append(":\n")); buff.append(indent(level + 6)).append("type: ").append(rspType.type()).append("\n"); } Map<Integer, String> statusCodes = Maps.newLinkedHashMap(rsp.statusCodes()); statusCodes.remove(rsp.statusCode()); statusCodes.forEach((sc, msg) -> { buff.append(indent(level + 3)).append(sc).append(":\n"); buff.append(indent(level + 4)).append("description: ").append(Doc.parse(msg, level + 8)) .append("\n"); }); } } for (Resource child : children) { buff.append(child.toString(level + 1)); } return buff.toString(); } private CharSequence param(final RouteParam p, final int level) { StringBuilder buff = new StringBuilder(); int offset; boolean body = p.paramType() == RouteParamType.BODY; if (!body) { buff.append(indent(level + 1)).append(p.name()).append(":\n"); offset = 1; } else { offset = 0; } RamlType type = RamlType.parse(p.type()); buff.append(indent(level + offset + 1)).append("type: ") .append(type.type()) .append("\n"); p.doc().ifPresent(doc -> buff.append(indent(level + offset + 1)) .append("description: ") .append(Doc.parse(doc, level + offset + 2)).append("\n")); if (!body) { buff.append(indent(level + offset + 1)).append("required: ").append(!p.optional()) .append("\n"); } if (p.value() != null) { buff.append(indent(level + offset + 1)).append("default: ").append(p.value()).append("\n"); } return buff; } } private static final Pattern VAR = Pattern.compile("\\:((?:[^/]+)+?)"); private Config conf; @Inject public RamlBuilder(@Named("raml") final Config conf) { this.conf = conf; } public String build(final List<RouteSpec> routes) { StringBuilder buff = new StringBuilder(); buff.append("#%RAML 1.0\n"); conf.root() .entrySet() .stream() .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) .forEach(e -> buff.append(e.getKey()).append(": ") .append(e.getValue().unwrapped()).append("\n")); // types Set<RamlType> types = new LinkedHashSet<>(); Consumer<Type> typeCollector = type -> { if (type != Object.class && type != void.class) { RamlType.parseAll(type).stream() .filter(t -> t.isObject() || t.isEnum()) .forEach(types::add); } }; routes.forEach(route -> { route.params().forEach(p -> { typeCollector.accept(p.type()); }); typeCollector.accept(route.response().type()); }); if (types.size() > 0) { buff.append("types:\n"); types.forEach(t -> buff.append(t.toString(2)).append("\n")); } // resources tree(routes, conf.getString("mediaType")).forEach(buff::append); return buff.toString().trim(); } private List<Resource> tree(final List<RouteSpec> routes, final String mediaType) { List<Resource> result = new ArrayList<>(); Map<String, Resource> hash = new HashMap<>(); for (RouteSpec route : routes) { String pattern = normalize(route.pattern()); List<String> segments = Splitter.on('/') .trimResults() .omitEmptyStrings() .splitToList(pattern); String prev = "/"; Resource root = null; for (int i = 0; i < segments.size(); i++) { String segment = segments.get(i); String it = prev + segment; Resource resource = hash.get(it); if (resource == null) { resource = new Resource("/" + segment, mediaType); if (i == 0) { root = resource; result.add(resource); } else { root.children.add(resource); } hash.put(it, resource); } root = resource; prev = it + "/"; } Resource resource = hash.get(pattern); if (resource == null) { resource = new Resource(pattern, mediaType); result.add(resource); } resource.routes.add(route); } return result; } private static String indent(final int level) { StringBuilder buff = new StringBuilder(); for (int i = 0; i < level * 2; i++) { buff.append(" "); } return buff.toString(); } private static String normalize(final String pattern) { Matcher matcher = VAR.matcher(pattern); StringBuilder result = new StringBuilder(); int end = 0; while (matcher.find()) { result.append(pattern, end, matcher.start()); result.append("{").append(matcher.group(1)).append("}"); end = matcher.end(); } result.append(pattern, end, pattern.length()); return result.toString(); } }