/** * 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.swagger; import static java.util.Objects.requireNonNull; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.EnumMap; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; import javax.inject.Named; import org.jooby.Jooby; import org.jooby.Route; import org.jooby.spec.RouteParam; import org.jooby.spec.RouteProcessor; import org.jooby.spec.RouteResponse; import org.jooby.spec.RouteSpec; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.typesafe.config.Config; import io.swagger.converter.ModelConverters; import io.swagger.models.Model; import io.swagger.models.Operation; import io.swagger.models.Path; import io.swagger.models.Response; import io.swagger.models.Swagger; import io.swagger.models.Tag; import io.swagger.models.parameters.BodyParameter; import io.swagger.models.parameters.FormParameter; import io.swagger.models.parameters.HeaderParameter; import io.swagger.models.parameters.Parameter; import io.swagger.models.parameters.PathParameter; import io.swagger.models.parameters.QueryParameter; import io.swagger.models.parameters.SerializableParameter; import io.swagger.models.properties.ArrayProperty; import io.swagger.models.properties.Property; import io.swagger.models.properties.PropertyBuilder; public class SwaggerBuilder { private static final Pattern VAR = Pattern.compile("\\:((?:[^/]+)+?)"); private static final Pattern SENTENCE = Pattern.compile("\\.|\\n"); /** The logging system. */ private final Logger log = LoggerFactory.getLogger(getClass()); private List<Route.Definition> routes; private Config config; private ObjectMapper mapper; private Class<? extends Jooby> appClass; @Inject public SwaggerBuilder(@Named("application.class") final Class<? extends Jooby> appClass, final Set<Route.Definition> routes, @Named("swagger") final Config config, @Named("swagger") final ObjectMapper mapper) { requireNonNull(appClass, "App class is required."); requireNonNull(routes, "Routes are required."); this.routes = ImmutableList.copyOf(routes); this.appClass = appClass; this.config = requireNonNull(config, "Swagger config is required."); this.mapper = requireNonNull(mapper, "Mapper is required."); } public <S extends Swagger> S build(final Optional<String> tagFilter, final Predicate<RouteSpec> filter, final Function<RouteSpec, String> tagProvider, final Class<S> swaggerType) { S swagger = newSwagger(swaggerType, mapper, config); RouteProcessor processor = new RouteProcessor(); List<RouteSpec> specs = processor.process(appClass, routes); Map<String, Path> paths = new LinkedHashMap<>(); Map<String, Tag> tags = new HashMap<>(); for (RouteSpec route : specs) { String tagname = tagProvider.apply(route); boolean process = filter.test(route) && tagFilter.map(tagname::equals).orElse(true); if (process) { String pattern = normalizePath(route.pattern()); /** * Tags */ Tag tag = tags.get(tagname); if (tag == null) { tag = new Tag(); tag.name(tagname); tags.put(tagname, tag); swagger.addTag(tag); } // tag summary route.summary().ifPresent(tag::description); /** * Path */ Path path = paths.get(pattern); if (path == null) { path = new Path(); paths.put(pattern, path); } /** * Operation */ Operation op = new Operation(); op.addTag(tag.getName()); route.name().ifPresent(op::operationId); /** * Doc and summary */ route.name().ifPresent(n -> op.summary(Route.normalize(n).substring(1))); route.doc().ifPresent(doc -> { String summary = Splitter.on(SENTENCE) .trimResults() .omitEmptyStrings() .split(doc) .iterator() .next(); op.summary(summary); op.description(doc); }); /** Consumes/Produces . */ route.consumes().stream() .filter(t -> !t.equals("*/*")) .forEach(type -> op.addConsumes(type)); route.produces().stream() .filter(t -> !t.equals("*/*")) .forEach(type -> op.addProduces(type)); /** * Params */ List<RouteParam> params = route.params(); for (RouteParam param : params) { op.addParameter(param(param, swagger::addDefinition)); } /** * Response */ Response rsp = new Response(); RouteResponse routersp = route.response(); Map<Integer, String> statusCodes = Maps.newHashMap(routersp.statusCodes()); int statusCode = routersp.statusCode(); String doc = routersp.doc().orElse(statusCodes.get(statusCode)); rsp.description(doc); Type returnType = routersp.type(); definitions(returnType, swagger::addDefinition); rsp.schema(ModelConverters.getInstance().readAsProperty(returnType)); op.addResponse(String.valueOf(statusCode), rsp); // additional status codes statusCodes.forEach((sc, label) -> { if (statusCode != sc.intValue()) { op.addResponse(sc.toString(), new Response().description(label)); } }); path.set(route.method().toLowerCase(), op); } else { log.debug("skipping: {}", route); } } swagger.paths(paths); return swagger; } @SuppressWarnings("rawtypes") private Parameter param(final RouteParam param, final BiConsumer<String, Model> definitions) { ModelConverters converter = ModelConverters.getInstance(); Type type = paramType(param.type()); final Property property = converter.readAsProperty(type); boolean required = !param.optional(); final Parameter result; switch (param.paramType()) { case BODY: { BodyParameter bp = new BodyParameter(); bp.setSchema(definitions(type, definitions)); result = bp; } break; case HEADER: { result = new HeaderParameter(); } break; case PATH: { result = new PathParameter(); } break; case FORM: { result = new FormParameter(); } break; default: { result = new QueryParameter(); } break; } // set type, format and items result.setDescription(property.getDescription()); serializable(result).ifPresent(ser -> { ser.setDescription(property.getDescription()); ser.setType(property.getType()); ser.setFormat(property.getFormat()); if (property instanceof ArrayProperty) { ser.setItems(((ArrayProperty) property).getItems()); } if (type instanceof Class) { Class possibleEnum = (Class) type; Object[] values = possibleEnum.getEnumConstants(); if (values != null) { List<String> enums = new ArrayList<>(); for (Object value : values) { enums.add(((Enum) value).name()); } ser.setEnum(enums); } } }); result.setName(param.name()); result.setRequired(required); param.doc().ifPresent(result::setDescription); return result; } private Model definitions(final Type type, final BiConsumer<String, Model> definitions) { ModelConverters converter = ModelConverters.getInstance(); final Property property = converter.readAsProperty(type); final Map<PropertyBuilder.PropertyId, Object> args = new EnumMap<>( PropertyBuilder.PropertyId.class); for (Map.Entry<String, Model> entry : converter.readAll(type).entrySet()) { definitions.accept(entry.getKey(), entry.getValue()); } return PropertyBuilder.toModel(PropertyBuilder.merge(property, args)); } private Optional<SerializableParameter> serializable(final Parameter param) { if (param instanceof SerializableParameter) { return Optional.of((SerializableParameter) param); } return Optional.empty(); } private Type paramType(final Type type) { if (type.getTypeName().startsWith("java.util.Optional") && type instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) type; return pt.getActualTypeArguments()[0]; } return type; } private <S extends Swagger> S newSwagger(final Class<S> type, final ObjectMapper mapper, final Config config) { // hack, get a hash from config and then use jackson to get the a swagger bean Map<String, Object> json = config.root().unwrapped(); return mapper.convertValue(json, type); } private static String normalizePath(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(); } }