package org.rakam.server.http;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.reflect.TypeToken;
import io.swagger.jackson.ModelResolver;
import io.swagger.models.ArrayModel;
import io.swagger.models.Model;
import io.swagger.models.ModelImpl;
import io.swagger.models.Operation;
import io.swagger.models.Path;
import io.swagger.models.RefModel;
import io.swagger.models.Response;
import io.swagger.models.Scheme;
import io.swagger.models.SecurityRequirement;
import io.swagger.models.Swagger;
import io.swagger.models.Tag;
import io.swagger.models.parameters.AbstractSerializableParameter;
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.MapProperty;
import io.swagger.models.properties.Property;
import io.swagger.models.properties.PropertyBuilder;
import io.swagger.models.properties.RefProperty;
import io.swagger.util.PrimitiveType;
import org.rakam.server.http.annotations.Api;
import org.rakam.server.http.annotations.ApiImplicitParam;
import org.rakam.server.http.annotations.ApiImplicitParams;
import org.rakam.server.http.annotations.ApiOperation;
import org.rakam.server.http.annotations.ApiParam;
import org.rakam.server.http.annotations.ApiResponse;
import org.rakam.server.http.annotations.ApiResponses;
import org.rakam.server.http.annotations.Authorization;
import org.rakam.server.http.annotations.BodyParam;
import org.rakam.server.http.annotations.HeaderParam;
import org.rakam.server.http.annotations.IgnoreApi;
import org.rakam.server.http.annotations.ResponseHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;
import sun.reflect.generics.reflectiveObjects.TypeVariableImpl;
import javax.inject.Named;
import javax.ws.rs.Consumes;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Produces;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import static java.lang.String.format;
public class SwaggerReader
{
private final Logger LOGGER = LoggerFactory.getLogger(SwaggerReader.class);
private final Swagger swagger;
private final ModelConverters modelConverters;
private final BiConsumer<Method, Operation> swaggerOperationConsumer;
private final Property errorProperty;
public SwaggerReader(Swagger swagger, ObjectMapper mapper, BiConsumer<Method, Operation> swaggerOperationConsumer, Map<Class, PrimitiveType> externalTypes)
{
this.swagger = swagger;
modelConverters = new ModelConverters(mapper);
this.swaggerOperationConsumer = swaggerOperationConsumer;
modelConverters.addConverter(new ModelResolver(mapper));
if (externalTypes != null) {
setExternalTypes(externalTypes);
}
mapper.registerModule(
new SimpleModule("swagger", Version.unknownVersion()) {
@Override
public void setupModule(SetupContext context) {
context.insertAnnotationIntrospector(new SwaggerJacksonAnnotationIntrospector());
}
});
errorProperty = modelConverters.readAsProperty(HttpServer.ErrorMessage.class);
swagger.addDefinition("ErrorMessage", modelConverters.read(HttpServer.ErrorMessage.class).entrySet().iterator().next().getValue());
}
private void setExternalTypes(Map<Class, PrimitiveType> externalTypes)
{
// ugly hack until swagger supports adding external classes as primitive types
try {
Field externalTypesField = PrimitiveType.class.getDeclaredField("EXTERNAL_CLASSES");
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
externalTypesField.setAccessible(true);
modifiersField.set(externalTypesField, externalTypesField.getModifiers() & ~Modifier.FINAL);
Map<String, PrimitiveType> externalTypesInternal = externalTypes.entrySet().stream()
.collect(Collectors.toMap(e -> e.getKey().getName(), e -> e.getValue()));
externalTypesField.set(null, externalTypesInternal);
}
catch (NoSuchFieldException | IllegalAccessException e) {
LOGGER.warn("Couldn't set external types", e);
}
}
public Swagger read(Class cls)
{
return read(cls, "", false, new ArrayList<>());
}
protected Swagger read(Class<?> cls, String parentPath, boolean readHidden, List<Parameter> parentParameters)
{
Api api = cls.getAnnotation(Api.class);
javax.ws.rs.Path apiPath = cls.getAnnotation(javax.ws.rs.Path.class);
if (cls.isAnnotationPresent(IgnoreApi.class)) {
return swagger;
}
// only read if allowing hidden apis OR api is not marked as hidden
if ((api != null && readHidden) || (api != null && !api.hidden())) {
// the value will be used as a tag for 2.0 UNLESS a Tags annotation is present
Set<String> tags = extractTags(api);
Authorization[] authorizations = api.authorizations();
List<SecurityRequirement> securities = new ArrayList<>();
for (Authorization auth : authorizations) {
if (auth.value() != null && !"".equals(auth.value())) {
SecurityRequirement security = new SecurityRequirement();
security.requirement(auth.value());
securities.add(security);
}
}
// parse the method
Method methods[] = cls.getMethods();
for (Method method : methods) {
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
javax.ws.rs.Path methodPath = method.getAnnotation(javax.ws.rs.Path.class);
if (method.isAnnotationPresent(IgnoreApi.class)) {
continue;
}
String operationPath = getPath(apiPath, methodPath, parentPath);
if (operationPath != null && apiOperation != null) {
String[] pps = operationPath.split("/");
String[] pathParts = new String[pps.length];
Map<String, String> regexMap = new HashMap<>();
for (int i = 0; i < pps.length; i++) {
String p = pps[i];
if (p.startsWith("{")) {
int pos = p.indexOf(":");
if (pos > 0) {
String left = p.substring(1, pos);
String right = p.substring(pos + 1, p.length() - 1);
pathParts[i] = "{" + left + "}";
regexMap.put(left, right);
}
else {
pathParts[i] = p;
}
}
else {
pathParts[i] = p;
}
}
StringBuilder pathBuilder = new StringBuilder();
for (String p : pathParts) {
if (!p.isEmpty()) {
pathBuilder.append("/").append(p);
}
}
operationPath = pathBuilder.toString();
String httpMethod = extractOperationMethod(apiOperation, method);
Operation operation = null;
try {
operation = parseMethod(cls, method);
}
catch (Exception e) {
LOGGER.warn("Unable to read method " + method.toString(), e);
}
if (operation == null) {
continue;
}
if (parentParameters != null) {
parentParameters.forEach(operation::parameter);
}
operation.getParameters().stream()
.filter(param -> regexMap.get(param.getName()) != null)
.forEach(param -> {
String pattern = regexMap.get(param.getName());
param.setPattern(pattern);
});
String protocols = apiOperation.protocols();
if (!"".equals(protocols)) {
String[] parts = protocols.split(",");
for (String part : parts) {
String trimmed = part.trim();
if (!"".equals(trimmed)) {
operation.scheme(Scheme.forValue(trimmed));
}
}
}
if (isSubResource(method)) {
Class<?> responseClass = method.getReturnType();
read(responseClass, operationPath, true, operation.getParameters());
}
// can't continue without a valid http method
if (httpMethod != null) {
ApiOperation op = method.getAnnotation(ApiOperation.class);
if (op != null) {
for (String tag : op.tags()) {
if (!"".equals(tag)) {
operation.tag(tag);
swagger.tag(new Tag().name(tag));
}
}
}
tags.forEach(operation::tag);
if (operation != null) {
Consumes consumesAnn = method.getAnnotation(Consumes.class);
if (consumesAnn != null && !Arrays.asList(consumesAnn.value()).stream().anyMatch(a -> a.startsWith("application/json"))) {
continue;
}
if (!apiOperation.consumes().isEmpty() && !apiOperation.consumes().startsWith("application/json")) {
continue;
}
operation.consumes("application/json");
Produces produces = method.getAnnotation(Produces.class);
if (produces != null) {
operation.produces(Arrays.asList(produces.value()));
}
else if (!apiOperation.produces().isEmpty()) {
operation.produces(apiOperation.produces());
}
else {
operation.produces("application/json");
}
securities.forEach(operation::security);
Path path = swagger.getPath(operationPath);
if (path == null) {
path = new Path();
swagger.path(operationPath, path);
}
swaggerOperationConsumer.accept(method, operation);
path.set(httpMethod, operation);
}
}
}
}
}
return swagger;
}
protected boolean isSubResource(Method method)
{
Class<?> responseClass = method.getReturnType();
if (responseClass != null && responseClass.getAnnotation(Api.class) != null) {
return true;
}
return false;
}
private void readImplicitParameters(Method method, Operation operation)
{
ApiImplicitParams implicitParams = method.getAnnotation(ApiImplicitParams.class);
if (implicitParams != null && implicitParams.value().length > 0) {
if (Arrays.stream(implicitParams.value())
.anyMatch(param -> param.dataType().equals("object"))) {
BodyParameter bodyParameter = new BodyParameter();
ModelImpl model = new ModelImpl();
for (ApiImplicitParam param : implicitParams.value()) {
Parameter p = readImplicitParam(param);
if (!(p instanceof AbstractSerializableParameter)) {
throw new IllegalStateException();
}
AbstractSerializableParameter serializableParameter = (AbstractSerializableParameter) p;
Property items = PropertyBuilder.build(serializableParameter.getType(),
serializableParameter.getFormat(), ImmutableMap.of());
items.setRequired(param.required());
items.setAccess(param.access());
items.setDefault(param.defaultValue() == null ? null : param.defaultValue());
model.addProperty(param.name(), items);
}
String name = method.getDeclaringClass().getName() + "." + method.getName();
model.setName(name);
// TODO: fix for https://github.com/swagger-api/swagger-codegen/issues/354 remove ref when the issue is fixed.
swagger.addDefinition(name, model);
bodyParameter.name(name);
bodyParameter.setSchema(new RefModel(model.getName()));
operation.addParameter(bodyParameter);
}
else {
for (ApiImplicitParam param : implicitParams.value()) {
Parameter p = readImplicitParam(param);
if (p != null) {
operation.addParameter(p);
}
}
}
}
}
protected Parameter readImplicitParam(ApiImplicitParam param)
{
final AbstractSerializableParameter p;
if (param.paramType().equalsIgnoreCase("path")) {
p = new PathParameter();
}
else if (param.paramType().equalsIgnoreCase("query")) {
p = new QueryParameter();
}
else if (param.paramType().equalsIgnoreCase("form") || param.paramType().equalsIgnoreCase("formData")) {
p = new FormParameter();
}
else if (param.paramType().equalsIgnoreCase("body")) {
p = null;
}
else if (param.paramType().equalsIgnoreCase("header")) {
p = new HeaderParameter();
}
else {
LOGGER.warn("Unknown implicit parameter type: [" + param.paramType() + "]");
return null;
}
p.setName(param.name());
p.setType(param.dataType());
p.setDefaultValue(param.defaultValue());
p.setAccess(param.access());
final Type type = typeFromString(param.dataType());
return ParameterProcessor.applyAnnotations(swagger, p, type == null ? String.class : type,
Arrays.<Annotation>asList(param));
}
public static Type typeFromString(String type)
{
final PrimitiveType primitive = PrimitiveType.fromName(type);
if (primitive != null) {
return primitive.getKeyClass();
}
try {
return Class.forName(type);
}
catch (Exception e) {
// LOGGER.error(String.format("Failed to resolve '%s' into class", type), e);
}
return null;
}
private static Type getActualReturnType(Method method)
{
if (method.getReturnType().equals(CompletableFuture.class)) {
Type responseClass = method.getGenericReturnType();
ParameterizedType type;
if (responseClass instanceof ParameterizedType) {
type = (ParameterizedType) responseClass;
}
else {
try {
// TODO: check super classes if this doesn't work.
type = (ParameterizedType) method.getDeclaringClass().getSuperclass().getMethod(method.getName(), method.getParameterTypes())
.getGenericReturnType();
}
catch (NoSuchMethodException e) {
throw Throwables.propagate(e);
}
}
return type.getActualTypeArguments()[0];
}
return method.getGenericReturnType();
}
private static Class getActualReturnClass(Method method)
{
if (method.getReturnType().equals(CompletableFuture.class)) {
Type responseClass = method.getGenericReturnType();
Type type = ((ParameterizedType) responseClass).getActualTypeArguments()[0];
if (type instanceof ParameterizedType) {
return (Class) ((ParameterizedType) type).getRawType();
}
else {
return (Class) type;
}
}
return method.getReturnType();
}
protected Set<String> extractTags(Api api)
{
Set<String> output = new LinkedHashSet<>();
boolean hasExplicitTags = false;
for (String tag : api.tags()) {
if (!"".equals(tag)) {
hasExplicitTags = true;
output.add(tag);
}
}
if (!hasExplicitTags) {
// derive tag from api path + description
String tagString = api.value().replace("/", "");
if (!"".equals(tagString)) {
output.add(tagString);
}
}
return output;
}
String getPath(javax.ws.rs.Path classLevelPath, javax.ws.rs.Path methodLevelPath, String parentPath)
{
if (classLevelPath == null && methodLevelPath == null) {
return null;
}
StringBuilder b = new StringBuilder();
if (parentPath != null && !"".equals(parentPath) && !"/".equals(parentPath)) {
if (!parentPath.startsWith("/")) {
parentPath = "/" + parentPath;
}
if (parentPath.endsWith("/")) {
parentPath = parentPath.substring(0, parentPath.length() - 1);
}
b.append(parentPath);
}
if (classLevelPath != null) {
b.append(classLevelPath.value());
}
if (methodLevelPath != null && !"/".equals(methodLevelPath.value())) {
String methodPath = methodLevelPath.value();
if (!methodPath.startsWith("/") && !b.toString().endsWith("/")) {
b.append("/");
}
if (methodPath.endsWith("/")) {
methodPath = methodPath.substring(0, methodPath.length() - 1);
}
b.append(methodPath);
}
String output = b.toString();
if (!output.startsWith("/")) {
output = "/" + output;
}
if (output.endsWith("/") && output.length() > 1) {
return output.substring(0, output.length() - 1);
}
else {
return output;
}
}
public Map<String, Property> parseResponseHeaders(ResponseHeader[] headers)
{
Map<String, Property> responseHeaders = null;
if (headers != null && headers.length > 0) {
for (ResponseHeader header : headers) {
String name = header.name();
if (!"".equals(name)) {
if (responseHeaders == null) {
responseHeaders = new HashMap<>();
}
String description = header.description();
Class<?> cls = header.response();
String container = header.responseContainer();
if (!cls.equals(java.lang.Void.class) && !"void".equals(cls.toString())) {
Property responseProperty;
Property property = modelConverters.readAsProperty(cls);
if (property != null) {
if ("list".equalsIgnoreCase(container)) {
responseProperty = new ArrayProperty(property);
}
else if ("map".equalsIgnoreCase(container)) {
responseProperty = new MapProperty(property);
}
else {
responseProperty = property;
}
responseProperty.setDescription(description);
responseHeaders.put(name, responseProperty);
}
}
}
}
}
return responseHeaders;
}
public Operation parseMethod(Class readClass, Method method)
{
Operation operation = new Operation();
ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
ApiResponses responseAnnotation = method.getAnnotation(ApiResponses.class);
String responseContainer = null;
Type responseClass = null;
Map<String, Property> defaultResponseHeaders = new HashMap<>();
Api parentApi = (Api) readClass.getAnnotation(Api.class);
String nickname = !apiOperation.nickname().isEmpty() ? apiOperation.nickname() : parentApi.nickname();
if (nickname.isEmpty()) {
nickname = method.getDeclaringClass().getName().replace(".", "_");
}
String methodName = method.getName().substring(0, 1).toUpperCase() + method.getName().substring(1);
String methodIdentifier = nickname.substring(0, 1).toUpperCase() + nickname.substring(1) + methodName;
if (apiOperation != null) {
if (apiOperation.hidden()) {
return null;
}
operation.operationId(methodName);
defaultResponseHeaders = parseResponseHeaders(apiOperation.responseHeaders());
operation
.summary(apiOperation.value())
.description(apiOperation.notes());
if (apiOperation.response() != null && !Void.class.equals(apiOperation.response())) {
responseClass = apiOperation.response();
}
if (!"".equals(apiOperation.responseContainer())) {
responseContainer = apiOperation.responseContainer();
}
if (apiOperation.authorizations() != null) {
List<SecurityRequirement> securities = new ArrayList<>();
for (Authorization auth : apiOperation.authorizations()) {
if (auth.value() != null && !"".equals(auth.value())) {
SecurityRequirement security = new SecurityRequirement();
security.requirement(auth.value());
security.setName(auth.value());
security.addScope(auth.value());
securities.add(security);
}
}
securities.forEach(operation::security);
}
}
if (responseClass == null && !method.getReturnType().equals(Void.class)) {
responseClass = getActualReturnType(method);
}
if (responseClass != null && !responseClass.equals(java.lang.Void.class)) {
if (responseClass instanceof Class && TypeToken.class.equals(((Class) responseClass).getSuperclass())) {
responseClass = ((ParameterizedType) ((Class) responseClass).getGenericSuperclass()).getActualTypeArguments()[0];
}
if (isPrimitive(responseClass)) {
Property responseProperty;
Property property = modelConverters.readAsProperty(responseClass);
if (property != null) {
if ("list".equalsIgnoreCase(responseContainer)) {
responseProperty = new ArrayProperty(property);
}
else if ("map".equalsIgnoreCase(responseContainer)) {
responseProperty = new MapProperty(property);
}
else {
responseProperty = property;
}
operation.response(200, new Response()
.description("Successful request")
.schema(responseProperty)
.headers(defaultResponseHeaders));
}
}
else if (!responseClass.equals(java.lang.Void.class) && !"void".equals(responseClass.toString())) {
String name = responseClass.getTypeName();
Model model = modelConverters.read(responseClass).get(name);
if (model == null) {
Property p = modelConverters.readAsProperty(responseClass);
operation.response(200, new Response()
.description("Successful request")
.schema(p)
.headers(defaultResponseHeaders));
}
else {
model.setReference(responseClass.getTypeName());
Property responseProperty;
if ("list".equalsIgnoreCase(responseContainer)) {
responseProperty = new ArrayProperty(new RefProperty().asDefault(name));
}
else if ("map".equalsIgnoreCase(responseContainer)) {
responseProperty = new MapProperty(new RefProperty().asDefault(name));
}
else {
responseProperty = new RefProperty().asDefault(name);
}
operation.response(200, new Response()
.description("Successful operation")
.schema(responseProperty)
.headers(defaultResponseHeaders));
swagger.model(name, model);
}
}
Map<String, Model> models = modelConverters.readAll(responseClass);
for (String key : models.keySet()) {
swagger.model(key, models.get(key));
swagger.addDefinition(key, models.get(key));
}
}
Annotation annotation;
annotation = method.getAnnotation(Consumes.class);
if (annotation != null) {
String[] apiConsumes = ((Consumes) annotation).value();
for (String mediaType : apiConsumes) {
operation.consumes(mediaType);
}
}
annotation = method.getAnnotation(Produces.class);
if (annotation != null) {
String[] apiProduces = ((Produces) annotation).value();
for (String mediaType : apiProduces) {
operation.produces(mediaType);
}
}
if (responseAnnotation != null) {
for (ApiResponse apiResponse : responseAnnotation.value()) {
Map<String, Property> responseHeaders = parseResponseHeaders(apiResponse.responseHeaders());
Response response = new Response()
.description(apiResponse.message())
.schema(errorProperty)
.headers(responseHeaders);
if (apiResponse.response() != null && apiResponse.response() != Void.class) {
response.schema(modelConverters.readAsProperty(apiResponse.response()));
}
if (apiResponse.code() == 0) {
operation.defaultResponse(response);
}
else {
operation.response(apiResponse.code(), response);
}
responseClass = apiResponse.response();
if (responseClass != null && !responseClass.equals(java.lang.Void.class)) {
Map<String, Model> models = modelConverters.read(responseClass);
for (String key : models.keySet()) {
response.schema(new RefProperty().asDefault(key));
swagger.model(key, models.get(key));
}
}
}
}
operation.deprecated(method.isAnnotationPresent(Deprecated.class));
java.lang.reflect.Parameter[] parameters;
Type explicitType = null;
String name, reference;
if (!apiOperation.request().equals(Void.class)) {
Class<?> clazz = apiOperation.request();
if (clazz.getSuperclass() != null && clazz.getSuperclass().equals(TypeToken.class)) {
explicitType = ((ParameterizedType) clazz.getGenericSuperclass()).getActualTypeArguments()[0];
parameters = null;
name = null;
reference = null;
}
else {
parameters = readApiBody(clazz);
name = clazz.getSimpleName();
reference = clazz.getName();
}
}
else if (Arrays.stream(method.getParameters()).anyMatch(p -> p.isAnnotationPresent(BodyParam.class))) {
Class type = Arrays.stream(method.getParameters())
.filter(p -> p.isAnnotationPresent(BodyParam.class))
.findAny().get().getType();
if (modelConverters.readAsProperty(method.getParameters()[0].getParameterizedType()) instanceof ArrayProperty) {
explicitType = type;
parameters = null;
name = null;
reference = null;
}
else {
parameters = readApiBody(type);
name = type.getSimpleName();
reference = type.getName();
}
}
else {
parameters = method.getParameters();
name = methodIdentifier;
reference = methodIdentifier;
}
if (parameters != null && parameters.length > 0) {
if (method.isAnnotationPresent(ApiImplicitParams.class)) {
readImplicitParameters(method, operation);
}
else {
List<String> params = Arrays.asList("string", "number", "integer", "boolean");
List<Property> properties = Arrays.stream(parameters)
.map(parameter ->
parameter.isAnnotationPresent(ApiParam.class) || parameter.isAnnotationPresent(HeaderParam.class) ?
modelConverters.readAsProperty(getActualType(readClass, parameter.getParameterizedType())) : null)
.collect(Collectors.toList());
boolean isSchema = properties.stream().anyMatch(property -> property == null || (params.indexOf(property.getType()) == -1 &&
!((property instanceof ArrayProperty) && params.indexOf(((ArrayProperty) property).getItems().getType()) > -1)));
List<Parameter> list;
if (!isSchema) {
list = readFormParameters(methodName, parameters);
}
else {
list = readMethodParameters(parameters, properties, name, reference);
}
operation.setParameters(list);
}
}
else if (explicitType != null) {
Property property = modelConverters.readAsProperty(explicitType);
BodyParameter bodyParameter = new BodyParameter();
bodyParameter.setName(methodIdentifier);
bodyParameter.setRequired(true);
modelConverters.readAll(explicitType).forEach(swagger::addDefinition);
if (property instanceof ArrayProperty) {
ArrayModel arrayModel = new ArrayModel();
Property items = ((ArrayProperty) property).getItems();
arrayModel.items(items);
swagger.addDefinition(methodIdentifier, arrayModel);
RefModel refModel = new RefModel();
refModel.set$ref(methodIdentifier);
bodyParameter.setSchema(refModel);
}
else {
throw new UnsupportedOperationException();
}
operation.setParameters(ImmutableList.of(bodyParameter));
}
if (operation.getResponses() == null) {
operation.defaultResponse(new Response().description("Successful request"));
}
return operation;
}
private static Type getActualType(Class readClass, Type parameterizedType)
{
// if the parameter has a generic type, it will be read as Object
// so we need to find the actual implementation and return that type.
// the generic type may not come from the HttpService class, if it's not keep track of it:
// ((TypeVariableImpl)parameters[2].getParameterizedType()).getGenericDeclaration().getTypeParameters()[0].getBounds()[0]
if (parameterizedType instanceof TypeVariableImpl) {
TypeVariable[] genericParameters = readClass.getSuperclass().getTypeParameters();
Type[] implementations = ((ParameterizedTypeImpl) readClass.getGenericSuperclass()).getActualTypeArguments();
for (int i = 0; i < genericParameters.length; i++) {
if (genericParameters[i].getName().equals(((TypeVariableImpl) parameterizedType).getName())) {
return implementations[i];
}
}
}
return parameterizedType;
}
private java.lang.reflect.Parameter[] readApiBody(Class<?> type)
{
List<Constructor<?>> constructors = Arrays.stream(type.getConstructors())
.filter(c -> c.isAnnotationPresent(JsonCreator.class))
.collect(Collectors.toList());
if (constructors.size() > 1) {
throw new IllegalArgumentException(format("%s has more then one constructor annotation with @ParamBody. There must be only one.",
type.getSimpleName()));
}
if (constructors.isEmpty()) {
throw new IllegalArgumentException(format("%s doesn't have any constructor annotation with @JsonCreator.",
type.getSimpleName()));
}
java.lang.reflect.Parameter[] parameters = constructors.get(0).getParameters();
// TODO fixme all parameters must have @ApiParam
if (parameters.length > 0 && !parameters[0].isAnnotationPresent(ApiParam.class)) {
throw new IllegalArgumentException(format("%s constructor parameters don't have @ApiParam annotation.",
type.getSimpleName()));
}
Model model = swagger.getDefinitions().get(type.getSimpleName());
if (model == null) {
modelConverters.readAll(type).forEach(swagger::addDefinition);
}
return parameters;
}
private List<Parameter> readMethodParameters(java.lang.reflect.Parameter[] parameters, List<Property> properties, String name, String reference)
{
ImmutableList.Builder<Parameter> builder = ImmutableList.builder();
ModelImpl model = new ModelImpl();
model.setName(name);
model.setReference(reference);
for (int i = 0; i < properties.size(); i++) {
Property property = properties.get(i);
java.lang.reflect.Parameter parameter = parameters[i];
if (parameter.isAnnotationPresent(ApiParam.class)) {
ApiParam ann = parameter.getAnnotation(ApiParam.class);
property.setRequired(ann.required());
if(!ann.description().isEmpty()) {
property.description(ann.defaultValue());
}
if(!ann.access().isEmpty()) {
property.setAccess(ann.defaultValue());
}
if(!ann.defaultValue().isEmpty()) {
property.setDefault(ann.defaultValue());
}
model.addProperty(ann.value(), property);
if (property instanceof RefProperty) {
Map<String, Model> subProperty = modelConverters.read(parameter.getParameterizedType());
String simpleRef = ((RefProperty) property).getSimpleRef();
swagger.addDefinition(simpleRef, subProperty.get(simpleRef));
}
if (property instanceof ArrayProperty) {
ArrayModel arrayModel = new ArrayModel();
Property items = ((ArrayProperty) property).getItems();
arrayModel.items(items);
if (items instanceof RefProperty) {
Type type = ((ParameterizedType) parameter.getParameterizedType()).getActualTypeArguments()[0];
// it reads fields of classes but what we actually want to do is to make the class serializable with Jackson library.
// therefore, it's a better idea to use constructor that has @JsonCreator annotation.
Map<String, Model> read = modelConverters.readAll(type);
model.addProperty(property.getName(), null);
for (Map.Entry<String, Model> entry : read.entrySet()) {
swagger.addDefinition(entry.getKey(), entry.getValue());
}
}
}
}
else if (parameter.isAnnotationPresent(Named.class)) {
continue;
}
else if (parameter.isAnnotationPresent(HeaderParam.class)) {
HeaderParam ann = parameter.getAnnotation(HeaderParam.class);
HeaderParameter headerParameter = new HeaderParameter();
headerParameter.setName(ann.value());
headerParameter.setType(property.getType());
headerParameter.setRequired(ann.required());
if(parameter.getParameterizedType() instanceof Class && ((Class) parameter.getParameterizedType()).isEnum()) {
headerParameter.setEnum(Arrays.stream(((Class) parameter.getParameterizedType())
.getEnumConstants()).map(e -> e.toString())
.collect(Collectors.toList()));
}
builder.add(headerParameter);
}
}
if(model.getProperties() != null) {
BodyParameter bodyParameter = new BodyParameter();
bodyParameter.name(name);
bodyParameter.setSchema(new RefModel(model.getName()));
bodyParameter.setRequired(true);
swagger.addDefinition(name, model);
builder.add(bodyParameter);
}
return builder.build();
}
private List<Parameter> readFormParameters(String methodName, java.lang.reflect.Parameter[] parameters)
{
ModelImpl model = new ModelImpl();
model.setType("object");
Arrays.stream(parameters).filter(p -> p.isAnnotationPresent(ApiParam.class)).forEach(parameter -> {
ApiParam ann = parameter.getAnnotation(ApiParam.class);
Property property = modelConverters.readAsProperty(parameter.getParameterizedType());
property.setRequired(ann.required());
property.setAccess(ann.access());
property.setDefault(ann.defaultValue());
property.setDescription(ann.description());
model.addProperty(ann.value(), property);
});
ImmutableList.Builder<Parameter> builder = ImmutableList.builder();
Arrays.stream(parameters).filter(p -> p.isAnnotationPresent(HeaderParam.class)).forEach(parameter -> {
HeaderParam ann = parameter.getAnnotation(HeaderParam.class);
HeaderParameter headerParameter = new HeaderParameter();
headerParameter.setName(ann.value());
Property property = modelConverters.readAsProperty(parameter.getParameterizedType());
headerParameter.setType(property.getType());
headerParameter.setRequired(ann.required());
if(parameter.getParameterizedType() instanceof Class && ((Class) parameter.getParameterizedType()).isEnum()) {
headerParameter.setEnum(Arrays.stream(((Class) parameter.getParameterizedType())
.getEnumConstants()).map(e -> e.toString())
.collect(Collectors.toList()));
}
builder.add(headerParameter);
});
if (model.getProperties() != null) {
BodyParameter param = new BodyParameter();
param.setRequired(true);
param.setName(methodName);
param.setSchema(model);
builder.add(param);
}
return builder.build();
}
private List<Parameter> getParameters(Class<?> cls, Type type, Annotation[] annotations)
{
// look for path, query
boolean isArray = isMethodArgumentAnArray(cls, type);
List<Parameter> parameters;
LOGGER.debug("getParameters for " + cls);
Set<Class<?>> classesToSkip = new HashSet<>();
parameters = extractParameters(annotations, type);
if (parameters.size() > 0) {
for (Parameter parameter : parameters) {
applyAnnotations(swagger, parameter, cls, annotations, isArray);
}
}
else {
LOGGER.debug("no parameter found, looking at body params");
if (classesToSkip.contains(cls) == false) {
if (type instanceof ParameterizedType) {
ParameterizedType ti = (ParameterizedType) type;
Type innerType = ti.getActualTypeArguments()[0];
if (innerType instanceof Class) {
Parameter param = applyAnnotations(swagger, null, (Class) innerType, annotations, isArray);
if (param != null) {
parameters.add(param);
}
}
}
else {
Parameter param = applyAnnotations(swagger, null, cls, annotations, isArray);
if (param != null) {
parameters.add(param);
}
}
}
}
return parameters;
}
public List<Parameter> extractParameters(Annotation[] annotations, Type type)
{
String defaultValue = null;
List<Parameter> parameters = new ArrayList<>();
Parameter parameter = null;
for (Annotation annotation : annotations) {
if (annotation instanceof ApiParam) {
FormParameter qp = new FormParameter()
.name(((ApiParam) annotation).value());
qp.setDefaultValue(defaultValue);
Property schema = modelConverters.readAsProperty(type);
if (schema != null) {
qp.setProperty(schema);
if (schema instanceof ArrayProperty) {
qp.setItems(((ArrayProperty) schema).getItems());
}
}
parameter = qp;
}
}
if (parameter != null) {
parameters.add(parameter);
}
return parameters;
}
public String extractOperationMethod(ApiOperation apiOperation, Method method)
{
if (apiOperation.httpMethod() != null && !"".equals(apiOperation.httpMethod())) {
return apiOperation.httpMethod().toLowerCase();
}
else if (method.getAnnotation(javax.ws.rs.GET.class) != null) {
return "get";
}
else if (method.getAnnotation(javax.ws.rs.PUT.class) != null) {
return "put";
}
else if (method.getAnnotation(javax.ws.rs.POST.class) != null) {
return "post";
}
else if (method.getAnnotation(javax.ws.rs.DELETE.class) != null) {
return "delete";
}
else if (method.getAnnotation(javax.ws.rs.OPTIONS.class) != null) {
return "options";
}
else if (method.getAnnotation(javax.ws.rs.HEAD.class) != null) {
return "patch";
}
else if (method.getAnnotation(HttpMethod.class) != null) {
HttpMethod httpMethod = method.getAnnotation(HttpMethod.class);
return httpMethod.value().toLowerCase();
}
return "post";
}
boolean isPrimitive(Type cls)
{
boolean out = false;
Property property = modelConverters.readAsProperty(cls);
if (property == null) {
out = false;
}
else if ("integer".equals(property.getType())) {
out = true;
}
else if ("string".equals(property.getType())) {
out = true;
}
else if ("number".equals(property.getType())) {
out = true;
}
else if ("boolean".equals(property.getType())) {
out = true;
}
else if ("array".equals(property.getType())) {
out = true;
}
else if ("file".equals(property.getType())) {
out = true;
}
return out;
}
public Parameter applyAnnotations(Swagger swagger, Parameter parameter, Class<?> cls, Annotation[] annotations, boolean isArray)
{
boolean shouldIgnore = false;
String allowableValues;
Optional<Annotation> any = Arrays.stream(annotations).filter(ann -> ann instanceof ApiParam).findAny();
if (any.isPresent()) {
ApiParam param = (ApiParam) any.get();
if (parameter != null) {
parameter.setRequired(param.required());
if (param.value() != null && !"".equals(param.value())) {
parameter.setName(param.value());
}
parameter.setDescription(param.value());
parameter.setAccess(param.access());
allowableValues = param.allowableValues();
if (allowableValues != null) {
if (allowableValues.startsWith("range")) {
// TODO handle range
}
else {
String[] values = allowableValues.split(",");
List<String> _enum = new ArrayList<>();
for (String value : values) {
String trimmed = value.trim();
if (!trimmed.equals("")) {
_enum.add(trimmed);
}
}
if (parameter instanceof SerializableParameter) {
SerializableParameter p = (SerializableParameter) parameter;
if (_enum.size() > 0) {
p.setEnum(_enum);
}
}
}
}
}
else if (shouldIgnore == false) {
// must be a body param
BodyParameter bp = new BodyParameter();
if (param.value() != null && !"".equals(param.value())) {
bp.setName(param.value());
}
else {
bp.setName("body");
}
bp.setDescription(param.value());
if (cls.isArray() || isArray) {
Class<?> innerType;
if (isArray) {// array has already been detected
innerType = cls;
}
else {
innerType = cls.getComponentType();
}
LOGGER.debug("inner type: " + innerType + " from " + cls);
Property innerProperty = modelConverters.readAsProperty(innerType);
if (innerProperty == null) {
Map<String, Model> models = modelConverters.read(innerType);
if (models.size() > 0) {
for (String name : models.keySet()) {
if (name.indexOf("java.util") == -1) {
bp.setSchema(
new ArrayModel().items(new RefProperty().asDefault(name)));
if (swagger != null) {
swagger.addDefinition(name, models.get(name));
}
}
}
}
models = modelConverters.readAll(innerType);
if (swagger != null) {
for (String key : models.keySet()) {
swagger.model(key, models.get(key));
}
}
}
else {
LOGGER.debug("found inner property " + innerProperty);
bp.setSchema(new ArrayModel().items(innerProperty));
// creation of ref property doesn't add model to definitions - do it now instead
if (innerProperty instanceof RefProperty && swagger != null) {
Map<String, Model> models = modelConverters.read(innerType);
String name = ((RefProperty) innerProperty).getSimpleRef();
swagger.addDefinition(name, models.get(name));
LOGGER.debug("added model definition for RefProperty " + name);
}
}
}
else {
Map<String, Model> models = modelConverters.read(cls);
if (models.size() > 0) {
for (String name : models.keySet()) {
if (name.indexOf("java.util") == -1) {
if (isArray) {
bp.setSchema(new ArrayModel().items(new RefProperty().asDefault(name)));
}
else {
bp.setSchema(new RefModel().asDefault(name));
}
if (swagger != null) {
swagger.addDefinition(name, models.get(name));
}
}
}
models = modelConverters.readAll(cls);
if (swagger != null) {
for (String key : models.keySet()) {
swagger.model(key, models.get(key));
}
}
}
else {
Property prop = modelConverters.readAsProperty(cls);
if (prop != null) {
ModelImpl model = new ModelImpl();
model.setType(prop.getType());
bp.setSchema(model);
}
}
}
parameter = bp;
}
}
return parameter;
}
public static boolean isMethodArgumentAnArray(final Class<?> paramClass, final Type paramGenericType)
{
final Class<?>[] interfaces = paramClass.getInterfaces();
boolean isArray = false;
for (final Class<?> aCls : interfaces) {
if (List.class.equals(aCls)) {
isArray = true;
break;
}
}
if (paramGenericType instanceof ParameterizedType) {
final Type[] parameterArgTypes = ((ParameterizedType) paramGenericType).getActualTypeArguments();
Class<?> testClass = paramClass;
for (Type parameterArgType : parameterArgTypes) {
if (testClass.isAssignableFrom(List.class)) {
isArray = true;
break;
}
testClass = (Class<?>) parameterArgType;
}
}
return isArray;
}
}