org.jooby.internal.swagger.SwaggerBuilder Maven / Gradle / Ivy
/**
* 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 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 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 build(final Optional tagFilter,
final Predicate filter, final Function tagProvider,
final Class swaggerType) {
S swagger = newSwagger(swaggerType, mapper, config);
RouteProcessor processor = new RouteProcessor();
List specs = processor.process(appClass, routes);
Map paths = new LinkedHashMap<>();
Map 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());
/**
* Doc and summary: default or full
*/
route.name().ifPresent(n -> op.summary(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 params = route.params();
for (RouteParam param : params) {
op.addParameter(param(param, swagger::addDefinition));
}
/**
* Response
*/
Response rsp = new Response();
RouteResponse routersp = route.response();
Map 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 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 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 definitions) {
ModelConverters converter = ModelConverters.getInstance();
final Property property = converter.readAsProperty(type);
final Map args = new EnumMap(
PropertyBuilder.PropertyId.class);
for (Map.Entry entry : converter.readAll(type).entrySet()) {
definitions.accept(entry.getKey(), entry.getValue());
}
return PropertyBuilder.toModel(PropertyBuilder.merge(property, args));
}
private Optional 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 newSwagger(final Class type, final ObjectMapper mapper,
final Config config) {
// hack, get a hash from config and then use jackson to get the a swagger bean
Map 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();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy