/**
 * Copyright 2018 Red Hat, Inc, and individual contributors.
 *
 * 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
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.wildfly.swarm.microprofile.openapi.runtime;

import java.beans.PropertyDescriptor;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.ws.rs.core.Application;

import org.apache.commons.beanutils.PropertyUtils;
import org.eclipse.microprofile.openapi.annotations.enums.Explode;
import org.eclipse.microprofile.openapi.models.Components;
import org.eclipse.microprofile.openapi.models.ExternalDocumentation;
import org.eclipse.microprofile.openapi.models.OpenAPI;
import org.eclipse.microprofile.openapi.models.Operation;
import org.eclipse.microprofile.openapi.models.PathItem;
import org.eclipse.microprofile.openapi.models.PathItem.HttpMethod;
import org.eclipse.microprofile.openapi.models.Paths;
import org.eclipse.microprofile.openapi.models.callbacks.Callback;
import org.eclipse.microprofile.openapi.models.examples.Example;
import org.eclipse.microprofile.openapi.models.headers.Header;
import org.eclipse.microprofile.openapi.models.info.Contact;
import org.eclipse.microprofile.openapi.models.info.Info;
import org.eclipse.microprofile.openapi.models.info.License;
import org.eclipse.microprofile.openapi.models.links.Link;
import org.eclipse.microprofile.openapi.models.media.Content;
import org.eclipse.microprofile.openapi.models.media.Discriminator;
import org.eclipse.microprofile.openapi.models.media.Encoding;
import org.eclipse.microprofile.openapi.models.media.MediaType;
import org.eclipse.microprofile.openapi.models.media.Schema;
import org.eclipse.microprofile.openapi.models.parameters.Parameter;
import org.eclipse.microprofile.openapi.models.parameters.Parameter.In;
import org.eclipse.microprofile.openapi.models.parameters.RequestBody;
import org.eclipse.microprofile.openapi.models.responses.APIResponse;
import org.eclipse.microprofile.openapi.models.responses.APIResponses;
import org.eclipse.microprofile.openapi.models.security.OAuthFlow;
import org.eclipse.microprofile.openapi.models.security.OAuthFlows;
import org.eclipse.microprofile.openapi.models.security.Scopes;
import org.eclipse.microprofile.openapi.models.security.SecurityRequirement;
import org.eclipse.microprofile.openapi.models.security.SecurityScheme;
import org.eclipse.microprofile.openapi.models.servers.Server;
import org.eclipse.microprofile.openapi.models.servers.ServerVariable;
import org.eclipse.microprofile.openapi.models.servers.ServerVariables;
import org.eclipse.microprofile.openapi.models.tags.Tag;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.ClassType;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.Indexer;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.MethodParameterInfo;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ArchivePath;
import org.jboss.shrinkwrap.api.Node;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.importer.ZipImporter;
import org.wildfly.swarm.microprofile.openapi.api.OpenApiConfig;
import org.wildfly.swarm.microprofile.openapi.api.models.ComponentsImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.ExternalDocumentationImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.OpenAPIImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.OperationImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.PathItemImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.PathsImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.callbacks.CallbackImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.examples.ExampleImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.headers.HeaderImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.info.ContactImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.info.InfoImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.info.LicenseImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.links.LinkImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.media.ContentImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.media.DiscriminatorImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.media.EncodingImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.media.MediaTypeImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.media.SchemaImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.parameters.ParameterImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.parameters.RequestBodyImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.responses.APIResponseImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.responses.APIResponsesImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.security.OAuthFlowImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.security.OAuthFlowsImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.security.ScopesImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.security.SecurityRequirementImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.security.SecuritySchemeImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.servers.ServerImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.servers.ServerVariableImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.servers.ServerVariablesImpl;
import org.wildfly.swarm.microprofile.openapi.api.models.tags.TagImpl;
import org.wildfly.swarm.microprofile.openapi.api.util.MergeUtil;
import org.wildfly.swarm.microprofile.openapi.runtime.scanner.OpenApiDataObjectScanner;
import org.wildfly.swarm.microprofile.openapi.runtime.util.JandexUtil;
import org.wildfly.swarm.microprofile.openapi.runtime.util.JandexUtil.JaxRsParameterInfo;
import org.wildfly.swarm.microprofile.openapi.runtime.util.JandexUtil.RefType;
import org.wildfly.swarm.microprofile.openapi.runtime.util.ModelUtil;
import org.wildfly.swarm.spi.api.JARArchive;

/**
 * Scans a deployment (using the archive and jandex annotation index) for JAX-RS and
 * OpenAPI annotations.  These annotations, if found, are used to generate a valid
 * OpenAPI model.  For reference, see:
 *
 * https://github.com/eclipse/microprofile-open-api/blob/master/spec/src/main/asciidoc/microprofile-openapi-spec.adoc#annotations
 *
 * @author eric.wittmann@gmail.com
 */
@SuppressWarnings("rawtypes")
public class OpenApiAnnotationScanner {

    private static Logger LOG = Logger.getLogger("org.wildfly.swarm.microprofile.openapi");

    private final IndexView index;

    private OpenAPIImpl oai;

    private String currentAppPath = "";
    private String currentResourcePath = "";
    private String[] currentConsumes;
    private String[] currentProduces;

    private SchemaRegistry schemaRegistry = new SchemaRegistry();

    /**
     * Constructor.
     * @param config
     * @param archive
     */
    public OpenApiAnnotationScanner(OpenApiConfig config, Archive archive) {
        this.index = archiveToIndex(config, archive);
    }

    /**
     * Index the archive to produce a jandex index.
     * @param config
     * @param archive
     */
    protected static IndexView archiveToIndex(OpenApiConfig config, Archive archive) {
        if (archive == null) {
            throw new RuntimeException("Archive was null!");
        }

        Indexer indexer = new Indexer();
        index(indexer, "org/wildfly/swarm/microprofile/openapi/runtime/scanner/CollectionStandin.class");
        index(indexer, "org/wildfly/swarm/microprofile/openapi/runtime/scanner/MapStandin.class");
        indexArchive(config, indexer, archive);
        return indexer.complete();
    }

    private static void index(Indexer indexer, String resName) {
        ClassLoader cl = OpenApiAnnotationScanner.class.getClassLoader();
        try (InputStream klazzStream = cl.getResourceAsStream(resName)) {
            indexer.index(klazzStream);
        } catch (IOException ioe) {
            throw new UncheckedIOException(ioe);
        }
    }

    /**
     * Indexes the given archive.
     * @param config
     * @param indexer
     * @param archive
     */
    @SuppressWarnings("unchecked")
    private static void indexArchive(OpenApiConfig config, Indexer indexer, Archive archive) {
        Map<ArchivePath, Node> c = archive.getContent();
        try {
            for (Map.Entry<ArchivePath, Node> each : c.entrySet()) {
                ArchivePath archivePath = each.getKey();
                if (archivePath.get().endsWith(OpenApiConstants.CLASS_SUFFIX) && acceptClassForScanning(config, archivePath.get())) {
                    try (InputStream contentStream = each.getValue().getAsset().openStream()) {
                        LOG.debugv("Indexing asset: {0} from archive: {1}", archivePath.get(), archive.getName());
                        indexer.index(contentStream);
                    }
                    continue;
                }
                if (archivePath.get().endsWith(OpenApiConstants.JAR_SUFFIX) && acceptJarForScanning(config, archivePath.get())) {
                    try (InputStream contentStream = each.getValue().getAsset().openStream()) {
                        JARArchive jarArchive = ShrinkWrap.create(JARArchive.class, archivePath.get())
                                .as(ZipImporter.class).importFrom(contentStream).as(JARArchive.class);
                        indexArchive(config, indexer, jarArchive);
                    }
                    continue;
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Returns true if the given JAR archive (dependency) should be cracked open and indexed
     * along with the rest of the deployment's classes.
     * @param config
     * @param jarName
     */
    private static boolean acceptJarForScanning(OpenApiConfig config, String jarName) {
        if (config.scanDependenciesDisable()) {
            return false;
        }
        Set<String> scanDependenciesJars = config.scanDependenciesJars();
        String nameOnly = new File(jarName).getName();
        if (scanDependenciesJars.isEmpty() || scanDependenciesJars.contains(nameOnly)) {
            return true;
        }
        return false;
    }

    /**
     * Returns true if the class represented by the given archive path should be included in
     * the annotation index.
     * @param config
     * @param archivePath
     */
    private static boolean acceptClassForScanning(OpenApiConfig config, String archivePath) {
        if (archivePath == null) {
            return false;
        }

        Set<String> scanClasses = config.scanClasses();
        Set<String> scanPackages = config.scanPackages();
        Set<String> scanExcludeClasses = config.scanExcludeClasses();
        Set<String> scanExcludePackages = config.scanExcludePackages();
        if (scanClasses.isEmpty() && scanPackages.isEmpty() && scanExcludeClasses.isEmpty() && scanExcludePackages.isEmpty()) {
            return true;
        }

        if (archivePath.startsWith(OpenApiConstants.WEB_ARCHIVE_CLASS_PREFIX)) {
            archivePath = archivePath.substring(OpenApiConstants.WEB_ARCHIVE_CLASS_PREFIX.length());
        }
        String fqcn = archivePath.replaceAll("/", ".").substring(0, archivePath.lastIndexOf(OpenApiConstants.CLASS_SUFFIX));
        String packageName = "";
        if (fqcn.contains(".")) {
            int idx = fqcn.lastIndexOf(".");
            packageName = fqcn.substring(0, idx);
        }

        boolean accept;
        // Includes
        if (scanClasses.isEmpty() && scanPackages.isEmpty()) {
            accept = true;
        } else if (!scanClasses.isEmpty() && scanPackages.isEmpty()) {
            accept = scanClasses.contains(fqcn);
        } else if (scanClasses.isEmpty() && !scanPackages.isEmpty()) {
            accept = scanPackages.contains(packageName);
        } else {
            accept = scanClasses.contains(fqcn) || scanPackages.contains(packageName);
        }
        // Excludes override includes
        if (!scanExcludeClasses.isEmpty() && scanExcludeClasses.contains(fqcn)) {
            accept = false;
        }
        if (!scanExcludePackages.isEmpty() && scanExcludePackages.contains(packageName)) {
            accept = false;
        }
        return accept;
    }

    /**
     * Scan the deployment for relevant annotations.  Returns an OpenAPI data model that was
     * built from those found annotations.
     */
    public OpenAPIImpl scan() {
        LOG.debug("Scanning deployment for OpenAPI and JAX-RS Annotations.");

        // Initialize a new OAI document.  Even if nothing is found, this will be returned.
        oai = new OpenAPIImpl();
        oai.setOpenapi(OpenApiConstants.OPEN_API_VERSION);

        // Get all jax-rs applications and convert them to OAI models (and merge them into a single one)
        Collection<ClassInfo> applications = this.index.getAllKnownSubclasses(DotName.createSimple(Application.class.getName()));
        for (ClassInfo classInfo : applications) {
            oai = MergeUtil.merge(oai, jaxRsApplicationToOpenApi(classInfo));
        }

        // TODO find all OpenAPIDefinition annotations at the package level

        // Now find all jax-rs endpoints
        Collection<ClassInfo> resourceClasses = JandexUtil.getJaxRsResourceClasses(this.index);
        for (ClassInfo resourceClass : resourceClasses) {
            processJaxRsResourceClass(oai, resourceClass);
        }

        // Now that all paths have been created, sort them (we don't have a better way to organize them).
        if (oai != null) {
            Paths paths = oai.getPaths();
            if (paths != null) {
                Paths sortedPaths = new PathsImpl();
                TreeSet<String> sortedKeys = new TreeSet<>(paths.keySet());
                for (String pathKey : sortedKeys) {
                    PathItem pathItem = paths.get(pathKey);
                    sortedPaths.addPathItem(pathKey, pathItem);
                }
                sortedPaths.setExtensions(paths.getExtensions());
                oai.setPaths(sortedPaths);
            }
        }

        return oai;
    }

    /**
     * Processes a JAX-RS {@link Application} and creates an {@link OpenAPI} model.  Performs
     * annotation scanning and other processing.  Returns a model unique to that single JAX-RS
     * app.
     * @param applicationClass
     */
    private OpenAPIImpl jaxRsApplicationToOpenApi(ClassInfo applicationClass) {
        OpenAPIImpl oai = new OpenAPIImpl();
        oai.setOpenapi(OpenApiConstants.OPEN_API_VERSION);

        // Get the @ApplicationPath info and save it for later (also support @Path which seems nonstandard but common).
        ////////////////////////////////////////
        AnnotationInstance appPathAnno = JandexUtil.getClassAnnotation(applicationClass, OpenApiConstants.DOTNAME_APPLICATION_PATH);
        if (appPathAnno == null) {
            appPathAnno = JandexUtil.getClassAnnotation(applicationClass, OpenApiConstants.DOTNAME_PATH);
        }
        if (appPathAnno != null) {
            this.currentAppPath = appPathAnno.value().asString();
        } else {
            this.currentAppPath = "/";
        }

        // Get the @OpenAPIDefinition annotation and process it.
        ////////////////////////////////////////
        AnnotationInstance openApiDefAnno = JandexUtil.getClassAnnotation(applicationClass, OpenApiConstants.DOTNAME_OPEN_API_DEFINITION);
        if (openApiDefAnno != null) {
            processDefinition(oai, openApiDefAnno);
        }

        // Process @SecurityScheme annotations
        ////////////////////////////////////////
        List<AnnotationInstance> securitySchemeAnnotations = JandexUtil.getRepeatableAnnotation(applicationClass,
                OpenApiConstants.DOTNAME_SECURITY_SCHEME, OpenApiConstants.DOTNAME_SECURITY_SCHEMES);
        for (AnnotationInstance annotation : securitySchemeAnnotations) {
            String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_SECURITY_SCHEME_NAME);
            if (name == null && JandexUtil.isRef(annotation)) {
                name = JandexUtil.nameFromRef(annotation);
            }
            if (name != null) {
                SecurityScheme securityScheme = readSecurityScheme(annotation);
                Components components = ModelUtil.components(oai);
                components.addSecurityScheme(name, securityScheme);
            }
        }

        // Process @Server annotations
        ///////////////////////////////////
        List<AnnotationInstance> serverAnnotations = JandexUtil.getRepeatableAnnotation(applicationClass,
                OpenApiConstants.DOTNAME_SERVER, OpenApiConstants.DOTNAME_SERVERS);
        for (AnnotationInstance annotation : serverAnnotations) {
            Server server = readServer(annotation);
            oai.addServer(server);
        }

        return oai;
    }

    /**
     * Processing a single JAX-RS resource class (annotated with @Path).
     * @param openApi
     * @param resourceClass
     */
    private void processJaxRsResourceClass(OpenAPIImpl openApi, ClassInfo resourceClass) {
        LOG.debug("Processing a JAX-RS resource class: " + resourceClass.simpleName());

        // Set the current resource path.
        AnnotationInstance pathAnno = JandexUtil.getClassAnnotation(resourceClass, OpenApiConstants.DOTNAME_PATH);
        this.currentResourcePath = pathAnno.value().asString();

        // TODO handle the use-case where the resource class extends a base class, and the base class has jax-rs relevant methods and annotations

        // Process @SecurityScheme annotations
        ////////////////////////////////////////
        List<AnnotationInstance> securitySchemeAnnotations = JandexUtil.getRepeatableAnnotation(resourceClass,
                OpenApiConstants.DOTNAME_SECURITY_SCHEME, OpenApiConstants.DOTNAME_SECURITY_SCHEMES);
        for (AnnotationInstance annotation : securitySchemeAnnotations) {
            String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_SECURITY_SCHEME_NAME);
            if (name == null && JandexUtil.isRef(annotation)) {
                name = JandexUtil.nameFromRef(annotation);
            }
            if (name != null) {
                SecurityScheme securityScheme = readSecurityScheme(annotation);
                Components components = ModelUtil.components(openApi);
                components.addSecurityScheme(name, securityScheme);
            }
        }

        // Process tags (both declarations and references)
        ////////////////////////////////////////
        Set<String> tagRefs = new HashSet<>();
        AnnotationInstance tagAnno = JandexUtil.getClassAnnotation(resourceClass, OpenApiConstants.DOTNAME_TAG);
        if (tagAnno != null) {
            if (JandexUtil.isRef(tagAnno)) {
                String tagRef = JandexUtil.stringValue(tagAnno, OpenApiConstants.PROP_REF);
                tagRefs.add(tagRef);
            } else {
                Tag tag = readTag(tagAnno);
                if (tag.getName() != null) {
                    openApi.addTag(tag);
                    tagRefs.add(tag.getName());
                }
            }
        }
        AnnotationInstance tagsAnno = JandexUtil.getClassAnnotation(resourceClass, OpenApiConstants.DOTNAME_TAGS);
        if (tagsAnno != null) {
            AnnotationValue tagsArrayVal = tagsAnno.value();
            if (tagsArrayVal != null) {
                AnnotationInstance[] tagsArray = tagsArrayVal.asNestedArray();
                for (AnnotationInstance ta : tagsArray) {
                    if (JandexUtil.isRef(ta)) {
                        String tagRef = JandexUtil.stringValue(ta, OpenApiConstants.PROP_REF);
                        tagRefs.add(tagRef);
                    } else {
                        Tag tag = readTag(ta);
                        if (tag.getName() != null) {
                            openApi.addTag(tag);
                            tagRefs.add(tag.getName());
                        }
                    }
                }
            }

            List<String> listValue = JandexUtil.stringListValue(tagsAnno, OpenApiConstants.PROP_REFS);
            if (listValue != null) {
                tagRefs.addAll(listValue);
            }
        }

        // Now find and process the operation methods
        ////////////////////////////////////////
        for (MethodInfo methodInfo : resourceClass.methods()) {
            AnnotationInstance get = methodInfo.annotation(OpenApiConstants.DOTNAME_GET);
            if (get != null) {
                processJaxRsMethod(openApi, resourceClass, methodInfo, get, HttpMethod.GET, tagRefs);
            }
            AnnotationInstance put = methodInfo.annotation(OpenApiConstants.DOTNAME_PUT);
            if (put != null) {
                processJaxRsMethod(openApi, resourceClass, methodInfo, put, HttpMethod.PUT, tagRefs);
            }
            AnnotationInstance post = methodInfo.annotation(OpenApiConstants.DOTNAME_POST);
            if (post != null) {
                processJaxRsMethod(openApi, resourceClass, methodInfo, post, HttpMethod.POST, tagRefs);
            }
            AnnotationInstance delete = methodInfo.annotation(OpenApiConstants.DOTNAME_DELETE);
            if (delete != null) {
                processJaxRsMethod(openApi, resourceClass, methodInfo, delete, HttpMethod.DELETE, tagRefs);
            }
            AnnotationInstance head = methodInfo.annotation(OpenApiConstants.DOTNAME_HEAD);
            if (head != null) {
                processJaxRsMethod(openApi, resourceClass, methodInfo, head, HttpMethod.HEAD, tagRefs);
            }
            AnnotationInstance options = methodInfo.annotation(OpenApiConstants.DOTNAME_OPTIONS);
            if (options != null) {
                processJaxRsMethod(openApi, resourceClass, methodInfo, options, HttpMethod.OPTIONS, tagRefs);
            }
        }
    }

    /**
     * Process a single JAX-RS method to produce an OpenAPI Operation.
     * @param openApi
     * @param resource
     * @param method
     * @param methodAnno
     * @param methodType
     * @param resourceTags
     */
    private void processJaxRsMethod(OpenAPIImpl openApi, ClassInfo resource, MethodInfo method,
            AnnotationInstance methodAnno, HttpMethod methodType, Set<String> resourceTags) {

        LOG.debugf("Processing jax-rs method: {0}", method.toString());

        // Figure out the path for the operation.  This is a combination of the App, Resource, and Method @Path annotations
        String path;
        if (method.hasAnnotation(OpenApiConstants.DOTNAME_PATH)) {
            AnnotationInstance pathAnno = method.annotation(OpenApiConstants.DOTNAME_PATH);
            String methodPath = pathAnno.value().asString();
            path = makePath(this.currentAppPath, this.currentResourcePath, methodPath);
        } else {
            path = makePath(this.currentAppPath, this.currentResourcePath);
        }

        // Get or create a PathItem to hold the operation
        PathItem pathItem = ModelUtil.paths(openApi).get(path);
        if (pathItem == null) {
            pathItem = new PathItemImpl();
            ModelUtil.paths(openApi).addPathItem(path, pathItem);
        }

        // Figure out the current @Produces and @Consumes (if any)
        currentConsumes = null;
        currentProduces = null;
        AnnotationInstance consumesAnno = method.annotation(OpenApiConstants.DOTNAME_CONSUMES);
        if (consumesAnno == null) {
            consumesAnno = JandexUtil.getClassAnnotation(method.declaringClass(), OpenApiConstants.DOTNAME_CONSUMES);
        }
        AnnotationInstance producesAnno = method.annotation(OpenApiConstants.DOTNAME_PRODUCES);
        if (producesAnno == null) {
            producesAnno = JandexUtil.getClassAnnotation(method.declaringClass(), OpenApiConstants.DOTNAME_PRODUCES);
        }

        if (consumesAnno != null) {
            AnnotationValue annotationValue = consumesAnno.value();
            if (annotationValue != null) {
                currentConsumes = annotationValue.asStringArray();
            } else {
                currentConsumes = OpenApiConstants.DEFAULT_CONSUMES;
            }
        }
        if (producesAnno != null) {
            AnnotationValue annotationValue = producesAnno.value();
            if (annotationValue != null) {
                currentProduces = annotationValue.asStringArray();
            } else {
                currentProduces = OpenApiConstants.DEFAULT_PRODUCES;
            }
        }

        Operation operation = new OperationImpl();

        // Process any @Operation annotation
        /////////////////////////////////////////
        if (method.hasAnnotation(OpenApiConstants.DOTNAME_OPERATION)) {
            AnnotationInstance operationAnno = method.annotation(OpenApiConstants.DOTNAME_OPERATION);
            // If the operation is marked as hidden, just bail here because we don't want it as part of the model.
            if (operationAnno.value(OpenApiConstants.PROP_HIDDEN) != null && operationAnno.value(OpenApiConstants.PROP_HIDDEN).asBoolean()) {
                return;
            }
            // Otherwise, set various bits of meta-data from the values in the @Operation annotation
            operation.setSummary(JandexUtil.stringValue(operationAnno, OpenApiConstants.PROP_SUMMARY));
            operation.setDescription(JandexUtil.stringValue(operationAnno, OpenApiConstants.PROP_DESCRIPTION));
            operation.setOperationId(JandexUtil.stringValue(operationAnno, OpenApiConstants.PROP_OPERATION_ID));
            operation.setDeprecated(JandexUtil.booleanValue(operationAnno, OpenApiConstants.PROP_DEPRECATED));
        }

        // Process tags - @Tag and @Tags annotations combines with the resource tags we've already found (passed in)
        /////////////////////////////////////////
        boolean hasOpTags = false;
        Set<String> tags = new HashSet<>();
        if (method.hasAnnotation(OpenApiConstants.DOTNAME_TAG)) {
            hasOpTags = true;
            AnnotationInstance tagAnno = method.annotation(OpenApiConstants.DOTNAME_TAG);
            if (JandexUtil.isRef(tagAnno)) {
                String tagRef = JandexUtil.stringValue(tagAnno, OpenApiConstants.PROP_REF);
                tags.add(tagRef);
            } else if (JandexUtil.isEmpty(tagAnno)) {
                // Nothing to do here.  The @Tag() was empty.
            } else {
                Tag tag = readTag(tagAnno);
                if (tag.getName() != null) {
                    openApi.addTag(tag);
                    tags.add(tag.getName());
                }
            }
        }
        if (method.hasAnnotation(OpenApiConstants.DOTNAME_TAGS)) {
            hasOpTags = true;
            AnnotationInstance tagsAnno = method.annotation(OpenApiConstants.DOTNAME_TAGS);
            AnnotationValue tagsArrayVal = tagsAnno.value();
            if (tagsArrayVal != null) {
                AnnotationInstance[] tagsArray = tagsArrayVal.asNestedArray();
                for (AnnotationInstance tagAnno : tagsArray) {
                    if (JandexUtil.isRef(tagAnno)) {
                        String tagRef = JandexUtil.stringValue(tagAnno, OpenApiConstants.PROP_REF);
                        tags.add(tagRef);
                    } else {
                        Tag tag = readTag(tagAnno);
                        if (tag.getName() != null) {
                            openApi.addTag(tag);
                            tags.add(tag.getName());
                        }
                    }
                }
            }

            List<String> listValue = JandexUtil.stringListValue(tagsAnno, OpenApiConstants.PROP_REFS);
            if (listValue != null) {
                tags.addAll(listValue);
            }
        }
        if (!hasOpTags) {
            tags.addAll(resourceTags);
        }
        if (!tags.isEmpty()) {
            operation.setTags(new ArrayList<>(tags));
        }

        // Process @Parameter annotations
        /////////////////////////////////////////
        List<AnnotationInstance> parameterAnnotations = JandexUtil.getRepeatableAnnotation(method,
                OpenApiConstants.DOTNAME_PARAMETER, OpenApiConstants.DOTNAME_PARAMETERS);
        for (AnnotationInstance annotation : parameterAnnotations) {
            Parameter parameter = readParameter(annotation);
            if (parameter == null) {
                // Param was hidden
                continue;
            }

            AnnotationTarget target = annotation.target();
            // If target is null, then the @Parameter was found wrapped in a @Parameters
            // If the target is METHOD, then the @Parameter is on the method itself
            // If the target is METHOD_PARAMETER, then the @Parameter is on one of the method's arguments (THIS ONE WE CARE ABOUT)
            if (target != null && target.kind() == Kind.METHOD_PARAMETER) {
                In in = parameterIn(target.asMethodParameter());
                parameter.setIn(in);

                // if the Parameter model we read does *NOT* have a Schema at this point, then create one from the method argument's type
                if (!ModelUtil.parameterHasSchema(parameter)) {
                    Type paramType = JandexUtil.getMethodParameterType(method, target.asMethodParameter().position());
                    Schema schema = typeToSchema(paramType);
                    ModelUtil.setParameterSchema(parameter, schema);
                }
            } else {
                // TODO if the @Parameter is on the method, we could perhaps find the one it refers to by name
                // and use its type to create a Schema (if missing)
            }

            operation.addParameter(parameter);
        }
        // Now process any jax-rs parameters that were NOT annotated with @Parameter (do not yet exist in the model)
        List<Type> parameters = method.parameters();
        for (int idx = 0; idx < parameters.size(); idx++) {
            JaxRsParameterInfo paramInfo = JandexUtil.getMethodParameterJaxRsInfo(method, idx);
            if (paramInfo != null && !ModelUtil.operationHasParameter(operation, paramInfo.name)) {
                Type paramType = parameters.get(idx);
                Parameter parameter = new ParameterImpl();
                parameter.setName(paramInfo.name);
                parameter.setIn(paramInfo.in);
                parameter.setRequired(true);
                Schema schema = typeToSchema(paramType);
                parameter.setSchema(schema);
                operation.addParameter(parameter);
            }

        }

        // TODO @Parameter can be located on a field - what does that mean?
        // TODO need to handle the case where we have @BeanParam annotations


        // Process any @RequestBody annotation
        /////////////////////////////////////////
        // note: the @RequestBody annotation can be found on a method argument *or* on the method
        List<AnnotationInstance> requestBodyAnnotations = JandexUtil.getRepeatableAnnotation(method, OpenApiConstants.DOTNAME_REQUEST_BODY, null);
        for (AnnotationInstance annotation : requestBodyAnnotations) {
            RequestBody requestBody = readRequestBody(annotation);
            // TODO if the method argument type is Request, don't generate a Schema!
            if (!ModelUtil.requestBodyHasSchema(requestBody)) {
                Type requestBodyType = null;
                if (annotation.target().kind() == AnnotationTarget.Kind.METHOD_PARAMETER) {
                    requestBodyType = JandexUtil.getMethodParameterType(method, annotation.target().asMethodParameter().position());
                } else if (annotation.target().kind() == AnnotationTarget.Kind.METHOD) {
                    requestBodyType = JandexUtil.getRequestBodyParameterClassType(method);
                }
                if (requestBodyType != null) {
                    Schema schema = typeToSchema(requestBodyType);
                    ModelUtil.setRequestBodySchema(requestBody, schema, currentConsumes);
                }
            }
            operation.setRequestBody(requestBody);
        }
        // If the request body is null, figure it out from the parameters.  Only if the
        // method declares that it @Consumes data
        if (operation.getRequestBody() == null && currentConsumes != null) {
            Type requestBodyType = JandexUtil.getRequestBodyParameterClassType(method);
            if (requestBodyType != null) {
                Schema schema = typeToSchema(requestBodyType);
                if (schema != null) {
                    RequestBody requestBody = new RequestBodyImpl();
                    ModelUtil.setRequestBodySchema(requestBody, schema, currentConsumes);
                    operation.setRequestBody(requestBody);
                }
            }
        }


        // Process @APIResponse annotations
        /////////////////////////////////////////
        List<AnnotationInstance> apiResponseAnnotations = JandexUtil.getRepeatableAnnotation(method,
                OpenApiConstants.DOTNAME_API_RESPONSE, OpenApiConstants.DOTNAME_API_RESPONSES);
        for (AnnotationInstance annotation : apiResponseAnnotations) {
            String responseCode = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_RESPONSE_CODE);
            if (responseCode == null) {
                responseCode = APIResponses.DEFAULT;
            }
            APIResponse response = readResponse(annotation);
            APIResponses responses = ModelUtil.responses(operation);
            responses.addApiResponse(responseCode, response);
        }
        // If there are no responses from annotations, try to create a response from the method return value.
        if (operation.getResponses() == null || operation.getResponses().isEmpty()) {
            createResponseFromJaxRsMethod(method, operation);
        }

        // Process @SecurityRequirement annotations
        ///////////////////////////////////////////
        List<AnnotationInstance> securityRequirementAnnotations = JandexUtil.getRepeatableAnnotation(method,
                OpenApiConstants.DOTNAME_SECURITY_REQUIREMENT, OpenApiConstants.DOTNAME_SECURITY_REQUIREMENTS);
        securityRequirementAnnotations.addAll(
            JandexUtil.getRepeatableAnnotation(resource, OpenApiConstants.DOTNAME_SECURITY_REQUIREMENT, OpenApiConstants.DOTNAME_SECURITY_REQUIREMENTS)
        );
        for (AnnotationInstance annotation : securityRequirementAnnotations) {
            SecurityRequirement requirement = readSecurityRequirement(annotation);
            if (requirement != null) {
                operation.addSecurityRequirement(requirement);
            }
        }

        // Process @Callback annotations
        /////////////////////////////////////////
        List<AnnotationInstance> callbackAnnotations = JandexUtil.getRepeatableAnnotation(method,
                OpenApiConstants.DOTNAME_CALLBACK, OpenApiConstants.DOTNAME_CALLBACKS);
        Map<String, Callback> callbacks = new LinkedHashMap<>();
        for (AnnotationInstance annotation : callbackAnnotations) {
            String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_NAME);
            if (name == null && JandexUtil.isRef(annotation)) {
                name = JandexUtil.nameFromRef(annotation);
            }
            if (name != null) {
                callbacks.put(name, readCallback(annotation));
            }

            if (!callbacks.isEmpty()) {
                operation.setCallbacks(callbacks);
            }
        }

        // Process @Server annotations
        ///////////////////////////////////
        List<AnnotationInstance> serverAnnotations = JandexUtil.getRepeatableAnnotation(method,
                OpenApiConstants.DOTNAME_SERVER, OpenApiConstants.DOTNAME_SERVERS);
        if (serverAnnotations.isEmpty()) {
            serverAnnotations.addAll(JandexUtil.getRepeatableAnnotation(method.declaringClass(),
                    OpenApiConstants.DOTNAME_SERVER, OpenApiConstants.DOTNAME_SERVERS));
        }
        for (AnnotationInstance annotation : serverAnnotations) {
            Server server = readServer(annotation);
            operation.addServer(server);
        }

        // Now set the operation on the PathItem as appropriate based on the Http method type
        ///////////////////////////////////////////
        switch (methodType) {
            case DELETE:
                pathItem.setDELETE(operation);
                break;
            case GET:
                pathItem.setGET(operation);
                break;
            case HEAD:
                pathItem.setHEAD(operation);
                break;
            case OPTIONS:
                pathItem.setOPTIONS(operation);
                break;
            case PATCH:
                pathItem.setPATCH(operation);
                break;
            case POST:
                pathItem.setPOST(operation);
                break;
            case PUT:
                pathItem.setPUT(operation);
                break;
            case TRACE:
                pathItem.setTRACE(operation);
                break;
            default:
                break;
        }
    }

    /**
     * Called when a jax-rs method's APIResponse annotations have all been processed but
     * no response was actually created for the operation.  This method will create a response
     * from the method information and add it to the given operation.  It will try to do this
     * by examining the method's return value and the type of operation (GET, PUT, POST, DELETE).
     *
     * If there is a return value of some kind (a non-void return type) then the response code
     * is assumed to be 200.
     *
     * If there not a return value (void return type) then either a 201 or 204 is returned,
     * depending on the type of request.
     *
     * TODO generate responses for each checked exception?
     * @param method
     * @param operation
     */
    private void createResponseFromJaxRsMethod(MethodInfo method, Operation operation) {
        Type returnType = method.returnType();

        Schema schema;
        APIResponses responses;
        APIResponse response;
        ContentImpl content;

        if (returnType.kind() == Type.Kind.VOID) {
            String code = "204";
            if (method.hasAnnotation(OpenApiConstants.DOTNAME_POST)) {
                code = "201";
            }
            responses = ModelUtil.responses(operation);
            response = new APIResponseImpl();
            responses.addApiResponse(code, response);
        } else {
            schema = typeToSchema(returnType);
            responses = ModelUtil.responses(operation);
            response = new APIResponseImpl();
            content = new ContentImpl();
            String[] produces = this.currentProduces;
            if (produces == null || produces.length == 0) {
                produces = OpenApiConstants.DEFAULT_PRODUCES;
            }
            for (String producesType : produces) {
                MediaType mt = new MediaTypeImpl();
                mt.setSchema(schema);
                content.addMediaType(producesType, mt);
            }
            response.setContent(content);
            responses.addApiResponse("200", response);
        }
    }

    /**
     * Converts a jandex type to a {@link Schema} model.
     * @param type
     */
    private Schema typeToSchema(Type type) {
        Schema schema = null;
        if (type.kind() == Type.Kind.CLASS) {
            schema = introspectClassToSchema(type.asClassType(), true);
        } else if (type.kind() == Type.Kind.PRIMITIVE) {
            schema = OpenApiDataObjectScanner.process(type.asPrimitiveType());
        } else {
            schema = OpenApiDataObjectScanner.process(index, type);
        }
        return schema;
    }

    /**
     * Determines where an @Parameter can be found (examples include Query, Path,
     * Header, Cookie, etc).
     * @param target
     */
    private In parameterIn(MethodParameterInfo paramInfo) {
        MethodInfo method = paramInfo.method();
        short paramPosition = paramInfo.position();
        List<AnnotationInstance> annotations = JandexUtil.getParameterAnnotations(method, paramPosition);
        for (AnnotationInstance annotation : annotations) {
            if (annotation.name().equals(OpenApiConstants.DOTNAME_QUERY_PARAM)) {
                return In.QUERY;
            }
            if (annotation.name().equals(OpenApiConstants.DOTNAME_PATH_PARAM)) {
                return In.PATH;
            }
            if (annotation.name().equals(OpenApiConstants.DOTNAME_HEADER_PARAM)) {
                return In.HEADER;
            }
            if (annotation.name().equals(OpenApiConstants.DOTNAME_COOKIE_PARAM)) {
                return In.COOKIE;
            }
        }
        return null;
    }

    /**
     * Make a path out of a number of path segments.
     * @param segments
     */
    protected static String makePath(String ... segments) {
        StringBuilder builder = new StringBuilder();
        for (String segment : segments) {
            if (segment.startsWith("/")) {
                segment = segment.substring(1);
            }
            if (segment.endsWith("/")) {
                segment = segment.substring(0, segment.length() - 1);
            }
            if (segment.isEmpty()) {
                continue;
            }
            builder.append("/");
            builder.append(segment);
        }
        String rval = builder.toString();
        if (rval.isEmpty()) {
            return "/";
        }
        return rval;
    }

    /**
     * Reads a OpenAPIDefinition annotation.
     * @param openApi
     * @param definitionAnno
     */
    protected void processDefinition(OpenAPIImpl openApi, AnnotationInstance definitionAnno) {
        LOG.debug("Processing an @OpenAPIDefinition annotation.");
        openApi.setInfo(readInfo(definitionAnno.value(OpenApiConstants.PROP_INFO)));
        openApi.setTags(readTags(definitionAnno.value(OpenApiConstants.PROP_TAGS)));
        openApi.setServers(readServers(definitionAnno.value(OpenApiConstants.PROP_SERVERS)));
        openApi.setSecurity(readSecurity(definitionAnno.value(OpenApiConstants.PROP_SECURITY)));
        openApi.setExternalDocs(readExternalDocs(definitionAnno.value(OpenApiConstants.PROP_EXTERNAL_DOCS)));
        openApi.setComponents(readComponents(definitionAnno.value(OpenApiConstants.PROP_COMPONENTS)));
    }

    /**
     * Reads an Info annotation.
     * @param infoAnno
     */
    private Info readInfo(AnnotationValue infoAnno) {
        if (infoAnno == null) {
            return null;
        }
        LOG.debug("Processing an @Info annotation.");
        AnnotationInstance nested = infoAnno.asNested();
        InfoImpl info = new InfoImpl();
        info.setTitle(JandexUtil.stringValue(nested, OpenApiConstants.PROP_TITLE));
        info.setDescription(JandexUtil.stringValue(nested, OpenApiConstants.PROP_DESCRIPTION));
        info.setTermsOfService(JandexUtil.stringValue(nested, OpenApiConstants.PROP_TERMS_OF_SERVICE));
        info.setContact(readContact(nested.value(OpenApiConstants.PROP_CONTACT)));
        info.setLicense(readLicense(nested.value(OpenApiConstants.PROP_LICENSE)));
        info.setVersion(JandexUtil.stringValue(nested, OpenApiConstants.PROP_VERSION));
        return info;
    }

    /**
     * Reads an Contact annotation.
     * @param contactAnno
     */
    private Contact readContact(AnnotationValue contactAnno) {
        if (contactAnno == null) {
            return null;
        }
        LOG.debug("Processing an @Contact annotation.");
        AnnotationInstance nested = contactAnno.asNested();
        ContactImpl contact = new ContactImpl();
        contact.setName(JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME));
        contact.setUrl(JandexUtil.stringValue(nested, OpenApiConstants.PROP_URL));
        contact.setEmail(JandexUtil.stringValue(nested, OpenApiConstants.PROP_EMAIL));
        return contact;
    }

    /**
     * Reads an License annotation.
     * @param licenseAnno
     */
    private License readLicense(AnnotationValue licenseAnno) {
        if (licenseAnno == null) {
            return null;
        }
        LOG.debug("Processing an @License annotation.");
        AnnotationInstance nested = licenseAnno.asNested();
        LicenseImpl license = new LicenseImpl();
        license.setName(JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME));
        license.setUrl(JandexUtil.stringValue(nested, OpenApiConstants.PROP_URL));
        return license;
    }

    /**
     * Reads any Tag annotations.  The annotation
     * value is an array of Tag annotations.
     * @param tagAnnos
     */
    private List<Tag> readTags(AnnotationValue tagAnnos) {
        if (tagAnnos == null) {
            return null;
        }
        LOG.debug("Processing an array of @Tag annotations.");
        AnnotationInstance[] nestedArray = tagAnnos.asNestedArray();
        List<Tag> tags = new ArrayList<>();
        for (AnnotationInstance tagAnno : nestedArray) {
            if (!JandexUtil.isRef(tagAnno)) {
                tags.add(readTag(tagAnno));
            }
        }
        return tags;
    }

    /**
     * Reads a single Tag annotation.
     * @param tagAnno
     */
    private Tag readTag(AnnotationInstance tagAnno) {
        if (tagAnno == null) {
            return null;
        }
        LOG.debug("Processing a single @Tag annotation.");
        TagImpl tag = new TagImpl();
        tag.setName(JandexUtil.stringValue(tagAnno, OpenApiConstants.PROP_NAME));
        tag.setDescription(JandexUtil.stringValue(tagAnno, OpenApiConstants.PROP_DESCRIPTION));
        tag.setExternalDocs(readExternalDocs(tagAnno.value(OpenApiConstants.PROP_EXTERNAL_DOCS)));
        return tag;
    }

    /**
     * Reads any Server annotations.  The annotation value is an array of Server annotations.
     * @param serverAnnos
     */
    private List<Server> readServers(AnnotationValue serverAnnos) {
        if (serverAnnos == null) {
            return null;
        }
        LOG.debug("Processing an array of @Server annotations.");
        AnnotationInstance[] nestedArray = serverAnnos.asNestedArray();
        List<Server> servers = new ArrayList<>();
        for (AnnotationInstance serverAnno : nestedArray) {
            servers.add(readServer(serverAnno));
        }
        return servers;
    }

    /**
     * Reads a single Server annotation.
     * @param serverAnno
     */
    private Server readServer(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        return readServer(value.asNested());
    }

    /**
     * Reads a single Server annotation.
     * @param serverAnno
     */
    private Server readServer(AnnotationInstance serverAnno) {
        if (serverAnno == null) {
            return null;
        }
        LOG.debug("Processing a single @Server annotation.");
        ServerImpl server = new ServerImpl();
        server.setUrl(JandexUtil.stringValue(serverAnno, OpenApiConstants.PROP_URL));
        server.setDescription(JandexUtil.stringValue(serverAnno, OpenApiConstants.PROP_DESCRIPTION));
        server.setVariables(readServerVariables(serverAnno.value(OpenApiConstants.PROP_VARIABLES)));
        return server;
    }

    /**
     * Reads an array of ServerVariable annotations, returning a new {@link ServerVariables} model.  The
     * annotation value is an array of ServerVariable annotations.
     * @param value
     * @return
     */
    private ServerVariables readServerVariables(AnnotationValue serverVariableAnnos) {
        if (serverVariableAnnos == null) {
            return null;
        }
        LOG.debug("Processing an array of @ServerVariable annotations.");
        AnnotationInstance[] nestedArray = serverVariableAnnos.asNestedArray();
        ServerVariables variables = new ServerVariablesImpl();
        for (AnnotationInstance serverVariableAnno : nestedArray) {
            String name = JandexUtil.stringValue(serverVariableAnno, OpenApiConstants.PROP_NAME);
            if (name != null) {
                variables.addServerVariable(name, readServerVariable(serverVariableAnno));
            }
        }
        return variables;
    }

    /**
     * Reads a single ServerVariable annotation.
     * @param serverVariableAnno
     */
    private ServerVariable readServerVariable(AnnotationInstance serverVariableAnno) {
        if (serverVariableAnno == null) {
            return null;
        }
        LOG.debug("Processing a single @ServerVariable annotation.");
        ServerVariable variable = new ServerVariableImpl();
        variable.setDescription(JandexUtil.stringValue(serverVariableAnno, OpenApiConstants.PROP_DESCRIPTION));
        variable.setEnumeration(JandexUtil.stringListValue(serverVariableAnno, OpenApiConstants.PROP_ENUMERATION));
        variable.setDefaultValue(JandexUtil.stringValue(serverVariableAnno, OpenApiConstants.PROP_DEFAULT_VALUE));
        return variable;
    }

    /**
     * Reads any SecurityRequirement annotations.  The annotation value is an array of
     * SecurityRequirement annotations.
     * @param value
     */
    private List<SecurityRequirement> readSecurity(AnnotationValue securityRequirementAnnos) {
        if (securityRequirementAnnos == null) {
            return null;
        }
        LOG.debug("Processing an array of @SecurityRequirement annotations.");
        AnnotationInstance[] nestedArray = securityRequirementAnnos.asNestedArray();
        List<SecurityRequirement> requirements = new ArrayList<>();
        for (AnnotationInstance requirementAnno : nestedArray) {
            SecurityRequirement requirement = readSecurityRequirement(requirementAnno);
            if (requirement != null) {
                requirements.add(requirement);
            }
        }
        return requirements;
    }

    /**
     * Reads a single SecurityRequirement annotation.
     * @param annotation
     */
    private SecurityRequirement readSecurityRequirement(AnnotationInstance annotation) {
        String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_NAME);
        if (name != null) {
            List<String> scopes = JandexUtil.stringListValue(annotation, OpenApiConstants.PROP_SCOPES);
            SecurityRequirement requirement = new SecurityRequirementImpl();
            if (scopes == null) {
                requirement.addScheme(name);
            } else {
                requirement.addScheme(name, scopes);
            }
            return requirement;
        }
        return null;
    }

    /**
     * Reads an ExternalDocumentation annotation.
     * @param externalDocAnno
     */
    private ExternalDocumentation readExternalDocs(AnnotationValue externalDocAnno) {
        if (externalDocAnno == null) {
            return null;
        }
        LOG.debug("Processing an @ExternalDocumentation annotation.");
        AnnotationInstance nested = externalDocAnno.asNested();
        ExternalDocumentation externalDoc = new ExternalDocumentationImpl();
        externalDoc.setDescription(JandexUtil.stringValue(nested, OpenApiConstants.PROP_DESCRIPTION));
        externalDoc.setUrl(JandexUtil.stringValue(nested, OpenApiConstants.PROP_URL));
        return externalDoc;
    }

    /**
     * Reads any Components annotations.
     * @param componentsAnno
     */
    private Components readComponents(AnnotationValue componentsAnno) {
        if (componentsAnno == null) {
            return null;
        }
        LOG.debug("Processing an @Components annotation.");
        AnnotationInstance nested = componentsAnno.asNested();
        Components components = new ComponentsImpl();
        // TODO for EVERY item below, handle the case where the annotation is ref-only.  then strip the ref path and use the final segment as the name
        components.setCallbacks(readCallbacks(nested.value(OpenApiConstants.PROP_CALLBACKS)));
        components.setExamples(readExamples(nested.value(OpenApiConstants.PROP_EXAMPLES)));
        components.setHeaders(readHeaders(nested.value(OpenApiConstants.PROP_HEADERS)));
        components.setLinks(readLinks(nested.value(OpenApiConstants.PROP_LINKS)));
        components.setParameters(readParameters(nested.value(OpenApiConstants.PROP_PARAMETERS)));
        components.setRequestBodies(readRequestBodies(nested.value(OpenApiConstants.PROP_REQUEST_BODIES)));
        components.setResponses(readResponses(nested.value(OpenApiConstants.PROP_RESPONSES)));
        components.setSchemas(readSchemas(nested.value(OpenApiConstants.PROP_SCHEMAS)));
        components.setSecuritySchemes(readSecuritySchemes(nested.value(OpenApiConstants.PROP_SECURITY_SCHEMES)));
        return components;
    }

    /**
     * Reads a map of Callback annotations.
     * @param value
     */
    private Map<String, Callback> readCallbacks(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a map of @Callback annotations.");
        Map<String, Callback> map = new LinkedHashMap<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
            if (name == null && JandexUtil.isRef(nested)) {
                name = JandexUtil.nameFromRef(nested);
            }
            if (name != null) {
                map.put(name, readCallback(nested));
            }
        }
        return map;
    }

    /**
     * Reads a Callback annotation into a model.
     * @param annotation
     */
    private Callback readCallback(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @Callback annotation.");
        Callback callback = new CallbackImpl();
        callback.setRef(JandexUtil.refValue(annotation, RefType.Callback));
        String expression = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_CALLBACK_URL_EXPRESSION);
        callback.put(expression, readCallbackOperations(annotation.value(OpenApiConstants.PROP_OPERATIONS)));
        return callback;
    }

    /**
     * Reads the CallbackOperation annotations as a PathItem.  The annotation value
     * in this case is an array of CallbackOperation annotations.
     * @param value
     */
    private PathItem readCallbackOperations(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing an array of @CallbackOperation annotations.");
        AnnotationInstance[] nestedArray = value.asNestedArray();
        PathItem pathItem = new PathItemImpl();
        for (AnnotationInstance operationAnno : nestedArray) {
            String method = JandexUtil.stringValue(operationAnno, OpenApiConstants.PROP_METHOD);
            Operation operation = readCallbackOperation(operationAnno);
            if (method == null) {
                continue;
            }
            try {
                PropertyDescriptor descriptor = PropertyUtils.getPropertyDescriptor(pathItem, method.toUpperCase());
                Method mutator = PropertyUtils.getWriteMethod(descriptor);
                mutator.invoke(pathItem, operation);
            } catch (Exception e) {
                LOG.error("Error reading a CallbackOperation annotation.", e);
            }
        }
        return pathItem;
    }

    /**
     * Reads a single CallbackOperation annotation.
     * @param operationAnno
     * @return
     */
    private Operation readCallbackOperation(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @CallbackOperation annotation.");
        Operation operation = new OperationImpl();
        operation.setSummary(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_SUMMARY));
        operation.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
        operation.setExternalDocs(readExternalDocs(annotation.value(OpenApiConstants.PROP_EXTERNAL_DOCS)));
        operation.setParameters(readCallbackOperationParameters(annotation.value(OpenApiConstants.PROP_PARAMETERS)));
        operation.setRequestBody(readRequestBody(annotation.value(OpenApiConstants.PROP_REQUEST_BODY)));
        operation.setResponses(readCallbackOperationResponses(annotation.value(OpenApiConstants.PROP_RESPONSES)));
        operation.setSecurity(readSecurity(annotation.value(OpenApiConstants.PROP_SECURITY)));
        operation.setExtensions(readExtensions(annotation.value(OpenApiConstants.PROP_EXTENSIONS)));
        return operation;
    }

    /**
     * Reads an array of Parameter annotations into a list.
     * @param value
     */
    private List<Parameter> readCallbackOperationParameters(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a list of @Parameter annotations.");
        List<Parameter> parameters = new ArrayList<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            parameters.add(readParameter(nested));
        }
        return parameters;
    }

    /**
     * Reads an array of APIResponse annotations into an {@link APIResponses} model.
     * @param value
     */
    private APIResponses readCallbackOperationResponses(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a list of @APIResponse annotations into an APIResponses model.");
        APIResponses responses = new APIResponsesImpl();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            String responseCode = JandexUtil.stringValue(nested, OpenApiConstants.PROP_RESPONSE_CODE);
            if (responseCode != null) {
                responses.put(responseCode, readResponse(nested));
            }
        }
        return responses;
    }

    /**
     * Reads a map of Example annotations.
     * @param value
     */
    private Map<String, Example> readExamples(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a map of @ExampleObject annotations.");
        Map<String, Example> map = new LinkedHashMap<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
            if (name == null && JandexUtil.isRef(nested)) {
                name = JandexUtil.nameFromRef(nested);
            }
            if (name != null) {
                map.put(name, readExample(nested));
            }
        }
        return map;
    }

    /**
     * Reads a Example annotation into a model.
     * @param annotation
     */
    private Example readExample(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @ExampleObject annotation.");
        Example example = new ExampleImpl();
        example.setSummary(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_SUMMARY));
        example.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
        example.setValue(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_VALUE));
        example.setExternalValue(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_EXTERNAL_VALUE));
        example.setRef(JandexUtil.refValue(annotation, RefType.Example));
        return example;
    }

    /**
     * Reads a map of Header annotations.
     * @param value
     */
    private Map<String, Header> readHeaders(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a map of @Header annotations.");
        Map<String, Header> map = new LinkedHashMap<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
            if (name == null && JandexUtil.isRef(nested)) {
                name = JandexUtil.nameFromRef(nested);
            }
            if (name != null) {
                map.put(name, readHeader(nested));
            }
        }
        return map;
    }

    /**
     * Reads a Header annotation into a model.
     * @param annotation
     */
    private Header readHeader(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @Header annotation.");
        Header header = new HeaderImpl();
        header.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
        header.setSchema(readSchema(annotation.value(OpenApiConstants.PROP_SCHEMA)));
        header.setRequired(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_REQUIRED));
        header.setDeprecated(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_DEPRECATED));
        header.setAllowEmptyValue(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_ALLOW_EMPTY_VALUE));
        header.setRef(JandexUtil.refValue(annotation, RefType.Header));
        return header;
    }

    /**
     * Reads a map of Link annotations.
     * @param value
     */
    private Map<String, Link> readLinks(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a map of @Link annotations.");
        Map<String, Link> map = new LinkedHashMap<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
            if (name == null && JandexUtil.isRef(nested)) {
                name = JandexUtil.nameFromRef(nested);
            }
            if (name != null) {
                map.put(name, readLink(nested));
            }
        }
        return map;
    }

    /**
     * Reads a Link annotation into a model.
     * @param annotation
     */
    private Link readLink(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @Link annotation.");
        Link link = new LinkImpl();
        link.setOperationRef(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_OPERATION_REF));
        link.setOperationId(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_OPERATION_ID));
        link.setParameters(readLinkParameters(annotation.value(OpenApiConstants.PROP_PARAMETERS)));
        link.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
        link.setRequestBody(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_REQUEST_BODY));
        link.setServer(readServer(annotation.value(OpenApiConstants.PROP_SERVER)));
        link.setRef(JandexUtil.refValue(annotation, RefType.Link));
        return link;
    }

    /**
     * Reads an array of LinkParameter annotations into a map.
     * @param value
     */
    private Map<String, Object> readLinkParameters(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        AnnotationInstance[] nestedArray = value.asNestedArray();
        Map<String, Object> linkParams = new LinkedHashMap<>();
        for (AnnotationInstance annotation : nestedArray) {
            String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_NAME);
            if (name != null) {
                String expression = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_EXPRESSION);
                linkParams.put(name, expression);
            }
        }
        return linkParams;
    }

    /**
     * Reads a map of Parameter annotations.
     * @param value
     */
    private Map<String, Parameter> readParameters(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a map of @Parameter annotations.");
        Map<String, Parameter> map = new LinkedHashMap<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
            if (name == null && JandexUtil.isRef(nested)) {
                name = JandexUtil.nameFromRef(nested);
            }
            if (name != null) {
                Parameter parameter = readParameter(nested);
                if (parameter != null) {
                    map.put(name, parameter);
                }
            }
        }
        return map;
    }

    /**
     * Reads a Parameter annotation into a model.
     * @param annotation
     */
    private Parameter readParameter(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @Link annotation.");

        // Params can be hidden. Skip if that's the case.
        Boolean isHidden = JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_HIDDEN);
        if (isHidden != null && isHidden == Boolean.TRUE) {
            return null;
        }

        Parameter parameter = new ParameterImpl();
        parameter.setName(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_NAME));
        parameter.setIn(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_IN, org.eclipse.microprofile.openapi.models.parameters.Parameter.In.class));
        parameter.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
        parameter.setRequired(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_REQUIRED));
        parameter.setDeprecated(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_DEPRECATED));
        parameter.setAllowEmptyValue(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_ALLOW_EMPTY_VALUE));
        parameter.setStyle(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_STYLE, org.eclipse.microprofile.openapi.models.parameters.Parameter.Style.class));
        parameter.setExplode(readExplode(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_EXPLODE, org.eclipse.microprofile.openapi.annotations.enums.Explode.class)));
        parameter.setAllowReserved(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_ALLOW_RESERVED));
        parameter.setSchema(readSchema(annotation.value(OpenApiConstants.PROP_SCHEMA)));
        parameter.setContent(readContent(annotation.value(OpenApiConstants.PROP_CONTENT), ContentDirection.Parameter));
        parameter.setExamples(readExamples(annotation.value(OpenApiConstants.PROP_EXAMPLES)));
        parameter.setExample(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_EXAMPLE));
        parameter.setRef(JandexUtil.refValue(annotation, RefType.Parameter));
        return parameter;
    }

    /**
     * Converts from an Explode enum to a true/false/null.
     * @param enumValue
     */
    private Boolean readExplode(Explode enumValue) {
        if (enumValue == Explode.TRUE) {
            return Boolean.TRUE;
        }
        if (enumValue == Explode.FALSE) {
            return Boolean.FALSE;
        }
        return null;
    }

    /**
     * Reads a single Content annotation into a model.  The value in this case is an array of
     * Content annotations.
     * @param value
     */
    private Content readContent(AnnotationValue value, ContentDirection direction) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a single @Content annotation.");
        Content content = new ContentImpl();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            String contentType = JandexUtil.stringValue(nested, OpenApiConstants.PROP_MEDIA_TYPE);
            MediaType mediaTypeModel = readMediaType(nested);
            if (contentType == null) {
                // If the content type is not provided in the @Content annotation, then
                // we assume it applies to all the jax-rs method's @Consumes or @Produces
                String[] mimeTypes = {};
                if (direction == ContentDirection.Input && currentConsumes != null) {
                    mimeTypes = currentConsumes;
                }
                if (direction == ContentDirection.Output && currentProduces != null) {
                    mimeTypes = currentProduces;
                }
                if (direction == ContentDirection.Parameter) {
                    mimeTypes = OpenApiConstants.DEFAULT_PARAMETER_MEDIA_TYPES;
                }
                for (String mimeType : mimeTypes) {
                    content.addMediaType(mimeType, mediaTypeModel);
                }
            } else {
                content.addMediaType(contentType, mediaTypeModel);
            }
        }
        return content;
    }

    /**
     * Reads a single Content annotation into a {@link MediaType} model.
     * @param nested
     */
    private MediaType readMediaType(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @Content annotation as a MediaType.");
        MediaType mediaType = new MediaTypeImpl();
        mediaType.setExamples(readExamples(annotation.value(OpenApiConstants.PROP_EXAMPLES)));
        mediaType.setSchema(readSchema(annotation.value(OpenApiConstants.PROP_SCHEMA)));
        mediaType.setEncoding(readEncodings(annotation.value(OpenApiConstants.PROP_ENCODING)));
        return mediaType;
    }

    /**
     * Reads an array of Encoding annotations as a Map.
     * @param value
     */
    private Map<String, Encoding> readEncodings(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a map of @Encoding annotations.");
        Map<String, Encoding> map = new LinkedHashMap<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance annotation : nestedArray) {
            String name = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_NAME);
            if (name != null) {
                map.put(name, readEncoding(annotation));
            }
        }
        return map;
    }

    /**
     * Reads a single Encoding annotation into a model.
     * @param annotation
     */
    private Encoding readEncoding(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @Encoding annotation.");
        Encoding encoding = new EncodingImpl();
        encoding.setContentType(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_CONTENT_TYPE));
        encoding.setStyle(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_STYLE, org.eclipse.microprofile.openapi.models.media.Encoding.Style.class));
        encoding.setExplode(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_EXPLODE));
        encoding.setAllowReserved(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_ALLOW_RESERVED));
        encoding.setHeaders(readHeaders(annotation.value(OpenApiConstants.PROP_HEADERS)));
        return encoding;
    }

    /**
     * Reads a map of RequestBody annotations.
     * @param value
     */
    private Map<String, RequestBody> readRequestBodies(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a map of @RequestBody annotations.");
        Map<String, RequestBody> map = new LinkedHashMap<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
            if (name == null && JandexUtil.isRef(nested)) {
                name = JandexUtil.nameFromRef(nested);
            }
            if (name != null) {
                map.put(name, readRequestBody(nested));
            }
        }
        return map;
    }

    /**
     * Reads a RequestBody annotation into a model.
     * @param value
     */
    private RequestBody readRequestBody(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        return readRequestBody(value.asNested());
    }

    /**
     * Reads a RequestBody annotation into a model.
     * @param annotation
     */
    private RequestBody readRequestBody(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @RequestBody annotation.");
        RequestBody requestBody = new RequestBodyImpl();
        requestBody.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
        requestBody.setContent(readContent(annotation.value(OpenApiConstants.PROP_CONTENT), ContentDirection.Input));
        requestBody.setRequired(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_REQUIRED));
        requestBody.setRef(JandexUtil.refValue(annotation, RefType.RequestBody));
        return requestBody;
    }

    /**
     * Reads a map of APIResponse annotations.
     * @param value
     */
    private Map<String, APIResponse> readResponses(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a map of @APIResponse annotations.");
        Map<String, APIResponse> map = new LinkedHashMap<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
            if (name == null && JandexUtil.isRef(nested)) {
                name = JandexUtil.nameFromRef(nested);
            }
            if (name != null) {
                map.put(name, readResponse(nested));
            }
        }
        return map;
    }

    /**
     * Reads a APIResponse annotation into a model.
     * @param annotation
     */
    private APIResponse readResponse(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @Response annotation.");
        APIResponse response = new APIResponseImpl();
        response.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
        response.setHeaders(readHeaders(annotation.value(OpenApiConstants.PROP_HEADERS)));
        response.setLinks(readLinks(annotation.value(OpenApiConstants.PROP_LINKS)));
        response.setContent(readContent(annotation.value(OpenApiConstants.PROP_CONTENT), ContentDirection.Output));
        response.setRef(JandexUtil.refValue(annotation, RefType.Response));
        return response;
    }

    /**
     * Reads a map of Schema annotations.
     * @param value
     */
    private Map<String, Schema> readSchemas(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a map of @Schema annotations.");
        Map<String, Schema> map = new LinkedHashMap<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
            if (name == null && JandexUtil.isRef(nested)) {
                name = JandexUtil.nameFromRef(nested);
            }
            if (name != null) {
                map.put(name, readSchema(nested));
            }
        }
        return map;
    }

    /**
     * Reads a Schema annotation into a model.
     * @param annotation
     */
    private Schema readSchema(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        return readSchema(value.asNested());
    }

    /**
     * Reads a Schema annotation into a model.
     * @param annotation
     */
    @SuppressWarnings("unchecked")
    private Schema readSchema(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @Schema annotation.");

        // Schemas can be hidden. Skip if that's the case.
        Boolean isHidden = JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_HIDDEN);
        if (isHidden != null && isHidden == Boolean.TRUE) {
            return null;
        }

        Schema schema = new SchemaImpl();

        schema.setNot(readClassSchema(annotation.value(OpenApiConstants.PROP_NOT), true));
        schema.setOneOf(readClassSchemas(annotation.value(OpenApiConstants.PROP_ONE_OF)));
        schema.setAnyOf(readClassSchemas(annotation.value(OpenApiConstants.PROP_ANY_OF)));
        schema.setAllOf(readClassSchemas(annotation.value(OpenApiConstants.PROP_ALL_OF)));
        schema.setTitle(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_TITLE));
        schema.setMultipleOf(JandexUtil.bigDecimalValue(annotation, OpenApiConstants.PROP_MULTIPLE_OF));
        schema.setMaximum(JandexUtil.bigDecimalValue(annotation, OpenApiConstants.PROP_MAXIMUM));
        schema.setExclusiveMaximum(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_EXCLUSIVE_MAXIMUM));
        schema.setMinimum(JandexUtil.bigDecimalValue(annotation, OpenApiConstants.PROP_MINIMUM));
        schema.setExclusiveMinimum(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_EXCLUSIVE_MINIMUM));
        schema.setMaxLength(JandexUtil.intValue(annotation, OpenApiConstants.PROP_MAX_LENGTH));
        schema.setMinLength(JandexUtil.intValue(annotation, OpenApiConstants.PROP_MIN_LENGTH));
        schema.setPattern(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_PATTERN));
        schema.setMaxProperties(JandexUtil.intValue(annotation, OpenApiConstants.PROP_MAX_PROPERTIES));
        schema.setMinProperties(JandexUtil.intValue(annotation, OpenApiConstants.PROP_MIN_PROPERTIES));
        schema.setRequired(JandexUtil.stringListValue(annotation, OpenApiConstants.PROP_REQUIRED_PROPERTIES));
        schema.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
        schema.setFormat(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_FORMAT));
        schema.setRef(JandexUtil.refValue(annotation, RefType.Schema));
        schema.setNullable(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_NULLABLE));
        schema.setReadOnly(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_READ_ONLY));
        schema.setWriteOnly(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_WRITE_ONLY));
        schema.setExample(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_EXAMPLE));
        schema.setExternalDocs(readExternalDocs(annotation.value(OpenApiConstants.PROP_EXTERNAL_DOCS)));
        schema.setDeprecated(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_DEPRECATED));
        schema.setType(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_TYPE, org.eclipse.microprofile.openapi.models.media.Schema.SchemaType.class));
        schema.setEnumeration((List<Object>) (Object) JandexUtil.stringListValue(annotation, OpenApiConstants.PROP_ENUMERATION));
        schema.setDefaultValue(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DEFAULT_VALUE));
        schema.setDiscriminator(readDiscriminatorMappings(annotation.value(OpenApiConstants.PROP_DISCRIMINATOR_MAPPING)));
        schema.setMaxItems(JandexUtil.intValue(annotation, OpenApiConstants.PROP_MAX_ITEMS));
        schema.setMinItems(JandexUtil.intValue(annotation, OpenApiConstants.PROP_MIN_ITEMS));
        schema.setUniqueItems(JandexUtil.booleanValue(annotation, OpenApiConstants.PROP_UNIQUE_ITEMS));

        if (JandexUtil.isSimpleClassSchema(annotation)) {
            Schema implSchema = readClassSchema(annotation.value(OpenApiConstants.PROP_IMPLEMENTATION), true);
            schema = MergeUtil.mergeObjects(implSchema, schema);
        } else if (JandexUtil.isSimpleArraySchema(annotation)) {
            Schema implSchema = readClassSchema(annotation.value(OpenApiConstants.PROP_IMPLEMENTATION), true);
            // If the @Schema annotation indicates an array type, then use the Schema
            // generated from the implementation Class as the "items" for the array.
            schema.setItems(implSchema);
        } else {
            Schema implSchema = readClassSchema(annotation.value(OpenApiConstants.PROP_IMPLEMENTATION), false);
            // If there is an impl class - merge the @Schema properties *onto* the schema
            // generated from the Class so that the annotation properties override the class
            // properties (as required by the MP+OAI spec).
            schema = MergeUtil.mergeObjects(implSchema, schema);
        }

        return schema;
    }

    /**
     * Reads an array of Class annotations to produce a list of {@link Schema} models.
     * @param value
     */
    private List<Schema> readClassSchemas(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a list of schema Class annotations.");
        Type[] classArray = value.asClassArray();
        List<Schema> schemas = new ArrayList<>(classArray.length);
        for (Type type : classArray) {
            ClassType ctype = (ClassType) type;
            Schema schema = introspectClassToSchema(ctype, true);
            schemas.add(schema);
        }
        return schemas;
    }

    /**
     * Introspect into the given Class to generate a Schema model.
     * @param value
     */
    private Schema readClassSchema(AnnotationValue value, boolean schemaReferenceSupported) {
        if (value == null) {
            return null;
        }
        ClassType ctype = (ClassType) value.asClass();
        Schema schema = introspectClassToSchema(ctype, schemaReferenceSupported);
        return schema;
    }

    /**
     * Introspects the given class type to generate a Schema model.  The boolean indicates
     * whether this class type should be turned into a reference.
     * @param ctype
     * @param schemaReferenceSupported
     */
    private Schema introspectClassToSchema(ClassType ctype, boolean schemaReferenceSupported) {
        if (ctype.name().equals(OpenApiConstants.DOTNAME_RESPONSE)) {
            return null;
        }
        if (schemaReferenceSupported && this.schemaRegistry.has(ctype)) {
            GeneratedSchemaInfo schemaInfo = this.schemaRegistry.lookup(ctype);
            Schema rval = new SchemaImpl();
            rval.setRef(schemaInfo.$ref);
            return rval;
        } else {
            Schema schema = OpenApiDataObjectScanner.process(index, ctype);
            if (schemaReferenceSupported && schema != null && this.index.getClassByName(ctype.name()) != null) {
                GeneratedSchemaInfo schemaInfo = this.schemaRegistry.register(ctype, schema);
                ModelUtil.components(oai).addSchema(schemaInfo.name, schema);
                Schema rval = new SchemaImpl();
                rval.setRef(schemaInfo.$ref);
                return rval;
            } else {
                return schema;
            }
        }
    }

    /**
     * Reads an array of DiscriminatorMapping annotations into a {@link Discriminator} model.
     * @param value
     */
    private Discriminator readDiscriminatorMappings(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a list of @DiscriminatorMapping annotations.");
        Discriminator discriminator = new DiscriminatorImpl();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (@SuppressWarnings("unused") AnnotationInstance nested : nestedArray) {
            // TODO iterate the discriminator mappings and do something sensible with them! :(
        }
        return discriminator;
    }

    /**
     * Reads a map of SecurityScheme annotations.
     * @param value
     */
    private Map<String, SecurityScheme> readSecuritySchemes(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a map of @SecurityScheme annotations.");
        Map<String, SecurityScheme> map = new LinkedHashMap<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance nested : nestedArray) {
            String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_SECURITY_SCHEME_NAME);
            if (name == null && JandexUtil.isRef(nested)) {
                name = JandexUtil.nameFromRef(nested);
            }
            if (name != null) {
                map.put(name, readSecurityScheme(nested));
            }
        }
        return map;
    }

    /**
     * Reads a SecurityScheme annotation into a model.
     * @param annotation
     */
    private SecurityScheme readSecurityScheme(AnnotationInstance annotation) {
        if (annotation == null) {
            return null;
        }
        LOG.debug("Processing a single @SecurityScheme annotation.");
        SecurityScheme securityScheme = new SecuritySchemeImpl();
        securityScheme.setType(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_TYPE, org.eclipse.microprofile.openapi.models.security.SecurityScheme.Type.class));
        securityScheme.setDescription(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_DESCRIPTION));
        securityScheme.setName(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_API_KEY_NAME));
        securityScheme.setIn(JandexUtil.enumValue(annotation, OpenApiConstants.PROP_IN, org.eclipse.microprofile.openapi.models.security.SecurityScheme.In.class));
        securityScheme.setScheme(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_SCHEME));
        securityScheme.setBearerFormat(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_BEARER_FORMAT));
        securityScheme.setFlows(readOAuthFlows(annotation.value(OpenApiConstants.PROP_FLOWS)));
        securityScheme.setOpenIdConnectUrl(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_OPEN_ID_CONNECT_URL));
        securityScheme.setRef(JandexUtil.refValue(annotation, RefType.SecurityScheme));
        return securityScheme;
    }

    /**
     * Reads an OAuthFlows annotation into a model.
     * @param value
     */
    private OAuthFlows readOAuthFlows(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a single @OAuthFlows annotation.");
        AnnotationInstance annotation = value.asNested();
        OAuthFlows flows = new OAuthFlowsImpl();
        flows.setImplicit(readOAuthFlow(annotation.value(OpenApiConstants.PROP_IMPLICIT)));
        flows.setPassword(readOAuthFlow(annotation.value(OpenApiConstants.PROP_PASSWORD)));
        flows.setClientCredentials(readOAuthFlow(annotation.value(OpenApiConstants.PROP_CLIENT_CREDENTIALS)));
        flows.setAuthorizationCode(readOAuthFlow(annotation.value(OpenApiConstants.PROP_AUTHORIZATION_CODE)));
        return flows;
    }

    /**
     * Reads a single OAuthFlow annotation into a model.
     * @param value
     */
    private OAuthFlow readOAuthFlow(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a single @OAuthFlow annotation.");
        AnnotationInstance annotation = value.asNested();
        OAuthFlow flow = new OAuthFlowImpl();
        flow.setAuthorizationUrl(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_AUTHORIZATION_URL));
        flow.setTokenUrl(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_TOKEN_URL));
        flow.setRefreshUrl(JandexUtil.stringValue(annotation, OpenApiConstants.PROP_REFRESH_URL));
        flow.setScopes(readOAuthScopes(annotation.value(OpenApiConstants.PROP_SCOPES)));
        return flow;
    }

    /**
     * Reads an array of OAuthScope annotations into a Scopes model.
     * @param value
     */
    private Scopes readOAuthScopes(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        LOG.debug("Processing a list of @OAuthScope annotations.");
        AnnotationInstance[] nestedArray = value.asNestedArray();
        Scopes scopes = new ScopesImpl();
        for (AnnotationInstance nested : nestedArray) {
            String name = JandexUtil.stringValue(nested, OpenApiConstants.PROP_NAME);
            if (name != null) {
                String description = JandexUtil.stringValue(nested, OpenApiConstants.PROP_DESCRIPTION);
                scopes.addScope(name, description);
            }
        }
        return scopes;
    }

    /**
     * Reads an array of Extension annotations.  The AnnotationValue in this case is
     * an array of Extension annotations.  These must be read and converted into a Map.
     * @param value
     */
    private Map<String, Object> readExtensions(AnnotationValue value) {
        if (value == null) {
            return null;
        }
        Map<String, Object> extensions = new LinkedHashMap<>();
        AnnotationInstance[] nestedArray = value.asNestedArray();
        for (AnnotationInstance annotation : nestedArray) {
            String extName = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_NAME);
            String extValue = JandexUtil.stringValue(annotation, OpenApiConstants.PROP_VALUE);
            extensions.put(extName, extValue);
        }
        return extensions;
    }

    /**
     * Simple enum to indicate whether an @Content annotation being processed is
     * an input or an output.
     * @author eric.wittmann@gmail.com
     */
    private static enum ContentDirection {
        Input, Output, Parameter
    }

    /**
     * Information about a single generated schema.
     * @author eric.wittmann@gmail.com
     */
    protected static class GeneratedSchemaInfo {
        public String name;
        public Schema schema;
        public String $ref;
    }

    /**
     * A simple registry used to track schemas that have been generated and inserted
     * into the #/components section of the
     * @author eric.wittmann@gmail.com
     */
    protected static class SchemaRegistry {
        private Map<DotName, GeneratedSchemaInfo> registry = new HashMap<>();
        private Set<String> names = new HashSet<>();

        public GeneratedSchemaInfo register(ClassType instanceClass, Schema schema) {
            String name = instanceClass.name().local();
            int idx = 1;
            while (this.names.contains(name)) {
                name = instanceClass.name().local() + idx++;
            }
            GeneratedSchemaInfo info = new GeneratedSchemaInfo();
            info.schema = schema;
            info.name = name;
            info.$ref = "#/components/schemas/" + name;

            registry.put(instanceClass.name(), info);
            names.add(name);

            return info;
        }

        public GeneratedSchemaInfo lookup(ClassType instanceClass) {
            return registry.get(instanceClass.name());
        }

        public boolean has(ClassType instanceClass) {
            return registry.containsKey(instanceClass.name());
        }
    }

}
