All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.swagger.jaxrs2.Reader Maven / Gradle / Ivy

There is a newer version: 2.0.0-rc2
Show newest version
package io.swagger.jaxrs2;

import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.introspect.AnnotatedParameter;
import com.fasterxml.jackson.databind.type.TypeFactory;
import io.swagger.converter.ModelConverters;
import io.swagger.jaxrs2.ext.OpenAPIExtension;
import io.swagger.jaxrs2.ext.OpenAPIExtensions;
import io.swagger.jaxrs2.util.ReaderUtils;
import io.swagger.oas.integration.ContextUtils;
import io.swagger.oas.integration.SwaggerConfiguration;
import io.swagger.oas.integration.api.OpenAPIConfiguration;
import io.swagger.oas.integration.api.OpenApiReader;
import io.swagger.oas.models.Components;
import io.swagger.oas.models.OpenAPI;
import io.swagger.oas.models.Operation;
import io.swagger.oas.models.PathItem;
import io.swagger.oas.models.Paths;
import io.swagger.oas.models.callbacks.Callback;
import io.swagger.oas.models.media.Content;
import io.swagger.oas.models.media.MediaType;
import io.swagger.oas.models.media.Schema;
import io.swagger.oas.models.parameters.Parameter;
import io.swagger.oas.models.parameters.RequestBody;
import io.swagger.oas.models.responses.ApiResponse;
import io.swagger.oas.models.responses.ApiResponses;
import io.swagger.oas.models.security.SecurityScheme;
import io.swagger.oas.models.tags.Tag;
import io.swagger.util.Json;
import io.swagger.util.ParameterProcessor;
import io.swagger.util.PathUtils;
import io.swagger.util.ReflectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;

public class Reader implements OpenApiReader {
    private static final Logger LOGGER = LoggerFactory.getLogger(Reader.class);
    public static final String DEFAULT_MEDIA_TYPE_VALUE = "*/*";

    protected OpenAPIConfiguration config;

    private OpenAPI openAPI;
    private Components components;
    private Paths paths;
    private Set openApiTags;
    javax.ws.rs.Consumes classConsumes;
    javax.ws.rs.Produces classProduces;
    javax.ws.rs.Produces methodProduces;

    private static final String GET_METHOD = "get";
    private static final String POST_METHOD = "post";
    private static final String PUT_METHOD = "put";
    private static final String DELETE_METHOD = "delete";
    private static final String PATCH_METHOD = "patch";
    private static final String TRACE_METHOD = "trace";
    private static final String HEAD_METHOD = "head";
    private static final String OPTIONS_METHOD = "options";

    public Reader() {
        this.openAPI = new OpenAPI();
        paths = new Paths();
        openApiTags = new LinkedHashSet<>();
        components = new Components();

    }

    public Reader(OpenAPI openAPI) {
        this();
        setConfiguration(new SwaggerConfiguration().openAPI(openAPI));
    }

    public Reader(OpenAPIConfiguration openApiConfiguration) {
        this();
        setConfiguration(openApiConfiguration);
    }

    public OpenAPI getOpenAPI() {
        return openAPI;
    }

    /**
     * Scans a single class for Swagger annotations - does not invoke ReaderListeners
     */
    public OpenAPI read(Class cls) {
        return read(cls, "");
    }

    /**
     * Scans a set of classes for both ReaderListeners and OpenAPI annotations. All found listeners will
     * be instantiated before any of the classes are scanned for OpenAPI annotations - so they can be invoked
     * accordingly.
     *
     * @param classes a set of classes to scan
     * @return the generated OpenAPI definition
     */
    public OpenAPI read(Set> classes) {
        Set> sortedClasses = new TreeSet<>(new Comparator>() {
            @Override
            public int compare(Class class1, Class class2) {
                if (class1.equals(class2)) {
                    return 0;
                } else if (class1.isAssignableFrom(class2)) {
                    return -1;
                } else if (class2.isAssignableFrom(class1)) {
                    return 1;
                }
                return class1.getName().compareTo(class2.getName());
            }
        });
        sortedClasses.addAll(classes);

        for (Class cls : sortedClasses) {
            read(cls, "");
        }

        return openAPI;
    }

    @Override
    public void setConfiguration(OpenAPIConfiguration openApiConfiguration) {
        if (openApiConfiguration != null) {
            this.config = ContextUtils.deepCopy(openApiConfiguration);
            if (openApiConfiguration.getOpenAPI() != null) {
                this.openAPI = this.config.getOpenAPI();
            }
        }
    }

    public OpenAPI read(Set> classes, Map resources) {
        return read(classes);
    }

    public OpenAPI read(Class cls, String parentPath) {
        io.swagger.oas.annotations.security.SecurityScheme apiSecurityScheme = ReflectionUtils.getAnnotation(cls, io.swagger.oas.annotations.security.SecurityScheme.class);
        io.swagger.oas.annotations.ExternalDocumentation apiExternalDocs = ReflectionUtils.getAnnotation(cls, io.swagger.oas.annotations.ExternalDocumentation.class);
        io.swagger.oas.annotations.info.Info apiInfo = ReflectionUtils.getAnnotation(cls, io.swagger.oas.annotations.info.Info.class);
        classConsumes = ReflectionUtils.getAnnotation(cls, javax.ws.rs.Consumes.class);
        classProduces = ReflectionUtils.getAnnotation(cls, javax.ws.rs.Produces.class);

        Optional securityScheme = SecurityParser.getSecurityScheme(apiSecurityScheme);
        if (securityScheme.isPresent()) {
            Map securitySchemeMap = new HashMap<>();
            if (StringUtils.isNotBlank(securityScheme.get().getName())) {
                securitySchemeMap.put(securityScheme.get().getName(), securityScheme.get());
                if (components.getSecuritySchemes() != null && components.getSecuritySchemes().size() != 0) {
                    components.getSecuritySchemes().putAll(securitySchemeMap);
                } else {
                    components.setSecuritySchemes(securitySchemeMap);
                }
            }
        }

        final javax.ws.rs.Path apiPath = ReflectionUtils.getAnnotation(cls, javax.ws.rs.Path.class);

        JavaType classType = TypeFactory.defaultInstance().constructType(cls);
        BeanDescription bd = Json.mapper().getSerializationConfig().introspect(classType);

        final List globalParameters = new ArrayList<>();

        // look for constructor-level annotated properties
        globalParameters.addAll(ReaderUtils.collectConstructorParameters(cls, openAPI));

        // look for field-level annotated properties
        globalParameters.addAll(ReaderUtils.collectFieldParameters(cls, openAPI));

        Method methods[] = cls.getMethods();
        for (Method method : methods) {
            RequestBody requestBody = new RequestBody();
            AnnotatedMethod annotatedMethod = bd.findMethod(method.getName(), method.getParameterTypes());
            methodProduces = ReflectionUtils.getAnnotation(method, javax.ws.rs.Produces.class);

            if (ReflectionUtils.isOverriddenMethod(method, cls)) {
                continue;
            }
            javax.ws.rs.Path methodPath = ReflectionUtils.getAnnotation(method, javax.ws.rs.Path.class);
            javax.ws.rs.Consumes methodConsumes = ReflectionUtils.getAnnotation(method, javax.ws.rs.Consumes.class);

            String operationPath = ReaderUtils.getPath(apiPath, methodPath, parentPath);

            Map regexMap = new LinkedHashMap<>();
            operationPath = PathUtils.parsePath(operationPath, regexMap);
            if (operationPath != null) {
                if (ReaderUtils.isIgnored(operationPath, config)) {
                    continue;
                }

                Operation operation = parseMethod(method);
                if (operation != null) {
                    PathItem pathItemObject;
                    if (openAPI.getPaths() != null && openAPI.getPaths().get(operationPath) != null) {
                        pathItemObject = openAPI.getPaths().get(operationPath);
                    } else {
                        pathItemObject = new PathItem();
                    }

                    String httpMethod = ReaderUtils.extractOperationMethod(operation, method, OpenAPIExtensions.chain());
                    if (StringUtils.isNotBlank(httpMethod)) {
                        setPathItemOperation(pathItemObject, httpMethod, operation);
                    }
                    List operationParameters = new ArrayList<>();
                    Annotation[][] paramAnnotations = ReflectionUtils.getParameterAnnotations(method);
                    if (annotatedMethod == null) {
                        Type[] genericParameterTypes = method.getGenericParameterTypes();
                        for (int i = 0; i < genericParameterTypes.length; i++) {
                            final Type type = TypeFactory.defaultInstance().constructType(genericParameterTypes[i], cls);
                            operationParameters.addAll(getParameters(type, Arrays.asList(paramAnnotations[i])));
                        }
                    } else {
                        for (int i = 0; i < annotatedMethod.getParameterCount(); i++) {
                            AnnotatedParameter param = annotatedMethod.getParameter(i);
                            final Type type = TypeFactory.defaultInstance().constructType(param.getParameterType(), cls);
                            List parameters = getParameters(type, Arrays.asList(paramAnnotations[i]));
                            for (Parameter parameter : parameters) {
                                Schema parameterSchema = parameter.getSchema();
                                if (StringUtils.isNotBlank(parameter.getIn())) {
                                    operationParameters.add(parameter);
                                } else {
                                    boolean isRequestBodyEmpty = true;
                                    if (StringUtils.isNotBlank(parameter.get$ref())) {
                                        requestBody.set$ref(parameter.get$ref());
                                        isRequestBodyEmpty = false;
                                    }
                                    if (StringUtils.isNotBlank(parameter.getDescription())) {
                                        requestBody.setDescription(parameter.getDescription());
                                        isRequestBodyEmpty = false;
                                    }
                                    if (Boolean.TRUE.equals(parameter.getRequired())) {
                                        requestBody.setRequired(parameter.getRequired());
                                        isRequestBodyEmpty = false;
                                    }

                                    if (parameter.getSchema() != null) {
                                        Content content = new Content();
                                        if (methodConsumes != null) {
                                            for (String value : methodConsumes.value()) {
                                                setMediaTypeToContent(parameter.getSchema(), content, value);
                                            }
                                        } else if (classConsumes != null) {
                                            for (String value : classConsumes.value()) {
                                                setMediaTypeToContent(parameter.getSchema(), content, value);
                                            }
                                        } else {
                                            setMediaTypeToContent(parameter.getSchema(), content, DEFAULT_MEDIA_TYPE_VALUE);
                                        }

                                        requestBody.setContent(content);
                                        isRequestBodyEmpty = false;
                                    }
                                    if (!isRequestBodyEmpty) {
                                        if (parameterSchema != null) {
                                            Map schemaMap = ModelConverters.getInstance().readAll(type);
                                            schemaMap.forEach((key, schema) -> {
                                                components.addSchemas(key, schema);
                                            });
                                        }
                                        operation.setRequestBody(requestBody);
                                    }

                                }
                            }
                        }
                    }
                    if (operationParameters.size() > 0) {
                        operation.setParameters(operationParameters);
                    }

                    paths.addPathItem(operationPath, pathItemObject);
                    if (openAPI.getPaths() != null) {
                        this.paths.putAll(openAPI.getPaths());
                    }

                    openAPI.setPaths(this.paths);
                }
            }
        }

        if (components.getSecuritySchemes() != null && components.getSecuritySchemes().size() > 0 ||
                components.getSchemas() != null && components.getSchemas().size() > 0) {
            openAPI.setComponents(components);
        }

        if (!openApiTags.isEmpty()) {
            Set tagsSet = new LinkedHashSet<>();
            tagsSet.addAll(openApiTags);
            if (openAPI.getTags() != null) {
                tagsSet.addAll(openAPI.getTags());
            }
            openAPI.setTags(new ArrayList<>(tagsSet));
        }

        OperationParser.getExternalDocumentation(apiExternalDocs).ifPresent(externalDocumentation -> openAPI.setExternalDocs(externalDocumentation));
        OperationParser.getInfo(apiInfo).ifPresent(info -> openAPI.setInfo(info));

        return openAPI;
    }

    private void setMediaTypeToContent(Schema schema, Content content, String value) {
        MediaType mediaTypeObject = new MediaType();
        mediaTypeObject.setSchema(schema);
        content.addMediaType(value, mediaTypeObject);
    }

    public Operation parseMethod(Method method) {
        JavaType classType = TypeFactory.defaultInstance().constructType(method.getDeclaringClass());
        return parseMethod(classType.getClass(), method);
    }

    private Operation parseMethod(Class cls, Method method) {
        Operation operation = new Operation();
        io.swagger.oas.annotations.Operation apiOperation = ReflectionUtils.getAnnotation(method, io.swagger.oas.annotations.Operation.class);
        io.swagger.oas.annotations.callbacks.Callback apiCallback = ReflectionUtils.getAnnotation(method, io.swagger.oas.annotations.callbacks.Callback.class);
        io.swagger.oas.annotations.security.SecurityRequirement apiSecurity = ReflectionUtils.getAnnotation(method, io.swagger.oas.annotations.security.SecurityRequirement.class);

        if (apiOperation != null) {

            Map callbacks = getCallbacks(apiCallback);

            if (callbacks.size() > 0) {
                operation.setCallbacks(callbacks);
            }
            SecurityParser.getSecurityRequirement(apiSecurity).ifPresent(operation::setSecurity);

            setOperationObjectFromApiOperationAnnotation(operation, apiOperation);
            if (StringUtils.isBlank(operation.getOperationId())) {
                operation.setOperationId(method.getName());
            }
        } else { // TODO #2312 - return an operation also if no Operation annotation, but @GET / @POST .. or @PATH
            return null;
        }
        // handle return type, add as response in case.
        Type returnType = method.getGenericReturnType();
        if (!shouldIgnoreClass(returnType.getTypeName())) {
            // TODO #2312 also add content to existing responses (from annotation) if it is not specified in annotation
            Map schemaMap = ModelConverters.getInstance().read(returnType);
            if (schemaMap != null  && !schemaMap.values().isEmpty()) {
                Schema returnTypeSchema = schemaMap.values().iterator().next();
                if (operation.getResponses() == null) {
                    operation.responses(
                            new ApiResponses()._default(
                                    new ApiResponse()
                                            .content(
                                                    new Content()
                                                        .addMediaType("*/*",
                                                                new MediaType()
                                                                .schema(new Schema().$ref(returnTypeSchema.getName())
                                                        )
                                                    )
                                            )
                            )
                    );
                }
                if (operation.getResponses().getDefault() != null &&
                        StringUtils.isBlank(operation.getResponses().getDefault().get$ref()) &&
                        operation.getResponses().getDefault().getContent() == null) {
                    operation.getResponses().getDefault().content(new Content()
                            .addMediaType("*/*",
                                    new MediaType()
                                            .schema(new Schema().$ref(returnTypeSchema.getName())
                                            )
                            )
                    );
                }
                schemaMap = ModelConverters.getInstance().readAll(returnType);
                schemaMap.forEach((key, schema) -> components.addSchemas(key, schema));

            }
        }

        return operation;
    }

    private boolean shouldIgnoreClass(String className) {
        if (StringUtils.isBlank(className)) {
            return true;
        }
        boolean ignore = false;
        ignore = ignore || className.startsWith("javax.ws.rs.");
        ignore = ignore || className.equalsIgnoreCase("void");
        return ignore;
    }

    private Map getCallbacks(io.swagger.oas.annotations.callbacks.Callback apiCallback) {
        Map callbackMap = new HashMap<>();
        if (apiCallback == null) {
            return callbackMap;
        }
        Callback callbackObject = new Callback();
        PathItem pathItemObject = new PathItem();
        for (io.swagger.oas.annotations.Operation callbackOperation : apiCallback.operation()) {
            Operation callbackNewOperation = new Operation();
            setOperationObjectFromApiOperationAnnotation(callbackNewOperation, callbackOperation);
            setPathItemOperation(pathItemObject, callbackOperation.method(), callbackNewOperation);
        }

        callbackObject.addPathItem(apiCallback.callbackUrlExpression(), pathItemObject);
        callbackMap.put(apiCallback.name(), callbackObject);

        return callbackMap;
    }

    private void setPathItemOperation(PathItem pathItemObject, String method, Operation callbackNewOperation) {
        switch (method) {
            case POST_METHOD:
                pathItemObject.post(callbackNewOperation);
                break;
            case GET_METHOD:
                pathItemObject.get(callbackNewOperation);
                break;
            case DELETE_METHOD:
                pathItemObject.delete(callbackNewOperation);
                break;
            case PUT_METHOD:
                pathItemObject.put(callbackNewOperation);
                break;
            case PATCH_METHOD:
                pathItemObject.patch(callbackNewOperation);
                break;
            case TRACE_METHOD:
                pathItemObject.trace(callbackNewOperation);
                break;
            case HEAD_METHOD:
                pathItemObject.head(callbackNewOperation);
                break;
            case OPTIONS_METHOD:
                pathItemObject.options(callbackNewOperation);
                break;
            default:
                // Do nothing here
                break;
        }
    }

    private void setOperationObjectFromApiOperationAnnotation(Operation operation, io.swagger.oas.annotations.Operation apiOperation) {
        if (StringUtils.isNotBlank(apiOperation.summary())) {
            operation.setSummary(apiOperation.summary());
        }
        if (StringUtils.isNotBlank(apiOperation.description())) {
            operation.setDescription(apiOperation.description());
        }
        if (StringUtils.isNotBlank(apiOperation.operationId())) {
            operation.setOperationId(getOperationId(apiOperation.operationId()));
        }
        if (apiOperation.deprecated()) {
            operation.setDeprecated(apiOperation.deprecated());
        }
        ReaderUtils.getStringListFromStringArray(apiOperation.tags()).ifPresent(operation::setTags);
        OperationParser.getTags(apiOperation.tags()).ifPresent(tag -> openApiTags.addAll(tag));
        OperationParser.getExternalDocumentation(apiOperation.externalDocs()).ifPresent(operation::setExternalDocs);
        OperationParser.getRequestBody(apiOperation.requestBody(), components).ifPresent(operation::setRequestBody);
        OperationParser.getApiResponses(apiOperation.responses(), classProduces, methodProduces, components).ifPresent(operation::setResponses);
        OperationParser.getServers(apiOperation.servers()).ifPresent(operation::setServers);
        OperationParser.getParametersList(apiOperation.parameters(), components).ifPresent(operation::setParameters);
    }

    protected String getOperationId(String operationId) {
        boolean operationIdUsed = existOperationId(operationId);
        String operationIdToFind = null;
        int counter = 0;
        while (operationIdUsed) {
            operationIdToFind = String.format("%s_%d", operationId, ++counter);
            operationIdUsed = existOperationId(operationIdToFind);
        }
        if (operationIdToFind != null) {
            operationId = operationIdToFind;
        }
        return operationId;
    }

    private boolean existOperationId(String operationId) {
        if (openAPI == null) {
            return false;
        }
        if (openAPI.getPaths() == null || openAPI.getPaths().isEmpty()) {
            return false;
        }
        for (PathItem path : openAPI.getPaths().values()) {
            String pathOperationId = extractOperationIdFromPathItem(path);
            if (operationId.equalsIgnoreCase(pathOperationId)) {
                return true;
            }

        }
        return false;
    }

    private List getParameters(Type type, List annotations) {
        final Iterator chain = OpenAPIExtensions.chain();
        if (!chain.hasNext()) {
            return Collections.emptyList();
        }
        LOGGER.debug("getParameters for {}", type);
        Set typesToSkip = new HashSet<>();
        final OpenAPIExtension extension = chain.next();
        LOGGER.debug("trying extension {}", extension);

        final List parameters = extension.extractParameters(annotations, type, typesToSkip, chain);
        if (!parameters.isEmpty()) {
            final List processed = new ArrayList<>(parameters.size());
            for (Parameter parameter : parameters) {
                if (ParameterProcessor.applyAnnotations(openAPI, parameter, type, annotations) != null) {
                    processed.add(parameter);
                    Map schemaMap = ModelConverters.getInstance().readAll(type);
                    schemaMap.forEach((key, schema) -> components.addSchemas(key, schema));
                }
            }
            return processed;
        } else {
            LOGGER.debug("no parameter found, looking at body params");
            final List body = new ArrayList<>();
            if (!typesToSkip.contains(type)) {
                Parameter param = ParameterProcessor.applyAnnotations(openAPI, null, type, annotations);
                if (param != null) {
                    body.add(param);
                }
            }
            return body;
        }
    }

    private void mergeComponents(OpenAPI openAPI, boolean isComponentEmpty) {
        Components openAPIComponent = openAPI.getComponents();
        if (!isComponentEmpty) {
            if (openAPIComponent != null) {
                if (components.getCallbacks() != null) {
                    components.getCallbacks().putAll(openAPIComponent.getCallbacks());
                }
                if (components.getExamples() != null) {
                    components.getExamples().putAll(openAPIComponent.getExamples());
                }
                if (components.getExtensions() != null) {
                    components.getExtensions().putAll(openAPIComponent.getExtensions());
                }
                if (components.getHeaders() != null) {
                    components.getHeaders().putAll(openAPIComponent.getHeaders());
                }
                if (components.getLinks() != null) {
                    components.getLinks().putAll(openAPIComponent.getLinks());
                }
                if (components.getParameters() != null) {
                    components.getParameters().putAll(openAPIComponent.getParameters());
                }
                if (components.getRequestBodies() != null) {
                    components.getRequestBodies().putAll(openAPIComponent.getRequestBodies());
                }
                if (components.getResponses() != null) {
                    components.getResponses().putAll(openAPIComponent.getResponses());
                }
                if (components.getSchemas() != null) {
                    components.getSchemas().putAll(openAPIComponent.getSchemas());
                }

            }
            openAPI.setComponents(components);
        }
    }

    private String extractOperationIdFromPathItem(PathItem path) {
        if (path.getGet() != null) {
            return path.getGet().getOperationId();
        } else if (path.getPost() != null) {
            return path.getPost().getOperationId();
        } else if (path.getPut() != null) {
            return path.getPut().getOperationId();
        } else if (path.getDelete() != null) {
            return path.getDelete().getOperationId();
        } else if (path.getOptions() != null) {
            return path.getOptions().getOperationId();
        } else if (path.getHead() != null) {
            return path.getHead().getOperationId();
        } else if (path.getPatch() != null) {
            return path.getPatch().getOperationId();
        }
        return "";
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy