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

io.micronaut.openapi.visitor.AbstractOpenApiVisitor Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017-2020 original authors
 *
 * Licensed 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
 *
 * https://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 io.micronaut.openapi.visitor;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import io.micronaut.annotation.processing.visitor.JavaClassElementExt;
import io.micronaut.core.annotation.AnnotationClassValue;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.beans.BeanMap;
import io.micronaut.core.bind.annotation.Bindable;
import io.micronaut.core.naming.NameUtils;
import io.micronaut.core.reflect.ClassUtils;
import io.micronaut.core.reflect.ReflectionUtils;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ArrayUtils;
import io.micronaut.core.util.CollectionUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.http.MediaType;
import io.micronaut.http.uri.UriMatchTemplate;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.ast.Element;
import io.micronaut.inject.ast.ElementModifier;
import io.micronaut.inject.ast.EnumElement;
import io.micronaut.inject.ast.FieldElement;
import io.micronaut.inject.ast.MemberElement;
import io.micronaut.inject.ast.PropertyElement;
import io.micronaut.inject.ast.TypedElement;
import io.micronaut.inject.visitor.VisitorContext;
import io.micronaut.openapi.javadoc.JavadocDescription;
import io.micronaut.openapi.javadoc.JavadocParser;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.core.util.PrimitiveType;
import io.swagger.v3.core.util.Yaml;
import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.enums.ParameterStyle;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.headers.Header;
import io.swagger.v3.oas.annotations.links.Link;
import io.swagger.v3.oas.annotations.links.LinkParameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.DiscriminatorMapping;
import io.swagger.v3.oas.annotations.media.ExampleObject;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.OAuthScope;
import io.swagger.v3.oas.annotations.servers.ServerVariable;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.ComposedSchema;
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;

import org.reactivestreams.Publisher;

import javax.annotation.Nullable;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Email;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.Negative;
import javax.validation.constraints.NegativeOrZero;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Positive;
import javax.validation.constraints.PositiveOrZero;
import javax.validation.constraints.Size;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.math.BigDecimal;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toMap;

/**
 * Abstract base class for OpenAPI visitors.
 *
 * @author graemerocher
 * @since 1.0
 */
abstract class AbstractOpenApiVisitor  {
    static final String ATTR_TEST_MODE = "io.micronaut.OPENAPI_TEST";
    static final String ATTR_OPENAPI = "io.micronaut.OPENAPI";
    static OpenAPI testReference;

    private static final Lock VISITED_ELEMENTS_LOCK = new ReentrantLock();
    private static final String ATTR_VISITED_ELEMENTS = "io.micronaut.OPENAPI.visited.elements";

    /**
     * The JSON mapper.
     */
    ObjectMapper jsonMapper = Json.mapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
    /**
     * The YAML mapper.
     */
    ObjectMapper yamlMapper = Yaml.mapper();

    /**
     * Stores the current in progress type.
     */
    private List inProgressSchemas = new ArrayList<>(10);

    /**
     * Increments the number of visited elements.
     * @param context The context
     */
    void incrementVisitedElements(VisitorContext context) {
        VISITED_ELEMENTS_LOCK.lock();
        try {
            context.put(ATTR_VISITED_ELEMENTS, Integer.valueOf(getVisitedElements(context).intValue() + 1));
        } finally {
            VISITED_ELEMENTS_LOCK.unlock();
        }

    }

    /**
     * Returns the number of visited elements.
     * @param context The context.
     * @return The number of visited elements.
     */
    int visitedElements(VisitorContext context) {
        VISITED_ELEMENTS_LOCK.lock();
        try {
            return getVisitedElements(context);
        } finally {
            VISITED_ELEMENTS_LOCK.unlock();
        }
    }

    private static Integer getVisitedElements(VisitorContext context) {
        Integer visitedElements = context.get(ATTR_VISITED_ELEMENTS, Integer.class).orElse(null);
        if (visitedElements == null) {
            visitedElements = Integer.valueOf(0);
            context.put(ATTR_VISITED_ELEMENTS, visitedElements);
        }
        return visitedElements;
    }

    /**
     * Convert the given map to a JSON node.
     *
     * @param values The values
     * @param context The visitor context
     * @return The node
     */
    JsonNode toJson(Map values, VisitorContext context) {
        Map newValues = toValueMap(values, context);
        return jsonMapper.valueToTree(newValues);
    }

    /**
     * Convert the given Map to a JSON node and then to the specified type.
     * @param  The output class type
     * @param values The values
     * @param context The visitor context
     * @param type The class
     * @return The converted instance
     */
     Optional toValue(Map values, VisitorContext context, Class type) {
        JsonNode node = toJson(values, context);
        try {
            return Optional.of(treeToValue(node, type));
        } catch (JsonProcessingException e) {
            context.warn("Error converting  [" + node + "]: to " + type + ": " + e.getMessage(), null);
        }
        return Optional.empty();
    }

    /**
     * Reads the security requirements annotation of the specified element.
     * @param element The Element to process.
     * @return A list of SecurityRequirement
     */
    List readSecurityRequirements(Element element) {
        return element.getAnnotationValuesByType(io.swagger.v3.oas.annotations.security.SecurityRequirement.class)
                .stream()
                .map(this::mapToSecurityRequirement)
                .collect(Collectors.toList());
    }

    /**
     * Resolve the PathItem for the given {@link UriMatchTemplate}.
     *
     * @param context The context
     * @param matchTemplate The match template
     * @return The {@link PathItem}
     */
    PathItem resolvePathItem(VisitorContext context, UriMatchTemplate matchTemplate) {
        OpenAPI openAPI = resolveOpenAPI(context);
        Paths paths = openAPI.getPaths();
        if (paths == null) {
            paths = new Paths();
            openAPI.setPaths(paths);
        }

        final String pathString = matchTemplate.toPathString();
        return paths.computeIfAbsent(pathString, key -> new PathItem());
    }

    /**
     * Resolve the {@link OpenAPI} instance.
     *
     * @param context The context
     * @return The {@link OpenAPI} instance
     */
    OpenAPI resolveOpenAPI(VisitorContext context) {
        OpenAPI openAPI = context.get(ATTR_OPENAPI, OpenAPI.class).orElse(null);
        if (openAPI == null) {
            openAPI = new OpenAPI();
            context.put(ATTR_OPENAPI, openAPI);
            if (Boolean.getBoolean(ATTR_TEST_MODE)) {
                testReference = openAPI;
            }
        }
        return openAPI;
    }

    /**
     * Converts Json node into a class' instance or throws 'com.fasterxml.jackson.core.JsonProcessingException', adds extensions if present.
     * @param jn The json node
     * @param clazz The output class instance
     * @param  The output class type
     * @return The converted instance
     * @throws JsonProcessingException if error
     */
    protected  T treeToValue(JsonNode jn, Class clazz) throws JsonProcessingException {
        T value = jsonMapper.treeToValue(jn, clazz);
        if (value != null) {
            resolveExtensions(jn).ifPresent(extensions -> BeanMap.of(value).put("extensions", extensions));
        }
        return value;
    }

    /**
     * Convert the values to a map.
     * @param values The values
     * @param context The visitor context
     * @return The map
     */
    protected Map toValueMap(Map values, VisitorContext context) {
        Map newValues = new HashMap<>(values.size());
        for (Map.Entry entry : values.entrySet()) {
            CharSequence key = entry.getKey();
            Object value = entry.getValue();

            if (value instanceof AnnotationValue) {
                AnnotationValue av = (AnnotationValue) value;
                if (av.getAnnotationName().equals(io.swagger.v3.oas.annotations.media.ArraySchema.class.getName())) {
                    final Map valueMap = resolveArraySchemaAnnotationValues(context, av);
                    newValues.put("schema", valueMap);
                } else {
                    final Map valueMap = resolveAnnotationValues(context, av);
                    newValues.put(key, valueMap);
                }
            } else if (value instanceof AnnotationClassValue) {
                AnnotationClassValue acv = (AnnotationClassValue) value;
                final Optional> type = acv.getType();
                type.ifPresent(aClass -> newValues.put(key, aClass));
            } else if (value != null) {
                if (value.getClass().isArray()) {
                    Object[] a = (Object[]) value;
                    if (ArrayUtils.isNotEmpty(a)) {
                        Object first = a[0];
                        boolean areAnnotationValues = first instanceof AnnotationValue;
                        boolean areClassValues = first instanceof AnnotationClassValue;

                        if (areClassValues) {
                            List classes = new ArrayList<>(a.length);
                            for (Object o : a) {
                                AnnotationClassValue acv = (AnnotationClassValue) o;
                                acv.getType().ifPresent(classes::add);
                            }
                            newValues.put(key, classes);
                        } else if (areAnnotationValues) {
                            String annotationName = ((AnnotationValue) first).getAnnotationName();
                            if (io.swagger.v3.oas.annotations.security.SecurityRequirement.class.getName().equals(annotationName)) {
                                List securityRequirements = new ArrayList<>(a.length);
                                for (Object o : a) {
                                    securityRequirements.add(mapToSecurityRequirement((AnnotationValue) o));
                                }
                                newValues.put(key, securityRequirements);
                            } else if (Extension.class.getName().equals(annotationName)) {
                                Map extensions = new HashMap<>();
                                for (Object o : a) {
                                    processExtensions(extensions, (AnnotationValue) o);
                                }
                                newValues.put("extensions", extensions);
                            } else if (Content.class.getName().equals(annotationName)) {
                                Map mediaTypes = annotationValueArrayToSubmap(a, "mediaType", context);
                                newValues.put(key, mediaTypes);
                            } else if (Link.class.getName().equals(annotationName) || Header.class.getName().equals(annotationName)) {
                                Map links = annotationValueArrayToSubmap(a, "name", context);
                                newValues.put(key, links);
                            } else if (LinkParameter.class.getName().equals(annotationName)) {
                                Map params = toTupleSubMap(a, "name",  "expression");
                                newValues.put(key, params);
                            } else if (OAuthScope.class.getName().equals(annotationName)) {
                                Map params = toTupleSubMap(a, "name",  "description");
                                newValues.put(key, params);
                            } else if (ApiResponse.class.getName().equals(annotationName)) {
                                Map responses = new LinkedHashMap();
                                for (Object o : a) {
                                    AnnotationValue sv = (AnnotationValue) o;
                                    String name = sv.get("responseCode", String.class).orElse("default");
                                    Map map = toValueMap(sv.getValues(), context);
                                    responses.put(name, map);
                                }
                                newValues.put(key, responses);
                            } else if (ExampleObject.class.getName().equals(annotationName)) {
                                Map examples = new LinkedHashMap();
                                for (Object o : a) {
                                    AnnotationValue sv = (AnnotationValue) o;
                                    String name = sv.get("name", String.class).orElse("example");
                                    Map map = toValueMap(sv.getValues(), context);
                                    examples.put(name, map);
                                }
                                newValues.put(key, examples);
                            } else if (ServerVariable.class.getName().equals(annotationName)) {
                                Map variables = new LinkedHashMap();
                                for (Object o : a) {
                                    AnnotationValue sv = (AnnotationValue) o;
                                    Optional n = sv.get("name", String.class);
                                    n.ifPresent(name -> {
                                        Map map = toValueMap(sv.getValues(), context);
                                        Object dv = map.get("defaultValue");
                                        if (dv != null) {
                                            map.put("default", dv);
                                        }
                                        variables.put(name, map);
                                    });
                                }
                                newValues.put(key, variables);
                            } else if (DiscriminatorMapping.class.getName().equals(annotationName)) {
                                final Map mappings = new HashMap<>();
                                for (Object o : a) {
                                    final AnnotationValue dv = (AnnotationValue) o;
                                    final Map valueMap = resolveAnnotationValues(context, dv);
                                    mappings.put(valueMap.get("value").toString(), valueMap.get("$ref").toString());
                                }
                                final Map discriminatorMap = getDiscriminatorMap(newValues);
                                discriminatorMap.put("mapping", mappings);
                                newValues.put("discriminator", discriminatorMap);
                            } else {
                                if (a.length == 1) {
                                    final AnnotationValue av = (AnnotationValue) a[0];
                                    final Map valueMap = resolveAnnotationValues(context, av);
                                    newValues.put(key, toValueMap(valueMap, context));
                                } else {

                                    List list = new ArrayList();
                                    for (Object o : a) {
                                        if (o instanceof AnnotationValue) {
                                            final AnnotationValue av = (AnnotationValue) o;
                                            final Map valueMap = resolveAnnotationValues(context, av);
                                            list.add(valueMap);
                                        } else {
                                            list.add(o);
                                        }
                                    }
                                    newValues.put(key, list);
                                }
                            }
                        } else {
                            newValues.put(key, value);
                        }
                    } else {
                        newValues.put(key, a);
                    }
                } else if (key.equals("discriminatorProperty")) {
                    final Map discriminatorMap = getDiscriminatorMap(newValues);
                    discriminatorMap.put("propertyName", parseJsonString(value).orElse(value));
                    newValues.put("discriminator", discriminatorMap);
                } else {
                    newValues.put(key, parseJsonString(value).orElse(value));
                }
            }
        }
        return newValues;
    }

    private Map getDiscriminatorMap(Map newValues) {
        return newValues.containsKey("discriminator") ? (Map) newValues.get("discriminator") : new HashMap<>();
    }

    // Copy of io.swagger.v3.core.util.AnnotationsUtils.getExtensions
    private void processExtensions(Map map, AnnotationValue extension) {
        String name = extension.getRequiredValue("name", String.class);
        final String key = name.length() > 0 ? org.apache.commons.lang3.StringUtils.prependIfMissing(name, "x-") : name;
        for (AnnotationValue prop : extension.getAnnotations("properties", ExtensionProperty.class)) {
            final String propertyName = prop.getRequiredValue("name", String.class);
            final String propertyValue = prop.getRequiredValue(String.class);
            JsonNode processedValue = null;
            final boolean propertyAsJson = prop.get("parseValue", boolean.class, false);
            if (org.apache.commons.lang3.StringUtils.isNotBlank(propertyName) && org.apache.commons.lang3.StringUtils.isNotBlank(propertyValue)) {
                if (key.isEmpty()) {
                    if (propertyAsJson) {
                        try {
                            processedValue = Json.mapper().readTree(propertyValue);
                            map.put(org.apache.commons.lang3.StringUtils.prependIfMissing(propertyName, "x-"), processedValue);
                        } catch (Exception e) {
                            map.put(org.apache.commons.lang3.StringUtils.prependIfMissing(propertyName, "x-"), propertyValue);
                        }
                    } else {
                        map.put(org.apache.commons.lang3.StringUtils.prependIfMissing(propertyName, "x-"), propertyValue);
                    }
                } else {
                    Object value = map.get(key);
                    if (!(value instanceof Map)) {
                        value = new LinkedHashMap<>();
                        map.put(key, value);
                    }
                    @SuppressWarnings("unchecked") final Map mapValue = (Map) value;
                    if (propertyAsJson) {
                        try {
                            processedValue = Json.mapper().readTree(propertyValue);
                            mapValue.put(propertyName, processedValue);
                        } catch (Exception e) {
                            mapValue.put(propertyName, propertyValue);
                        }
                    } else {
                        mapValue.put(propertyName, propertyValue);
                    }
                }
            }
        }
    }

    private Optional parseJsonString(Object object) {
        if (object instanceof String) {
            String string = (String) object;
            try {
                return Optional.of(jsonMapper.readValue(string, Map.class));
            } catch (IOException e) {
                return Optional.empty();
            }
        }
        return Optional.empty();
    }

    private  void processAnnotationValue(VisitorContext context, AnnotationValue annotationValue, Map arraySchemaMap, List filters, Class type) {
        Map values = annotationValue.getValues().entrySet().stream()
            .filter(entry -> filters == null || ! filters.contains(entry.getKey()))
            .collect(toMap(
                 e -> e.getKey().equals("requiredProperties") ? "required" : e.getKey(), Map.Entry::getValue));
        Optional schema = toValue(values, context, type);
        schema.ifPresent(s -> schemaToValueMap(arraySchemaMap, s));
    }

    private Map resolveArraySchemaAnnotationValues(VisitorContext context, AnnotationValue av) {
        final Map arraySchemaMap = new HashMap<>(10);
        // properties
        av.get("arraySchema", AnnotationValue.class).ifPresent(annotationValue ->
            processAnnotationValue(context, (AnnotationValue) annotationValue, arraySchemaMap, Arrays.asList("ref", "implementation"), Schema.class)
        );
        // items
        av.get("schema", AnnotationValue.class).ifPresent(annotationValue -> {
            Optional impl = annotationValue.get("implementation", String.class);
            Optional type = annotationValue.get("type", String.class);
            Optional format = annotationValue.get("format", String.class);
            Optional classElement = Optional.empty();
            PrimitiveType primitiveType = null;
            if (impl.isPresent()) {
                classElement = context.getClassElement(impl.get());
            } else if (type.isPresent()) {
                // if format is "binary", we want PrimitiveType.BINARY
                primitiveType = PrimitiveType.fromName(format.isPresent() && format.get().equals("binary") ? format.get() : type.get());
                if (primitiveType == null) {
                    classElement = context.getClassElement(type.get());
                } else {
                    classElement = context.getClassElement(primitiveType.getKeyClass());
                }
            }
            if (classElement.isPresent()) {
                if (primitiveType == null) {
                    final ArraySchema schema = arraySchema(resolveSchema(null, classElement.get(), context, Collections.emptyList()));
                    schemaToValueMap(arraySchemaMap, schema);
                } else {
                    // For primitive type, just copy description field is present.
                    final Schema items = primitiveType.createProperty();
                    items.setDescription((String) annotationValue.get("description", String.class).orElse(null));
                    final ArraySchema schema = arraySchema(items);
                    schemaToValueMap(arraySchemaMap, schema);
                }
            } else {
                arraySchemaMap.putAll(resolveAnnotationValues(context, annotationValue));
            }
        });
        // other properties (minItems,...)
        processAnnotationValue(context, av, arraySchemaMap, Arrays.asList("schema", "arraySchema"), ArraySchema.class);
        return arraySchemaMap;
    }

    private Map resolveAnnotationValues(VisitorContext context, AnnotationValue av) {
        final Map valueMap = toValueMap(av.getValues(), context);
        bindSchemaIfNeccessary(context, av, valueMap);
        final String annotationName = av.getAnnotationName();
        if (Parameter.class.getName().equals(annotationName)) {
            normalizeEnumValues(valueMap, CollectionUtils.mapOf(
                    "in", ParameterIn.class,
                    "style", ParameterStyle.class
            ));
        }
        return valueMap;
    }

    private Map toTupleSubMap(Object[] a, String entryKey, String entryValue) {
        Map params = new LinkedHashMap();
        for (Object o : a) {
            AnnotationValue sv = (AnnotationValue) o;
            final Optional n = sv.get(entryKey, String.class);
            final Optional expr = sv.get(entryValue, String.class);
            if (n.isPresent() && expr.isPresent()) {
                params.put(n.get(), expr.get());
            }
        }
        return params;
    }

    /**
     * Resolves the schema for the given type element.
     *
     * @param definingElement The defining element
     * @param type The type element
     * @param context The context
     * @param mediaTypes An optional media type
     * @return The schema or null if it cannot be resolved
     */
    protected @Nullable Schema resolveSchema(@Nullable Element definingElement, ClassElement type, VisitorContext context, List mediaTypes) {
        return resolveSchema(resolveOpenAPI(context), definingElement, type, context, mediaTypes);
    }

    /**
     * Resolves the schema for the given type element.
     *
     * @param openAPI The OpenAPI object
     * @param definingElement The defining element
     * @param type The type element
     * @param context The context
     * @param mediaTypes An optional media type
     * @return The schema or null if it cannot be resolved
     */
    protected @Nullable Schema resolveSchema(OpenAPI openAPI, @Nullable Element definingElement, ClassElement type, VisitorContext context, List mediaTypes) {
        Schema schema = null;

        if (type instanceof EnumElement) {
            schema = getSchemaDefinition(openAPI, context, type, definingElement, mediaTypes);
        } else {

            boolean isPublisher = false;
            boolean isObservable = false;

            // StreamingFileUpload implements Publisher, but it should be not considered as a Publisher in the spec file
            if (!type.isAssignable("io.micronaut.http.multipart.StreamingFileUpload") && isContainerType(type)) {
                isPublisher = type.isAssignable(Publisher.class.getName()) && !type.isAssignable("reactor.core.publisher.Mono");
                isObservable = type.isAssignable("io.reactivex.Observable") && !type.isAssignable("reactor.core.publisher.Mono");
                type = type.getFirstTypeArgument().orElse(null);
            }
            if (type != null) {

                String typeName = type.getName();
                // File upload case
                if ("io.micronaut.http.multipart.StreamingFileUpload".equals(typeName) ||
                    "io.micronaut.http.multipart.CompletedFileUpload".equals(typeName) ||
                    "io.micronaut.http.multipart.CompletedPart".equals(typeName) ||
                    "io.micronaut.http.multipart.PartData".equals(typeName)) {
                    isPublisher = isPublisher && ! "io.micronaut.http.multipart.PartData".equals(typeName);
                    // For file upload, we use PrimitiveType.BINARY
                    typeName = PrimitiveType.BINARY.name();
                }
                PrimitiveType primitiveType = PrimitiveType.fromName(typeName);
                if (!type.isArray() && ClassUtils.isJavaLangType(typeName)) {
                    schema = getPrimitiveType(typeName);
                } else if (!type.isArray() && primitiveType != null) {
                    schema = primitiveType.createProperty();
                } else if (type.isAssignable(Map.class.getName())) {
                    schema = new MapSchema();
                    if (type.getTypeArguments().isEmpty()) {
                        schema.setAdditionalProperties(true);
                    } else {
                        Element valueType = type.getTypeArguments().get("V");
                        if (valueType.getName().equals(Object.class.getName())) {
                            schema.setAdditionalProperties(true);
                        } else {
                            Schema additionalPropertiesSchema = getPrimitiveType(valueType.getName());
                            if (additionalPropertiesSchema == null) {
                                additionalPropertiesSchema = getSchemaDefinition(openAPI, context, valueType, definingElement, mediaTypes);
                            }
                            schema.setAdditionalProperties(additionalPropertiesSchema);
                        }
                    }
                } else if (type.isIterable()) {
                    if (primitiveType == null) {
                        Optional componentType = type.getFirstTypeArgument();
                        if (componentType.isPresent()) {
                            schema = getPrimitiveType(componentType.get().getName());
                        } else {
                            schema = getPrimitiveType(Object.class.getName());
                        }

                        if (schema == null && componentType.isPresent()) {
                            ClassElement componentElement = componentType.get();
                            // we must have a POJO so let's create a component
                            schema = getSchemaDefinition(openAPI, context, componentElement, definingElement, mediaTypes);
                        }
                    } else {
                        schema = getPrimitiveType(typeName);
                    }
                    if (schema != null) {
                        schema = arraySchema(schema);
                    }
                } else {
                    schema = getSchemaDefinition(openAPI, context, type, definingElement, mediaTypes);
                }

            }

            if (schema != null) {
                boolean isStream = false;
                for (MediaType mediaType: mediaTypes) {
                    if (MediaType.TEXT_EVENT_STREAM_TYPE.equals(mediaType) || MediaType.APPLICATION_JSON_STREAM_TYPE.equals(mediaType)) {
                        isStream = true;
                        break;
                    }
                }
                if (!isStream && (isPublisher || isObservable)) {
                    schema = arraySchema(schema);
                }
            }
        }
        return schema;
    }

    /**
     * Resolve the components.
     * @param openAPI The open API
     * @return The components
     */
    protected Components resolveComponents(OpenAPI openAPI) {
        Components components = openAPI.getComponents();
        if (components == null) {
            components = new Components();
            openAPI.setComponents(components);
        }
        return components;
    }

    private List> handleUnwrapped(Schema innerModel, String prefix, String suffix,
            VisitorContext context) {
        Map properties = innerModel.getProperties();
        if (properties == null) {
            return Collections.emptyList();
        }
        List> props = new ArrayList<>(properties.size());
        if (StringUtils.isEmpty(suffix) && StringUtils.isEmpty(prefix)) {
            props.addAll(properties.entrySet());
        } else {
            if (prefix == null) {
                prefix = "";
            }
            if (suffix == null) {
                suffix = "";
            }
            for (Entry prop : (Set>) properties.entrySet()) {
                try {
                    Schema clonedProp = jsonMapper.readValue(Json.pretty(prop.getValue()), Schema.class);
                    clonedProp.setName(prefix + prop.getKey() + suffix);
                    props.add(new AbstractMap.SimpleEntry<>(clonedProp.getName(), clonedProp));
                } catch (IOException e) {
                    context.warn("Exception cloning property " + e.getMessage(), null);
                }
            }
        }
        return props;
    }

    /**
     * Processes a schema property.
     * @param context The visitor context
     * @param element The element
     * @param elementType The element type
     * @param parentSchema The parent schema
     * @param propertySchema The property schema
     */
    protected void processSchemaProperty(VisitorContext context, Element element, ClassElement elementType, Schema parentSchema, Schema propertySchema) {
        if (propertySchema != null) {
            List> props;
            AnnotationValue uw = element.getAnnotation(JsonUnwrapped.class);
            if (uw != null && uw.booleanValue("enabled").orElse(Boolean.TRUE)) {
                Map schemas = resolveSchemas(resolveOpenAPI(context));
                String schemaName = element.stringValue(io.swagger.v3.oas.annotations.media.Schema.class, "name").orElse(computeDefaultSchemaName(null, elementType));
                propertySchema = schemas.get(schemaName);
                props = handleUnwrapped(propertySchema, uw.stringValue("prefix").orElse(null), uw.stringValue("suffix").orElse(null), context);
            } else {
                propertySchema = bindSchemaForElement(context, element, elementType, propertySchema);
                String propertyName = Optional.ofNullable(propertySchema.getName()).orElse(element.getName());
                props = Collections.singletonList(new AbstractMap.SimpleEntry<>(propertyName, propertySchema));
            }
            final boolean required = element.isAnnotationPresent(NotNull.class)
                    || element.isAnnotationPresent(NotBlank.class)
                    || element.isAnnotationPresent(NotEmpty.class);
            for (Entry prop: props) {
                parentSchema.addProperties(prop.getKey(), prop.getValue());
                if (required) {
                    List requiredList = parentSchema.getRequired();
                    // Check for duplicates
                    if (requiredList == null || !requiredList.contains(prop.getKey())) {
                        parentSchema.addRequiredItem(prop.getKey());
                    }
                }
            }
        }
    }

    /**
     * Binds the schema for the given element.
     *
     * @param context The context
     * @param element The element
     * @param elementType The element type
     * @param schemaToBind The schema to bind
     * @return The bound schema
     */
    protected Schema bindSchemaForElement(VisitorContext context, Element element, ClassElement elementType, Schema schemaToBind) {
        AnnotationValue schemaAnn = element.getAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
        if (schemaAnn != null) {
            schemaToBind = bindSchemaAnnotationValue(context, element, schemaToBind, schemaAnn);
            Optional schemaName = schemaAnn.get("name", String.class);
            if (schemaName.isPresent()) {
                schemaToBind.setName(schemaName.get());
            }
        }
        AnnotationValue arraySchemaAnn = element.getAnnotation(io.swagger.v3.oas.annotations.media.ArraySchema.class);
        if (arraySchemaAnn != null) {
            schemaToBind = bindArraySchemaAnnotationValue(context, element, schemaToBind, arraySchemaAnn);
            Optional schemaName = arraySchemaAnn.get("name", String.class);
            if (schemaName.isPresent()) {
                schemaToBind.setName(schemaName.get());
            }
        }

        Schema finalSchemaToBind = schemaToBind;
        final boolean isIterableOrMap = elementType.isIterable() || elementType.isAssignable(Map.class);

        if (isIterableOrMap) {
            if (element.isAnnotationPresent(NotEmpty.class)) {
                finalSchemaToBind.setMinItems(1);
            }
            element.getValue(Size.class, "min", Integer.class).ifPresent(finalSchemaToBind::setMinItems);
            element.getValue(Size.class, "max", Integer.class).ifPresent(finalSchemaToBind::setMaxItems);
        } else {
            if ("string".equals(finalSchemaToBind.getType())) {
                if (element.isAnnotationPresent(NotEmpty.class) || element.isAnnotationPresent(NotBlank.class)) {
                    finalSchemaToBind.setMinLength(1);
                }
                element.getValue(Size.class, "min", Integer.class).ifPresent(finalSchemaToBind::setMinLength);
                element.getValue(Size.class, "max", Integer.class).ifPresent(finalSchemaToBind::setMaxLength);
            }

            if (element.isAnnotationPresent(Negative.class)) {
                finalSchemaToBind.setMaximum(BigDecimal.ZERO);
            }
            if (element.isAnnotationPresent(NegativeOrZero.class)) {
                finalSchemaToBind.setMaximum(BigDecimal.ZERO);
            }
            if (element.isAnnotationPresent(Positive.class)) {
                finalSchemaToBind.setMinimum(BigDecimal.ZERO);
            }
            if (element.isAnnotationPresent(PositiveOrZero.class)) {
                finalSchemaToBind.setMinimum(BigDecimal.ZERO);
            }
            element.getValue(Max.class, BigDecimal.class).ifPresent(finalSchemaToBind::setMaximum);
            element.getValue(Min.class, BigDecimal.class).ifPresent(finalSchemaToBind::setMinimum);
            element.getValue(DecimalMax.class, BigDecimal.class).ifPresent(finalSchemaToBind::setMaximum);
            element.getValue(DecimalMin.class, BigDecimal.class).ifPresent(finalSchemaToBind::setMinimum);
            if (element.isAnnotationPresent(Email.class)) {
                finalSchemaToBind.setFormat("email");
            }

            element.findAnnotation(Pattern.class).flatMap(p -> p.get("regexp", String.class)).ifPresent(finalSchemaToBind::setPattern);
        }

        setSchemaDocumentation(element, schemaToBind);
        if (element.isAnnotationPresent(Deprecated.class)) {
            schemaToBind.setDeprecated(true);
        }

        final String defaultValue = element.getValue(Bindable.class, "defaultValue", String.class).orElse(null);
        if (defaultValue != null && schemaToBind.getDefault() == null) {
            schemaToBind.setDefault(defaultValue);
        }
        if (element.isAnnotationPresent(Nullable.class)) {
            schemaToBind.setNullable(true);
        }
        return schemaToBind;
    }

    private void setSchemaDocumentation(Element element, Schema schemaToBind) {
        if (StringUtils.isEmpty(schemaToBind.getDescription())) {
            Optional documentation = element.getDocumentation();
            String doc = documentation.orElse(null);
            if (doc != null) {
                JavadocDescription desc = new JavadocParser().parse(doc);
                schemaToBind.setDescription(desc.getMethodDescription());
            }
        }
    }

    /**
     * Binds the schema for the given element.
     *
     * @param context The context
     * @param element The element
     * @param schemaToBind The schema to bind
     * @param schemaAnn The schema annotation
     * @return The bound schema
     */
    protected Schema bindSchemaAnnotationValue(VisitorContext context, Element element, Schema schemaToBind, AnnotationValue schemaAnn) {
        JsonNode schemaJson = toJson(schemaAnn.getValues(), context);
        return doBindSchemaAnnotationValue(context, element, schemaToBind, schemaJson, schemaAnn.get("defaultValue", String.class).orElse(null),
                schemaAnn.get("allowableValues", String[].class).orElse(null));
    }

    private Schema doBindSchemaAnnotationValue(VisitorContext context, Element element, Schema schemaToBind,
            JsonNode schemaJson, String defaultValue, String... allowableValues) {
        try {
            schemaToBind = jsonMapper.readerForUpdating(schemaToBind).readValue(schemaJson);
            if (StringUtils.isNotEmpty(defaultValue)) {
                schemaToBind.setDefault(defaultValue);
            }
            if (ArrayUtils.isNotEmpty(allowableValues)) {
                for (String allowableValue : allowableValues) {
                    if (schemaToBind.getEnum() == null || !schemaToBind.getEnum().contains(allowableValue)) {
                        schemaToBind.addEnumItemObject(allowableValue);
                    }
                }
            }
        } catch (IOException e) {
            context.warn("Error reading Swagger Schema for element [" + element + "]: " + e.getMessage(), element);
        }
        return schemaToBind;
    }

    /**
     * Binds the array schema for the given element.
     *
     * @param context The context
     * @param element The element
     * @param schemaToBind The schema to bind
     * @param schemaAnn The schema annotation
     * @return The bound schema
     */
    protected Schema bindArraySchemaAnnotationValue(VisitorContext context, Element element, Schema schemaToBind, AnnotationValue schemaAnn) {
        JsonNode schemaJson = toJson(schemaAnn.getValues(), context);
        if (schemaJson.isObject()) {
            ObjectNode objNode = (ObjectNode) schemaJson;
            JsonNode arraySchema = objNode.remove("arraySchema");
            // flatten
            if (arraySchema != null && arraySchema.isObject()) {
                ((ObjectNode) arraySchema).remove("implementation");
                objNode.setAll((ObjectNode) arraySchema);
            }
            // remove schema that maps to 'items'
            JsonNode items = objNode.remove("schema");
            if (items != null && schemaToBind instanceof ArraySchema && ((ArraySchema) schemaToBind).getItems() != null) {
                ArraySchema arrSchemaToBind = (ArraySchema) schemaToBind;
                // if it has no $ref add properties, otherwise we are good
                if (arrSchemaToBind.getItems().get$ref() == null) {
                    try {
                        arrSchemaToBind.items(jsonMapper.readerForUpdating(arrSchemaToBind.getItems()).readValue(items));
                    } catch (IOException e) {
                        context.warn("Error reading Swagger Schema for element [" + element + "]: " + e.getMessage(), element);
                    }
                }
            }
        }
        return doBindSchemaAnnotationValue(context, element, schemaToBind, schemaJson, null);
    }

    private Optional> resolveExtensions(JsonNode jn) {
        try {
            JsonNode extensionsNode = jn.get("extensions");
            if (extensionsNode != null) {
                return Optional.ofNullable(jsonMapper.treeToValue(extensionsNode, Map.class));
            }
        } catch (JsonProcessingException e) {
            // Ignore
        }
        return Optional.empty();
    }

    private Map annotationValueArrayToSubmap(Object[] a, String classifier, VisitorContext context) {
        Map mediaTypes = new LinkedHashMap();
        for (Object o : a) {
            AnnotationValue sv = (AnnotationValue) o;
            String name = sv.get(classifier, String.class).orElse(null);
            if (name == null && classifier.equals("mediaType")) {
                name = MediaType.APPLICATION_JSON;
            }
            if (name != null) {
                Map map = toValueMap(sv.getValues(), context);
                mediaTypes.put(name, map);
            }
        }
        return mediaTypes;
    }

    private void schemaToValueMap(Map valueMap, Schema schema) {
        if (schema != null) {
            final BeanMap beanMap = BeanMap.of(schema);
            for (Map.Entry e : beanMap.entrySet()) {
                final Object v = e.getValue();
                if (v != null) {
                    valueMap.put(e.getKey(), v);
                }
            }
            if (schema.get$ref() != null) {
                valueMap.put("$ref", schema.get$ref());
            }
        }
    }

    private void bindSchemaIfNeccessary(VisitorContext context, AnnotationValue av, Map valueMap) {
        final Optional impl = av.get("implementation", String.class);
        final Optional schema = av.get("schema", String.class);
        final Optional anyOf = av.get("anyOf", Argument.of(String[].class));
        final Optional oneOf = av.get("oneOf", Argument.of(String[].class));
        final Optional allOf = av.get("allOf", Argument.of(String[].class));
        // remap keys.
        Object o = valueMap.remove("defaultValue");
        if (o != null) {
            valueMap.put("default", o);
        }
        o = valueMap.remove("allowableValues");
        if (o != null) {
            valueMap.put("enum", o);
        }
        boolean isSchema = io.swagger.v3.oas.annotations.media.Schema.class.getName().equals(av.getAnnotationName());
        if (isSchema && impl.isPresent()) {
            final String className = impl.get();
            bindSchemaForClassName(context, valueMap, className);
        }
        if (DiscriminatorMapping.class.getName().equals(av.getAnnotationName()) && schema.isPresent()) {
            final String className = schema.get();
            bindSchemaForClassName(context, valueMap, className);
        }
        if (isSchema && (anyOf.isPresent() || oneOf.isPresent() || allOf.isPresent())) {
            anyOf.ifPresent(anyOfList -> bindSchemaForComposite(context, valueMap, anyOfList, "anyOf"));
            oneOf.ifPresent(oneOfList -> bindSchemaForComposite(context, valueMap, oneOfList, "oneOf"));
            allOf.ifPresent(allOfList -> bindSchemaForComposite(context, valueMap, allOfList, "allOf"));
        }
    }

    private void bindSchemaForComposite(VisitorContext context, Map valueMap, String[] classNames, String key) {
        final List> namesToSchemas = Arrays.stream(classNames).map(className -> {
            final Optional classElement = context.getClassElement(className);
            Map schemaMap = new HashMap<>();
            if (classElement.isPresent()) {
                final Schema schema = resolveSchema(null, classElement.get(), context, Collections.emptyList());
                schemaToValueMap(schemaMap, schema);
            }
            return schemaMap;
        }).collect(Collectors.toList());
        valueMap.put(key, namesToSchemas);
    }

    private void bindSchemaForClassName(VisitorContext context, Map valueMap, String className) {
        final Optional classElement = context.getClassElement(className);
        if (classElement.isPresent()) {
            final Schema schema = resolveSchema(null, classElement.get(), context, Collections.emptyList());
            schemaToValueMap(valueMap, schema);
        }
    }

    private void checkAllOf(ComposedSchema composedSchema) {
        if (composedSchema != null && composedSchema.getAllOf() != null && !composedSchema.getAllOf().isEmpty() && composedSchema.getProperties() != null
                && !composedSchema.getProperties().isEmpty()) {
            // put all properties as siblings of allOf
            ObjectSchema propSchema = new ObjectSchema();
            propSchema.properties(composedSchema.getProperties());
            propSchema.setDescription(composedSchema.getDescription());
            propSchema.setRequired(composedSchema.getRequired());
            composedSchema.setProperties(null);
            composedSchema.setDescription(null);
            composedSchema.setRequired(null);
            composedSchema.setType(null);
            composedSchema.addAllOfItem(propSchema);
        }
    }

    private Schema getSchemaDefinition(
            OpenAPI openAPI,
            VisitorContext context,
            Element type,
            @Nullable Element definingElement,
            List mediaTypes) {
        // To break the recursion
        if (inProgressSchemas.contains(type.getSimpleName())) {
            return null;
        }
        AnnotationValue schemaValue = definingElement == null ? null : definingElement.getDeclaredAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
        if (schemaValue == null) {
            schemaValue = type.getDeclaredAnnotation(io.swagger.v3.oas.annotations.media.Schema.class);
        }
        Schema schema;
        Map schemas = resolveSchemas(openAPI);
        if (schemaValue == null) {
            final boolean isBasicType = ClassUtils.isJavaBasicType(type.getName());
            final PrimitiveType primitiveType;
            if (isBasicType) {
                primitiveType = ClassUtils.forName(type.getName(), getClass().getClassLoader()).map(PrimitiveType::fromType).orElse(null);
            } else {
                primitiveType = null;
            }
            if (primitiveType == null) {
                String schemaName = computeDefaultSchemaName(definingElement, type);
                schema = schemas.get(schemaName);
                if (schema == null) {

                    if (type instanceof EnumElement) {
                        schema = new Schema();
                        schema.setName(schemaName);
                        schemas.put(schemaName, schema);

                        schema.setType("string");
                        schema.setEnum(((EnumElement) type).values());
                    } else {
                        if (type instanceof TypedElement) {
                            ClassElement classElement = ((TypedElement) type).getType();
                            Optional superType = classElement == null ? Optional.empty() : classElement.getSuperType();
                            if (superType.isPresent()) {
                                schema = new ComposedSchema();
                                while (superType.isPresent()) {
                                    final ClassElement superElement = superType.get();
                                    String parentSchemaName = computeDefaultSchemaName(definingElement, superElement);
                                    if (schemas.get(parentSchemaName) != null
                                            || getSchemaDefinition(openAPI, context, superElement, null, mediaTypes) != null) {
                                        Schema parentSchema = new Schema();
                                        parentSchema.set$ref(schemaRef(parentSchemaName));
                                        ((ComposedSchema) schema).addAllOfItem(parentSchema);
                                    }
                                    superType = superElement.getSuperType();
                                }
                            } else {
                                schema = new Schema();
                            }
                        } else {
                            schema = new Schema();
                        }
                        schema.setType("object");
                        schema.setName(schemaName);
                        schemas.put(schemaName, schema);

                        populateSchemaProperties(openAPI, context, type, schema, mediaTypes);
                        if (schema instanceof ComposedSchema) {
                            checkAllOf((ComposedSchema) schema);
                        }
                    }
                }
            } else {
                return primitiveType.createProperty();
            }
        } else {
            String schemaName = schemaValue.get("name", String.class).orElse(computeDefaultSchemaName(definingElement, type));
            schema = schemas.get(schemaName);
            if (schema == null) {
                inProgressSchemas.add(schemaName);
                try {
                    schema = readSchema(schemaValue, openAPI, context, type, mediaTypes);
                    if (schema != null) {
                        schema.setName(schemaName);
                        schemas.put(schemaName, schema);
                    }
                } catch (JsonProcessingException e) {
                    context.warn("Error reading Swagger Parameter for element [" + type + "]: " + e.getMessage(), type);
                } finally {
                    inProgressSchemas.remove(schemaName);
                }
            }
        }
        if (schema != null) {
            setSchemaDocumentation(type, schema);
            Schema schemaRef = new Schema();
            schemaRef.set$ref(schemaRef(schema.getName()));
            schemaRef.setDescription(schema.getDescription());
            return schemaRef;
        }
        return null;
    }

    /**
     * Reads schema.
     *
     * @param schemaValue annotation value
     * @param openAPI     The OpenApi
     * @param context     The VisitorContext
     * @param type        The element
     * @param mediaTypes   The media types of schema
     * @return New schema instance
     * @throws JsonProcessingException when Json parsing fails
     */
    protected Schema readSchema(AnnotationValue schemaValue, OpenAPI openAPI, VisitorContext context, @Nullable Element type, List mediaTypes) throws JsonProcessingException {
        Map values = schemaValue.getValues()
                .entrySet()
                .stream()
                .collect(toMap(e -> e.getKey().equals("requiredProperties") ? "required" : e.getKey(), Map.Entry::getValue));
        Optional schemaOpt = toValue(values, context, Schema.class);
        if (!schemaOpt.isPresent()) {
            return null;
        }
        Schema schema = schemaOpt.get();
        ComposedSchema composedSchema = null;
        if (schema instanceof ComposedSchema) {
            composedSchema = (ComposedSchema) schema;
            final Optional allOf = schemaValue.get("allOf", String[].class);
            if (allOf.isPresent() && allOf.get().length > 0) {
                final String[] names = allOf.get();
                List schemaList = namesToSchemas(openAPI, context, names, mediaTypes);
                for (Schema s: schemaList) {
                    composedSchema.addAllOfItem(s);
                }
            }

            final Optional anyOf = schemaValue.get("anyOf", String[].class);
            if (anyOf.isPresent() && anyOf.get().length > 0) {
                final String[] names = anyOf.get();
                List schemaList = namesToSchemas(openAPI, context, names, mediaTypes);
                for (Schema s: schemaList) {
                    composedSchema.addAnyOfItem(s);
                }
            }

            final Optional oneof = schemaValue.get("oneOf", String[].class);
            if (oneof.isPresent() && oneof.get().length > 0) {
                final String[] names = oneof.get();
                List schemaList = namesToSchemas(openAPI, context, names, mediaTypes);
                for (Schema s: schemaList) {
                    composedSchema.addOneOfItem(s);
                }
            }

            schema.setType("object");
        }
        if (type instanceof EnumElement) {
            schema.setType("string");
            schema.setEnum(((EnumElement) type).values());
        } else if (schema instanceof ObjectSchema || composedSchema != null) {
            populateSchemaProperties(openAPI, context, type, schema, mediaTypes);
            checkAllOf(composedSchema);
        }
        return schema;
    }

    private List namesToSchemas(OpenAPI openAPI, VisitorContext context, String[] names, List mediaTypes) {
        return Arrays.stream(names).flatMap((Function>) className -> {
                                        final Optional classElement = context.getClassElement(className);
                                        if (classElement.isPresent()) {
                                            final Schema schemaDefinition = getSchemaDefinition(openAPI, context, classElement.get(), null, mediaTypes);
                                            if (schemaDefinition != null) {
                                                return Stream.of(schemaDefinition);
                                            }
                                        }

                                        return Stream.empty();
                                    }).collect(Collectors.toList());
    }

    private String schemaRef(String schemaName) {
        return "#/components/schemas/" + schemaName;
    }

    private String computeDefaultSchemaName(Element definingElement, Element type) {
        final String metaAnnName = definingElement == null ? null : definingElement.getAnnotationNameByStereotype(io.swagger.v3.oas.annotations.media.Schema.class).orElse(null);
        if (metaAnnName != null && !io.swagger.v3.oas.annotations.media.Schema.class.getName().equals(metaAnnName)) {
            return NameUtils.getSimpleName(metaAnnName);
        }
        if (type instanceof TypedElement) {
            return computeNameWithGenerics(((TypedElement) type).getType());
        }
        return type.getSimpleName();
    }

    private String computeNameWithGenerics(ClassElement classElement) {
        StringBuilder builder = new StringBuilder(classElement.getSimpleName());
        computeNameWithGenerics(classElement, builder, new HashSet<>());
        return builder.toString();
    }

    private void computeNameWithGenerics(ClassElement classElement, StringBuilder builder, Set computed) {
        computed.add(classElement.getName());
        final Map typeArguments = classElement.getTypeArguments();
        final Iterator i = typeArguments.values().iterator();
        if (i.hasNext()) {

            builder.append('_');
            while (i.hasNext()) {
                final ClassElement ce = i.next();
                builder.append(ce.getSimpleName());
                if (!computed.contains(ce.getName())) {
                    computeNameWithGenerics(ce, builder, computed);
                }
                if (i.hasNext()) {
                    builder.append('.');
                }
            }

            builder.append('_');
        }
    }

    /**
     * Returns true if classElement is a JavaClassElement.
     * @param classElement A ClassElement.
     * @param context The context.
     * @return true if classElement is a JavaClassElement.
     */
    static boolean isJavaElement(ClassElement classElement, VisitorContext context) {
        return classElement != null && (("io.micronaut.annotation.processing.visitor.JavaClassElement".equals(classElement.getClass().getName())
                || "io.micronaut.annotation.processing.visitor.JavaClassElementExt".equals(classElement.getClass().getName())) && "io.micronaut.annotation.processing.visitor.JavaVisitorContext".equals(context.getClass().getName()));
    }

    private void populateSchemaProperties(OpenAPI openAPI, VisitorContext context, Element type, Schema schema, List mediaTypes) {
        ClassElement classElement = null;
        if (type instanceof ClassElement) {
            classElement = (ClassElement) type;
        } else if (type instanceof TypedElement) {
            classElement = ((TypedElement) type).getType();
        }

        if (classElement != null) {
            List beanProperties;
            final boolean isJavaElement = isJavaElement(classElement, context);
            JavaClassElementExt jce = null;
            if (isJavaElement) {
                jce = new JavaClassElementExt(classElement, context);
                beanProperties = jce.beanProperties();
            } else {
                beanProperties = classElement.getBeanProperties().stream().filter(p -> ! "groovy.lang.MetaClass".equals(p.getType().getName())).collect(Collectors.toList());
            }
            processPropertyElements(openAPI, context, type, schema, beanProperties, mediaTypes);
            if (isJavaElement) {
                List fluentMethodsProperties = jce.fluentBeanProperties();
                processPropertyElements(openAPI, context, type, schema, fluentMethodsProperties, mediaTypes);
            }

            final List publicFields = classElement.getFields(mods -> mods.contains(ElementModifier.PUBLIC) && mods.size() == 1);

            processPropertyElements(openAPI, context, type, schema, publicFields, mediaTypes);
        }
    }

    private void processPropertyElements(OpenAPI openAPI, VisitorContext context, Element type, Schema schema, List publicFields, List mediaTypes) {
        for (TypedElement publicField : publicFields) {
            if (publicField.isAnnotationPresent(JsonIgnore.class) || publicField.isAnnotationPresent(Hidden.class)) {
                continue;
            }

            if (publicField instanceof MemberElement && ((MemberElement) publicField).getDeclaringType().equals(type)) {

                Schema propertySchema = resolveSchema(openAPI, null, publicField.getType(), context, mediaTypes);

                processSchemaProperty(
                        context,
                        publicField,
                        publicField.getType(),
                        schema,
                        propertySchema
                );
            }
        }
    }

    private Map resolveSchemas(OpenAPI openAPI) {
        Components components = resolveComponents(openAPI);
        Map schemas = components.getSchemas();
        if (schemas == null) {
            schemas = new LinkedHashMap<>();
            components.setSchemas(schemas);
        }
        return schemas;
    }

    private ArraySchema arraySchema(Schema schema) {
        if (schema == null) {
            return null;
        }
        ArraySchema arraySchema = new ArraySchema();
        arraySchema.setItems(schema);
        return arraySchema;
    }

    private Schema getPrimitiveType(String typeName) {
        Schema schema = null;
        Optional aClass = ClassUtils.getPrimitiveType(typeName);
        if (!aClass.isPresent()) {
            aClass = ClassUtils.forName(typeName, getClass().getClassLoader());
        }

        if (aClass.isPresent()) {
            Class concreteType = aClass.get();
            Class wrapperType = ReflectionUtils.getWrapperType(concreteType);

            PrimitiveType primitiveType = PrimitiveType.fromType(wrapperType);
            if (primitiveType != null) {
                schema = primitiveType.createProperty();
            }
        }
        return schema;
    }

    private boolean isContainerType(ClassElement type) {
        return CollectionUtils.setOf(
                Optional.class.getName(),
                Future.class.getName(),
                Publisher.class.getName(),
                "io.reactivex.Single",
                "io.reactivex.Observable",
                "io.reactivex.Maybe"
        ).stream().anyMatch(type::isAssignable);
    }

    /**
     * Processes {@link io.swagger.v3.oas.annotations.security.SecurityScheme}
     * annotations.
     *
     * @param element The element
     * @param context The visitor context
     */
    protected void processSecuritySchemes(ClassElement element, VisitorContext context) {
        final List> values = element
                .getAnnotationValuesByType(io.swagger.v3.oas.annotations.security.SecurityScheme.class);
        final OpenAPI openAPI = resolveOpenAPI(context);
        for (AnnotationValue securityRequirementAnnotationValue : values) {

            final Optional n = securityRequirementAnnotationValue.get("name", String.class);
            n.ifPresent(name -> {

                final Map map = toValueMap(securityRequirementAnnotationValue.getValues(), context);
                if (map.containsKey("paramName")) {
                    map.put("name", map.remove("paramName"));
                } else {
                    map.remove("name");
                }
                normalizeEnumValues(map, CollectionUtils.mapOf("type", SecurityScheme.Type.class, "in", SecurityScheme.In.class));
                Optional securityRequirement = toValue(map, context, SecurityScheme.class);
                securityRequirement.ifPresent(securityScheme -> {

                    try {
                        securityScheme.setIn(Enum.valueOf(SecurityScheme.In.class, map.get("in").toString().toUpperCase(Locale.ENGLISH)));
                    } catch (Exception e) {
                        // ignore
                    }
                    resolveComponents(openAPI).addSecuritySchemes(name, securityScheme);
                });
            });
        }
    }

    /**
     * Normalizes enum values stored in the map.
     *
     * @param paramValues The values
     * @param enumTypes The enum types.
     */
    protected void normalizeEnumValues(Map paramValues, Map> enumTypes) {
        for (Map.Entry> entry : enumTypes.entrySet()) {
            final String name = entry.getKey();
            final Class enumType = entry.getValue();
            Object in = paramValues.get(name);
            if (in != null) {
                try {
                    final Enum enumInstance = Enum.valueOf(enumType, in.toString());
                    paramValues.put(name, enumInstance.toString());
                } catch (Exception e) {
                    // ignore
                }
            }

        }
    }

    /**
     * Maps annotation value to {@link io.swagger.v3.oas.annotations.security.SecurityRequirement}.
     * Correct format is:
     *  custom_name:
     *    - custom_scope1
     *    - custom_scope2
     * @param r The value of {@link SecurityRequirement}.
     * @return converted object.
     */
    protected SecurityRequirement mapToSecurityRequirement(AnnotationValue r) {
        String name = r.getRequiredValue("name", String.class);
        List scopes = r.get("scopes", String[].class).map(Arrays::asList).orElse(Collections.emptyList());
        SecurityRequirement securityRequirement = new SecurityRequirement();
        securityRequirement.addList(name, scopes);
        return securityRequirement;
    }

    /**
     * Converts annotation to model.
     * @param  The model type.
     * @param  The annotation type.
     * @param element The element to process.
     * @param context The context.
     * @param annotationType The annotation type.
     * @param modelType The model type.
     * @param tagList The initial list of models.
     * @return A list of model objects.
     */
     List processOpenApiAnnotation(Element element, VisitorContext context, Class annotationType, Class modelType, List tagList) {
        List> annotations = element.getAnnotationValuesByType(annotationType);
        if (CollectionUtils.isNotEmpty(annotations)) {
            if (CollectionUtils.isEmpty(tagList)) {
                tagList = new ArrayList<>();
            }
            for (AnnotationValue tag : annotations) {
                Map values;
                if (tag.getAnnotationName().equals(SecurityRequirement.class.getName()) && tag.getValues().size() > 0) {
                    Object name = tag.getValues().get("name");
                    Object scopes = Optional.ofNullable(tag.getValues().get("scopes")).orElse(new ArrayList());
                    values = Collections.singletonMap((CharSequence) name, scopes);
                } else {
                    values = tag.getValues();
                }
                toValue(values, context, modelType).ifPresent(tagList::add);
            }
        }
        return tagList;
    }

}