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

io.helidon.microprofile.graphql.server.SchemaGenerator Maven / Gradle / Ivy

/*
 * Copyright (c) 2020, 2021 Oracle and/or its affiliates.
 *
 * 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 io.helidon.microprofile.graphql.server;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.MethodDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DiscoveredMethod;

import graphql.schema.DataFetcher;
import graphql.schema.DataFetcherFactories;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLScalarType;
import graphql.schema.PropertyDataFetcher;
import jakarta.json.bind.annotation.JsonbProperty;
import org.eclipse.microprofile.graphql.Description;
import org.eclipse.microprofile.graphql.GraphQLApi;
import org.eclipse.microprofile.graphql.Id;
import org.eclipse.microprofile.graphql.Input;
import org.eclipse.microprofile.graphql.Interface;
import org.eclipse.microprofile.graphql.Mutation;
import org.eclipse.microprofile.graphql.Name;
import org.eclipse.microprofile.graphql.NonNull;
import org.eclipse.microprofile.graphql.Query;
import org.eclipse.microprofile.graphql.Source;
import org.eclipse.microprofile.graphql.Type;

import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_DATE_SCALAR;
import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_DATE_TIME_SCALAR;
import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_OFFSET_DATE_TIME_SCALAR;
import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_TIME_SCALAR;
import static io.helidon.microprofile.graphql.server.CustomScalars.CUSTOM_ZONED_DATE_TIME_SCALAR;
import static io.helidon.microprofile.graphql.server.FormattingHelper.DATE;
import static io.helidon.microprofile.graphql.server.FormattingHelper.NO_FORMATTING;
import static io.helidon.microprofile.graphql.server.FormattingHelper.NUMBER;
import static io.helidon.microprofile.graphql.server.FormattingHelper.formatDate;
import static io.helidon.microprofile.graphql.server.FormattingHelper.formatNumber;
import static io.helidon.microprofile.graphql.server.FormattingHelper.getCorrectDateFormatter;
import static io.helidon.microprofile.graphql.server.FormattingHelper.getCorrectNumberFormat;
import static io.helidon.microprofile.graphql.server.FormattingHelper.getFormattingAnnotation;
import static io.helidon.microprofile.graphql.server.FormattingHelper.isJsonbAnnotationPresent;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DATETIME_SCALAR;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DATE_SCALAR;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DiscoveredMethod.MUTATION_TYPE;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.DiscoveredMethod.QUERY_TYPE;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_DATETIME_SCALAR;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_DATE_SCALAR;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_OFFSET_DATETIME_SCALAR;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_TIME_SCALAR;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.FORMATTED_ZONED_DATETIME_SCALAR;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ID;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.STRING;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.TIME_SCALAR;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.checkScalars;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ensureConfigurationException;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ensureFormat;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.ensureValidName;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getAnnotationValue;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getArrayLevels;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getDefaultValueAnnotationValue;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getDescription;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getFieldAnnotations;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getFieldName;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getGraphQLType;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getMethodAnnotations;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getMethodName;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getParameterAnnotations;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getRootArrayClass;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getSafeClass;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getScalar;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getSimpleName;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.getTypeName;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isArrayType;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isDateTimeScalar;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isEnumClass;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isGraphQLType;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.isPrimitive;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.shouldIgnoreField;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.shouldIgnoreMethod;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.stripMethodName;
import static io.helidon.microprofile.graphql.server.SchemaGeneratorHelper.validateIDClass;

/**
 * Various utilities for generating {@link Schema}s from classes.
 */
class SchemaGenerator {

    /**
     * "is" prefix.
     */
    protected static final String IS = "is";

    /**
     * "get" prefix.
     */
    protected static final String GET = "get";

    /**
     * "set" prefix.
     */
    protected static final String SET = "set";

    /**
     * Logger.
     */
    private static final Logger LOGGER = Logger.getLogger(SchemaGenerator.class.getName());

    /**
     * {@link JandexUtils} instance to hold indexes.
     */
    private JandexUtils jandexUtils;

    /**
     * Holds the {@link Set} of unresolved types while processing the annotations.
     */
    private Set setUnresolvedTypes = new HashSet<>();

    /**
     * Holds the {@link Set} of additional methods that need to be added to types.
     */
    private Set setAdditionalMethods = new HashSet<>();

    private final Set> collectedApis = new HashSet<>();

    /**
     * Construct a {@link SchemaGenerator}.
     *
     * @param builder the {@link io.helidon.microprofile.graphql.server.SchemaGenerator.Builder} to construct from
     */
    private SchemaGenerator(Builder builder) {
        this.collectedApis.addAll(builder.collectedApis);
        jandexUtils = JandexUtils.create();
        jandexUtils.loadIndexes();
        if (!jandexUtils.hasIndex()) {
            String message = "Unable to find or load jandex index files: "
                    + jandexUtils.getIndexFile() + ".\nEnsure you are using the "
                    + "jandex-maven-plugin when you are building your application";
            LOGGER.warning(message);
        }
    }

    /**
     * Fluent API builder to create {@link SchemaGenerator}.
     *
     * @return new builder instance
     */
    public static Builder builder() {
        return new SchemaGenerator.Builder();
    }

    /**
     * Generate a {@link Schema} by scanning all discovered classes using the {@link GraphQlCdiExtension}.
     *
     * @return a {@link Schema}
     * @throws java.lang.IllegalStateException in case the schema cannot be generated
     */
    public Schema generateSchema() {
        int count = collectedApis.size();

        LOGGER.info("Discovered " + count + " annotated GraphQL API class" + (count != 1 ? "es" : ""));

        try {
            return generateSchemaFromClasses(collectedApis);
        } catch (IntrospectionException | ClassNotFoundException e) {
            throw new IllegalStateException("Cannot generate schema", e);
        }
    }

    /**
     * Generate a {@link Schema} from a given array of classes.  The classes are checked to see if they contain any of the
     * annotations from the microprofile spec.
     *
     * @param clazzes array of classes to check
     * @return a {@link Schema}
     *
     * @throws IntrospectionException if any errors with introspection
     * @throws ClassNotFoundException if any classes are not found
     */
    protected Schema generateSchemaFromClasses(Set> clazzes) throws IntrospectionException, ClassNotFoundException {
        Schema schema = Schema.create();
        setUnresolvedTypes.clear();
        setAdditionalMethods.clear();

        SchemaType rootQueryType = SchemaType.builder().name(schema.getQueryName()).build();
        SchemaType rootMutationType = SchemaType.builder().name(schema.getMutationName()).build();

        // process any specific classes with the Input, Type or Interface annotations
        for (Class clazz : clazzes) {
            // only include interfaces and concrete classes/enums
            if (clazz.isInterface() || (!clazz.isInterface() && !Modifier.isAbstract(clazz.getModifiers()))) {
                // Discover Enum via annotation
                if (clazz.isAnnotationPresent(org.eclipse.microprofile.graphql.Enum.class)) {
                    schema.addEnum(generateEnum(clazz));
                    continue;
                }

                // Type, Interface, Input are all treated similarly
                Type typeAnnotation = clazz.getAnnotation(Type.class);
                Interface interfaceAnnotation = clazz.getAnnotation(Interface.class);
                Input inputAnnotation = clazz.getAnnotation(Input.class);

                if (typeAnnotation != null && inputAnnotation != null) {
                    ensureConfigurationException(LOGGER, "Class " + clazz.getName() + " has been annotated with"
                            + " both Type and Input");
                }

                if (typeAnnotation != null || interfaceAnnotation != null) {
                    if (interfaceAnnotation != null && !clazz.isInterface()) {
                        ensureConfigurationException(LOGGER, "Class " + clazz.getName() + " has been annotated with"
                                + " @Interface but is not one");
                    }

                    // assuming value for annotation overrides @Name
                    String typeName = getTypeName(clazz, true);
                    SchemaType type = SchemaType.builder()
                            .name(typeName.isBlank() ? clazz.getSimpleName() : typeName)
                            .valueClassName(clazz.getName()).build();
                    type.isInterface(clazz.isInterface());
                    type.description(getDescription(clazz.getAnnotation(Description.class)));

                    // add the discovered type
                    addTypeToSchema(schema, type);

                    if (type.isInterface()) {
                        // is an interface so check for any implementors and add them too
                        jandexUtils.getKnownImplementors(clazz.getName()).forEach(c -> setUnresolvedTypes.add(c.getName()));
                    }
                } else if (inputAnnotation != null) {
                    String clazzName = clazz.getName();
                    String simpleName = clazz.getSimpleName();

                    SchemaInputType inputType = generateType(clazzName, true).createInputType("");
                    // if the name of the InputType was not changed then append "Input"
                    if (inputType.name().equals(simpleName)) {
                        inputType.name(inputType.name() + "Input");
                    }

                    if (!schema.containsInputTypeWithName(inputType.name())) {
                        schema.addInputType(inputType);
                        checkInputType(schema, inputType);
                    }
                }

                // obtain top level query API's
                if (clazz.isAnnotationPresent(GraphQLApi.class)) {
                    processGraphQLApiAnnotations(rootQueryType, rootMutationType, schema, clazz);
                }
            }
        }

        schema.addType(rootQueryType);
        schema.addType(rootMutationType);

        // process unresolved types
        processUnresolvedTypes(schema);

        // look though all of interface type and see if any of the known types implement
        // the interface and if so, add the interface to the type
        schema.getTypes().stream().filter(SchemaType::isInterface).forEach(it -> {
            schema.getTypes().stream().filter(t -> !t.isInterface() && t.valueClassName() != null).forEach(type -> {
                Class interfaceClass = getSafeClass(it.valueClassName());
                Class typeClass = getSafeClass(type.valueClassName());
                if (interfaceClass != null
                        && typeClass != null
                        && interfaceClass.isAssignableFrom(typeClass)) {
                    type.implementingInterface(it.name());
                }
            });
        });

        // process any additional methods required via the @Source annotation
        for (DiscoveredMethod dm : setAdditionalMethods) {
            // add the discovered method to the type
            SchemaType type = schema.getTypeByClass(dm.source());
            if (type != null) {
                SchemaFieldDefinition fd = newFieldDefinition(dm, null);
                // add all arguments which are not source arguments
                if (dm.arguments().size() > 0) {
                    dm.arguments().stream().filter(a -> !a.isSourceArgument())
                            .forEach(fd::addArgument);
                }

                // check for existing DataFetcher
                fd.dataFetcher(DataFetcherUtils.newMethodDataFetcher(
                        schema, dm.method().getDeclaringClass(), dm.method(),
                        dm.source(), fd.arguments().toArray(new SchemaArgument[0])));
                type.addFieldDefinition(fd);

                // we are creating this as a type so ignore any Input annotation
                String simpleName = getSimpleName(fd.returnType(), true);
                String returnType = fd.returnType();
                if (!simpleName.equals(returnType)) {
                    updateLongTypes(schema, returnType, simpleName);
                }
            }
        }

        // process default values for dates
        processDefaultDateTimeValues(schema);

        // process the @GraphQLApi annotated classes
        if (rootQueryType.fieldDefinitions().size() == 0 && rootMutationType.fieldDefinitions().size() == 0) {
            LOGGER.warning("Unable to find any classes with @GraphQLApi annotation."
                                   + "Unable to build schema");
        }

        return schema;
    }

    /**
     * Process all {@link SchemaFieldDefinition}s and {@link SchemaArgument}s and update the default values for any scalars.
     *
     * @param schema {@link Schema} to update
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    private void processDefaultDateTimeValues(Schema schema) {
        // concatenate both the SchemaType and SchemaInputType
        Stream streamInputTypes = schema.getInputTypes().stream().map(it -> (SchemaType) it);
        Stream streamAll = Stream.concat(streamInputTypes, schema.getTypes().stream());
        streamAll.forEach(t -> {
            t.fieldDefinitions().forEach(fd -> {
                String returnType = fd.returnType();
                // only check Date/Time/DateTime scalars that are not Queries or don't have data fetchers
                // as default formatting has already been dealt with
                if (isDateTimeScalar(returnType) && (t.name().equals(Schema.QUERY) || fd.dataFetcher() == null)) {
                    String[] existingFormat = fd.format();
                    // check if this type is an array type and if so then get the actual original type
                    Class clazzOriginalType = fd.originalArrayType() != null
                            ? fd.originalArrayType() : fd.originalType();
                    String[] newFormat = ensureFormat(returnType, clazzOriginalType.getName(), existingFormat);
                    if (!Arrays.equals(newFormat, existingFormat) && newFormat.length == 2) {
                        // formats differ so set the new format and DataFetcher
                        fd.format(newFormat);
                        if (fd.dataFetcher() == null) {
                            // create the raw array to pass to the retrieveFormattingDataFetcher method
                            DataFetcher dataFetcher = retrieveFormattingDataFetcher(
                                    new String[] {DATE, newFormat[0], newFormat[1]},
                                    fd.name(), clazzOriginalType.getName());
                            fd.dataFetcher(dataFetcher);
                        }
                        fd.defaultFormatApplied(true);
                        SchemaScalar scalar = schema.getScalarByName(fd.returnType());
                        GraphQLScalarType newScalarType = null;
                        if (fd.returnType().equals(FORMATTED_DATE_SCALAR)) {
                            fd.returnType(DATE_SCALAR);
                            newScalarType = CUSTOM_DATE_SCALAR;
                        } else if (fd.returnType().equals(FORMATTED_TIME_SCALAR)) {
                            fd.returnType(TIME_SCALAR);
                            newScalarType = CUSTOM_TIME_SCALAR;
                        } else if (fd.returnType().equals(FORMATTED_DATETIME_SCALAR)) {
                            fd.returnType(DATETIME_SCALAR);
                            newScalarType = CUSTOM_DATE_TIME_SCALAR;
                        } else if (fd.returnType().equals(FORMATTED_OFFSET_DATETIME_SCALAR)) {
                            fd.returnType(FORMATTED_OFFSET_DATETIME_SCALAR);
                            newScalarType = CUSTOM_OFFSET_DATE_TIME_SCALAR;
                        } else if (fd.returnType().equals(FORMATTED_ZONED_DATETIME_SCALAR)) {
                            fd.returnType(FORMATTED_ZONED_DATETIME_SCALAR);
                            newScalarType = CUSTOM_ZONED_DATE_TIME_SCALAR;
                        }

                        // clone the scalar with the new scalar name
                        SchemaScalar newScalar = new SchemaScalar(fd.returnType(), scalar.actualClass(),
                                                                  newScalarType, scalar.defaultFormat());
                        if (!schema.containsScalarWithName(newScalar.name())) {
                            schema.addScalar(newScalar);
                        }
                    }
                }

                // check the SchemaArguments
                fd.arguments().forEach(a -> {
                    String argumentType = a.argumentType();
                    if (isDateTimeScalar(argumentType)) {
                        String[] existingArgFormat = a.format();
                        Class clazzOriginalType = a.originalArrayType() != null
                                ? a.originalArrayType() : a.originalType();
                        String[] newArgFormat = ensureFormat(argumentType, clazzOriginalType.getName(), existingArgFormat);
                        if (!Arrays.equals(newArgFormat, existingArgFormat) && newArgFormat.length == 2) {
                            a.format(newArgFormat);
                        }
                    }
                });
            });
        });
    }

    /**
     * Process any unresolved types.
     *
     * @param schema {@link Schema} to add types to
     */
    private void processUnresolvedTypes(Schema schema) {
        // create any types that are still unresolved. e.g. an Order that contains OrderLine objects
        // also ensure if the unresolved type contains another unresolved type then we process it
        while (setUnresolvedTypes.size() > 0) {
            String returnType = setUnresolvedTypes.iterator().next();

            setUnresolvedTypes.remove(returnType);
            try {
                String simpleName = getSimpleName(returnType, true);

                SchemaScalar scalar = getScalar(returnType);
                if (scalar != null) {
                    if (!schema.containsScalarWithName(scalar.name())) {
                        schema.addScalar(scalar);
                    }
                    // update the return type with the scalar
                    updateLongTypes(schema, returnType, scalar.name());
                } else if (isEnumClass(returnType)) {
                    SchemaEnum newEnum = generateEnum(Class.forName(returnType));
                    if (!schema.containsEnumWithName(simpleName)) {
                        schema.addEnum(newEnum);
                    }
                    updateLongTypes(schema, returnType, newEnum.name());
                } else {
                    // we will either know this type already or need to add it
                    boolean fExists = schema.getTypes().stream()
                            .filter(t -> t.name().equals(simpleName)).count() > 0;
                    if (!fExists) {
                        SchemaType newType = generateType(returnType, false);

                        // update any return types to the discovered scalars
                        checkScalars(schema, newType);
                        schema.addType(newType);
                    }
                    // need to update any FieldDefinitions that contained the original "long" type of c
                    updateLongTypes(schema, returnType, simpleName);
                }
            } catch (Exception e) {
                ensureConfigurationException(LOGGER, "Cannot get GraphQL type for " + returnType, e);
            }
        }
    }

    /**
     * Generate a {@link SchemaType} from a given class.
     *
     * @param realReturnType the class to generate type from
     * @param isInputType    indicates if the type is an input type and if not the Input annotation will be ignored
     * @return a {@link SchemaType}
     * @throws IntrospectionException if any errors with introspection
     * @throws ClassNotFoundException if any classes are not found
     */
    private SchemaType generateType(String realReturnType, boolean isInputType)
            throws IntrospectionException, ClassNotFoundException {

        // if isInputType=false then we ignore the name annotation in case
        // an annotated input type was also used as a return type
        String simpleName = getSimpleName(realReturnType, !isInputType);
        SchemaType type = SchemaType.builder().name(simpleName).valueClassName(realReturnType).build();
        type.description(getDescription(Class.forName(realReturnType).getAnnotation(Description.class)));

        for (Map.Entry entry
                : retrieveGetterBeanMethods(Class.forName(realReturnType), isInputType).entrySet()) {
            DiscoveredMethod discoveredMethod = entry.getValue();
            String valueTypeName = discoveredMethod.returnType();
            SchemaFieldDefinition fd = newFieldDefinition(discoveredMethod, null);
            type.addFieldDefinition(fd);

            if (!ID.equals(valueTypeName) && valueTypeName.equals(fd.returnType())) {
                // value class was unchanged meaning we need to resolve
                setUnresolvedTypes.add(valueTypeName);
            }
        }
        return type;
    }

    /**
     * Process a class with a {@link GraphQLApi} annotation.
     *
     * @param rootQueryType    the root query type
     * @param rootMutationType the root mutation type
     * @param clazz            {@link Class} to introspect
     * @throws IntrospectionException
     */
    @SuppressWarnings("rawtypes")
    private void processGraphQLApiAnnotations(SchemaType rootQueryType,
                                              SchemaType rootMutationType,
                                              Schema schema,
                                              Class clazz)
            throws IntrospectionException, ClassNotFoundException {

        for (Map.Entry entry : retrieveAllAnnotatedBeanMethods(clazz).entrySet()) {
            DiscoveredMethod discoveredMethod = entry.getValue();
            Method method = discoveredMethod.method();
            SchemaFieldDefinition fd = null;

            // only include discovered methods in the original type where either the source is null
            // or the source is not null and it has a query annotation
            String source = discoveredMethod.source();
            if (source == null || discoveredMethod.isQueryAnnotated()) {
                fd = newFieldDefinition(discoveredMethod, getMethodName(method));
            }

            // if the source was not null, save it for later processing on the correct type
            if (source != null) {
                String additionReturnType = getGraphQLType(discoveredMethod.returnType());
                setAdditionalMethods.add(discoveredMethod);
                // add the unresolved type for the source
                if (!isGraphQLType(additionReturnType)) {
                    setUnresolvedTypes.add(additionReturnType);
                }
            }

            SchemaType schemaType = discoveredMethod.methodType() == QUERY_TYPE
                    ? rootQueryType
                    : rootMutationType;

            // add all the arguments and check to see if they contain types that are not yet known
            // this check is done no matter if the field definition is going to be created or not
            for (SchemaArgument a : discoveredMethod.arguments()) {
                String originalTypeName = a.argumentType();
                String typeName = getGraphQLType(originalTypeName);

                a.argumentType(typeName);
                String returnType = a.argumentType();

                if (originalTypeName.equals(returnType) && !ID.equals(returnType) && !a.isDataFetchingEnvironment()) {
                    // type name has not changed, so this must be either a Scalar, Enum or a Type
                    // Note: Interfaces are not currently supported as InputTypes in 1.0 of the Specification
                    // if is Scalar or enum then add to unresolved types and they will be dealt with
                    if (getScalar(returnType) != null || isEnumClass(returnType)) {
                        setUnresolvedTypes.add(returnType);
                    } else {
                        // create the input Type here
                        SchemaInputType inputType = generateType(returnType, true).createInputType("");
                        // if the name of the InputType was not changed then append "Input"
                        if (inputType.name().equals(Class.forName(returnType).getSimpleName())) {
                            inputType.name(inputType.name() + "Input");
                        }

                        if (!schema.containsInputTypeWithName(inputType.name())) {
                            schema.addInputType(inputType);
                            checkInputType(schema, inputType);
                        }
                        a.argumentType(inputType.name());
                    }
                }
                if (fd != null) {
                    fd.addArgument(a);
                }
            }

            if (fd != null) {
                DataFetcher dataFetcher = null;
                String[] format = discoveredMethod.format();
                SchemaScalar dateScalar = getScalar(discoveredMethod.returnType());

                // if the type is a Date/Time/DateTime scalar and there is currently no format,
                // then use the default format if there is one
                if ((dateScalar != null && isDateTimeScalar(dateScalar.name()) && isFormatEmpty(format))) {
                    Class originalType = fd.isArrayReturnType() ? fd.originalArrayType() : fd.originalType();
                    String[] newFormat = ensureFormat(dateScalar.name(),
                                                      originalType.getName(), new String[2]);
                    if (newFormat.length == 2) {
                        format = new String[] {DATE, newFormat[0], newFormat[1]};
                    }
                }
                if (!isFormatEmpty(format)) {
                    // a format exists on the method return type so format it after returning the value
                    final String graphQLType = getGraphQLType(fd.returnType());
                    final DataFetcher methodDataFetcher = DataFetcherUtils.newMethodDataFetcher(schema, clazz, method, null,
                                                                                                fd.arguments().toArray(
                                                                                                        new SchemaArgument[0]));
                    final String[] newFormat = new String[] {format[0], format[1], format[2]};

                    if (dateScalar != null && isDateTimeScalar(dateScalar.name())) {
                        dataFetcher = DataFetcherFactories.wrapDataFetcher(methodDataFetcher,
                                                                           (e, v) -> {
                                                                               DateTimeFormatter dateTimeFormatter =
                                                                                       getCorrectDateFormatter(
                                                                                               graphQLType, newFormat[2],
                                                                                               newFormat[1]);
                                                                               return dateTimeFormatter == null
                                                                                       ? formatDate(v, new SimpleDateFormat(newFormat[1]))
                                                                                       : formatDate(v, dateTimeFormatter);
                                                                           });
                    } else {
                        dataFetcher = DataFetcherFactories.wrapDataFetcher(methodDataFetcher,
                                                                           (e, v) -> {
                                                                               NumberFormat numberFormat = getCorrectNumberFormat(
                                                                                       graphQLType, newFormat[2], newFormat[1]);
                                                                               boolean isScalar = SchemaGeneratorHelper.getScalar(
                                                                                       discoveredMethod.returnType()) != null;
                                                                               return formatNumber(v, isScalar, numberFormat);
                                                                           });
                        fd.returnType(STRING);
                    }
                } else {
                    // no formatting, just call the method
                    dataFetcher = DataFetcherUtils.newMethodDataFetcher(schema, clazz, method, null,
                                                                        fd.arguments().toArray(new SchemaArgument[0]));
                }
                fd.dataFetcher(dataFetcher);
                fd.description(discoveredMethod.description());

                schemaType.addFieldDefinition(fd);

                checkScalars(schema, schemaType);

                String returnType = discoveredMethod.returnType();
                // check to see if this is a known type
                if (returnType.equals(fd.returnType()) && !setUnresolvedTypes.contains(returnType)
                        && !ID.equals(returnType)) {
                    // value class was unchanged meaning we need to resolve
                    setUnresolvedTypes.add(returnType);
                }
            }
        }
    }

    /**
     * Return true if the format is empty or undefined.
     *
     * @param format format to check
     * @return true if the format is empty or undefined
     */
    protected static boolean isFormatEmpty(String[] format) {
        if (format == null || format.length == 0) {
            return true;
        }
        // now check that each value is not null
        for (String entry : format) {
            if (entry == null) {
                return true;
            }
        }
        return false;
    }

    /**
     * Check this new {@link SchemaInputType} contains any types, then they must also have InputTypes created for them if they are
     * not enums or scalars.
     *
     * @param schema          {@link Schema} to add to
     * @param schemaInputType {@link SchemaInputType} to check
     * @throws IntrospectionException if issues with introspection
     * @throws ClassNotFoundException if class not found
     */
    private void checkInputType(Schema schema, SchemaInputType schemaInputType)
            throws IntrospectionException, ClassNotFoundException {
        // if this new Type contains any types, then they must also have
        // InputTypes created for them if they are not enums or scalars
        Set setInputTypes = new HashSet<>();
        setInputTypes.add(schemaInputType);

        while (setInputTypes.size() > 0) {
            SchemaInputType type = setInputTypes.iterator().next();
            setInputTypes.remove(type);

            // check each field definition to see if any return types are unknownInputTypes
            for (SchemaFieldDefinition fdi : type.fieldDefinitions()) {
                String fdReturnType = fdi.returnType();

                if (!isGraphQLType(fdReturnType)) {
                    // must be either an unknown input type, Scalar or Enum
                    if (getScalar(fdReturnType) != null || isEnumClass(fdReturnType)) {
                        setUnresolvedTypes.add(fdReturnType);
                    } else {
                        // must be a type, create a new input Type but do not add it to
                        // the schema if it already exists
                        SchemaInputType newInputType = generateType(fdReturnType, true).createInputType("");

                        // if the name of the InputType was not changed then append "Input"
                        if (newInputType.name().equals(Class.forName(newInputType.valueClassName()).getSimpleName())) {
                            newInputType.name(newInputType.name() + "Input");
                        }

                        if (!schema.containsInputTypeWithName(newInputType.name())) {
                            schema.addInputType(newInputType);
                            setInputTypes.add(newInputType);
                        }
                        fdi.returnType(newInputType.name());
                    }
                }
            }
        }
    }

    /**
     * Add the given {@link SchemaType} to the {@link Schema}.
     *
     * @param schema the {@link Schema} to add to
     * @throws IntrospectionException if issues with introspection
     * @throws ClassNotFoundException if class not found
     */
    private void addTypeToSchema(Schema schema, SchemaType type)
            throws IntrospectionException, ClassNotFoundException {

        String valueClassName = type.valueClassName();
        retrieveGetterBeanMethods(Class.forName(valueClassName), false).forEach((k, v) -> {
            SchemaFieldDefinition fd = newFieldDefinition(v, null);
            type.addFieldDefinition(fd);

            checkScalars(schema, type);

            String returnType = v.returnType();
            // check to see if this is a known type
            if (!ID.equals(returnType) && returnType.equals(fd.returnType()) && !setUnresolvedTypes.contains(returnType)) {
                // value class was unchanged meaning we need to resolve
                setUnresolvedTypes.add(returnType);
            }
        });

        // check if this Type is an interface then obtain all concrete classes that implement the type
        // and add them to the set of unresolved types
        if (type.isInterface()) {
            Collection> setConcreteClasses = jandexUtils.getKnownImplementors(valueClassName);
            setConcreteClasses.forEach(c -> setUnresolvedTypes.add(c.getName()));
        }

        schema.addType(type);
    }

    /**
     * Generate an {@link SchemaEnum} from a given  {@link java.lang.Enum}.
     *
     * @param clazz the {@link java.lang.Enum} to introspect
     * @return a new {@link SchemaEnum} or null if the class provided is not an {@link java.lang.Enum}
     */
    private SchemaEnum generateEnum(Class clazz) {
        if (clazz.isEnum()) {
            SchemaEnum newSchemaEnum = SchemaEnum.builder().name(getTypeName(clazz)).build();

            Arrays.stream(clazz.getEnumConstants())
                    .map(Object::toString)
                    .forEach(newSchemaEnum::addValue);
            return newSchemaEnum;
        }
        return null;
    }

    /**
     * Return a new {@link SchemaFieldDefinition} with the given field and class.
     *
     * @param discoveredMethod the {@link DiscoveredMethod}
     * @param optionalName     optional name for the field definition
     * @return a {@link SchemaFieldDefinition}
     */
    @SuppressWarnings("rawtypes")
    private SchemaFieldDefinition newFieldDefinition(DiscoveredMethod discoveredMethod,
                                                     String optionalName) {
        String valueClassName = discoveredMethod.returnType();
        String graphQLType = getGraphQLType(valueClassName);
        DataFetcher dataFetcher = null;
        String propertyName = discoveredMethod.propertyName();
        String name = discoveredMethod.name();

        boolean isArrayReturnType = discoveredMethod.isArrayReturnType() || discoveredMethod.isCollectionType()
                || discoveredMethod.isMap();

        if (isArrayReturnType) {
            if (discoveredMethod.isMap()) {
                // add DataFetcher that will just retrieve the values() from the Map.
                // The microprofile-graphql spec does not specify how Maps are treated
                // and leaves this up to the individual implementation. This implementation
                // supports returning the values of the map only and does not support using
                // Maps or types with Maps as return values from a query or mutation
                dataFetcher = DataFetcherUtils.newMapValuesDataFetcher(propertyName);
            }
        }

        // check for format on the property
        // note: currently the format will be an array of [3] as defined by FormattingHelper.getFormattingAnnotation
        String[] format = discoveredMethod.format();
        if (propertyName != null && !isFormatEmpty(format)) {
            if (!isGraphQLType(valueClassName)) {
                dataFetcher = retrieveFormattingDataFetcher(format, propertyName, graphQLType);

                // if the format is a number format then set the return type to String
                if (NUMBER.equals(format[0])) {
                    graphQLType = STRING;
                }
            }
        } else {
            // Add a PropertyDataFetcher if the name has been changed via annotation
            if (propertyName != null && !propertyName.equals(name)) {
                dataFetcher = new PropertyDataFetcher(propertyName);
            }
        }

        SchemaFieldDefinition fd = SchemaFieldDefinition.builder()
                .name(optionalName != null
                              ? optionalName
                              : discoveredMethod.name())
                .returnType(graphQLType)
                .arrayReturnType(isArrayReturnType)
                .returnTypeMandatory(discoveredMethod.isReturnTypeMandatory())
                .arrayLevels(discoveredMethod.arrayLevels())
                .dataFetcher(dataFetcher)
                .originalType(discoveredMethod.method().getReturnType())
                .arrayReturnTypeMandatory(discoveredMethod.isArrayReturnTypeMandatory())
                .originalArrayType(isArrayReturnType ? discoveredMethod.originalArrayType() : null)
                .build();

        if (format != null && format.length == 3) {
            fd.format(new String[] {format[1], format[2]});
        }

        fd.description(discoveredMethod.description());
        fd.jsonbFormat(discoveredMethod.isJsonbFormat());
        fd.defaultValue(discoveredMethod.defaultValue());
        fd.jsonbProperty(discoveredMethod.isJsonbProperty());
        return fd;
    }

    /**
     * Return the correct formatting {@link DataFetcher} to format the date or number field.
     *
     * @param rawFormat    raw format with [0] = type, [1] = locale, [2] = format
     * @param propertyName property to fetch from
     * @param type         the type of the data - from Class.getName()
     * @return the correct {@link DataFetcher}
     */
    private DataFetcher retrieveFormattingDataFetcher(String[] rawFormat, String propertyName, String type) {
        return NUMBER.equals(rawFormat[0])
                ? new DataFetcherUtils.NumberFormattingDataFetcher(propertyName, type, rawFormat[1], rawFormat[2])
                : new DataFetcherUtils.DateFormattingDataFetcher(propertyName, type, rawFormat[1], rawFormat[2]);
    }

    /**
     * Look in the given {@link Schema} for any field definitions, arguments and key value classes that contain the return type of
     * the long return type and replace with short return type.
     *
     * @param schema          schema to introspect
     * @param longReturnType  long return type
     * @param shortReturnType short return type
     */
    @SuppressWarnings("unchecked")
    private void updateLongTypes(Schema schema, String longReturnType, String shortReturnType) {
        // concatenate both the SchemaType and SchemaInputType
        Stream streamInputTypes = schema.getInputTypes().stream().map(it -> (SchemaType) it);
        Stream streamAll = Stream.concat(streamInputTypes, schema.getTypes().stream());
        streamAll.forEach(t -> {
            t.fieldDefinitions().forEach(fd -> {
                if (fd.returnType().equals(longReturnType)) {
                    fd.returnType(shortReturnType);
                }

                // check arguments
                fd.arguments().forEach(a -> {
                    if (a.argumentType().equals(longReturnType)) {
                        a.argumentType(shortReturnType);
                    }
                });
            });
        });

        // look through set of additional methods added for Source annotations
        setAdditionalMethods.forEach(m -> {
            m.arguments().forEach(a -> {
                if (a.argumentType().equals(longReturnType)) {
                    a.argumentType(shortReturnType);
                }
            });
        });
    }

    /**
     * Return a {@link Map} of all the discovered methods which have the {@link Query} or {@link Mutation} annotations.
     *
     * @param clazz Class to introspect
     * @return a {@link Map} of the methods and return types
     *
     * @throws IntrospectionException if any errors with introspection
     * @throws ClassNotFoundException if any classes are not found
     */
    protected Map retrieveAllAnnotatedBeanMethods(Class clazz)
            throws IntrospectionException, ClassNotFoundException {
        Map mapDiscoveredMethods = new HashMap<>();
        for (Method m : getAllMethods(clazz)) {
            boolean isQuery = m.getAnnotation(Query.class) != null;
            boolean isMutation = m.getAnnotation(Mutation.class) != null;
            boolean hasSourceAnnotation = Arrays.stream(m.getParameters()).anyMatch(p -> p.getAnnotation(Source.class) != null);
            if (isMutation && isQuery) {
                ensureConfigurationException(LOGGER, "The class " + clazz.getName()
                        + " may not have both a Query and Mutation annotation");
            }
            if (isQuery || isMutation || hasSourceAnnotation) {
                DiscoveredMethod discoveredMethod = generateDiscoveredMethod(m, clazz, null, false, true);
                discoveredMethod.methodType(isQuery || hasSourceAnnotation ? QUERY_TYPE : MUTATION_TYPE);
                String name = discoveredMethod.name();
                if (mapDiscoveredMethods.containsKey(name)) {
                    ensureConfigurationException(LOGGER, "A method named " + name + " already exists on "
                            + "the " + (isMutation ? "mutation" : "query")
                            + " " + discoveredMethod.method().getName());
                }
                mapDiscoveredMethods.put(name, discoveredMethod);
            }
        }
        return mapDiscoveredMethods;
    }

    /**
     * Retrieve only the getter methods for the {@link Class}.
     *
     * @param clazz       the {@link Class} to introspect
     * @param isInputType indicates if this type is an input type
     * @return a {@link Map} of the methods and return types
     * @throws IntrospectionException if there were errors introspecting classes
     * @throws ClassNotFoundException if any class is not found
     */
    protected Map retrieveGetterBeanMethods(Class clazz,
                                                                      boolean isInputType)
            throws IntrospectionException, ClassNotFoundException {
        Map mapDiscoveredMethods = new HashMap<>();

        for (Method m : getAllMethods(clazz)) {
            if (m.getName().equals("getClass") || shouldIgnoreMethod(m, isInputType)) {
                continue;
            }

            Optional optionalPdReadMethod = Arrays
                    .stream(Introspector.getBeanInfo(clazz).getPropertyDescriptors())
                    .filter(p -> p.getReadMethod() != null && p.getReadMethod().getName().equals(m.getName())).findFirst();

            if (optionalPdReadMethod.isPresent()) {
                PropertyDescriptor propertyDescriptor = optionalPdReadMethod.get();
                Method writeMethod = propertyDescriptor.getWriteMethod();
                boolean ignoreWriteMethod = isInputType && writeMethod != null && shouldIgnoreMethod(writeMethod, true);

                // only include if the field should not be ignored
                if (!shouldIgnoreField(clazz, propertyDescriptor.getName()) && !ignoreWriteMethod) {
                    // this is a getter method, include it here
                    DiscoveredMethod discoveredMethod =
                            generateDiscoveredMethod(m, clazz, propertyDescriptor, isInputType, false);
                    mapDiscoveredMethods.put(discoveredMethod.name(), discoveredMethod);
                }
            }
        }
        return mapDiscoveredMethods;
    }

    /**
     * Return all {@link Method}s for a given {@link Class}.
     *
     * @param clazz the {@link Class} to introspect
     * @return all {@link Method}s for a given {@link Class}
     *
     * @throws IntrospectionException if any errors with introspection
     */
    protected List getAllMethods(Class clazz) throws IntrospectionException {
        return Arrays.asList(Introspector.getBeanInfo(clazz).getMethodDescriptors())
                .stream()
                .map(MethodDescriptor::getMethod)
                .collect(Collectors.toList());
    }

    /**
     * Generate a {@link DiscoveredMethod} from the given arguments.
     *
     * @param method            {@link Method} being introspected
     * @param clazz             {@link Class} being introspected
     * @param pd                {@link PropertyDescriptor} for the property being introspected (may be null if retrieving all
     *                          methods as in the case for a {@link Query} annotation)
     * @param isInputType       indicates if the method is part of an input type
     * @param isQueryOrMutation indicates if this is for a query or mutation
     * @return a {@link DiscoveredMethod}
     * @throws ClassNotFoundException if any class is not found
     */
    private DiscoveredMethod generateDiscoveredMethod(Method method, Class clazz,
                                                      PropertyDescriptor pd, boolean isInputType,
                                                      boolean isQueryOrMutation) throws ClassNotFoundException {
        String[] format = new String[0];
        String description = null;
        boolean isReturnTypeMandatory = false;
        boolean isArrayReturnTypeMandatory = false;
        boolean isJsonbFormat = false;
        boolean isJsonbProperty;
        String defaultValue = null;
        String varName = stripMethodName(method, !isQueryOrMutation);

        String annotatedName = getMethodName(isInputType ? pd.getWriteMethod() : method);
        if (annotatedName != null) {
            varName = annotatedName;
        } else if (pd != null) {   // check the field only if this is a getter
            annotatedName = getFieldName(clazz, pd.getName());
            if (annotatedName != null) {
                varName = annotatedName;
            }
        }

        Method methodToCheck = isInputType ? pd.getWriteMethod() : method;
        isJsonbProperty = methodToCheck != null && methodToCheck.getAnnotation(JsonbProperty.class) != null;

        ensureValidName(LOGGER, varName);

        Class returnClazz = method.getReturnType();
        String returnClazzName = returnClazz.getName();
        ensureNonVoidQueryOrMutation(returnClazzName, method, clazz);

        if (pd != null) {
            boolean fieldHasIdAnnotation = false;
            Field field = null;

            try {
                field = clazz.getDeclaredField(pd.getName());
                fieldHasIdAnnotation = field != null && field.getAnnotation(Id.class) != null;
                description = getDescription(field.getAnnotation(Description.class));
                defaultValue = isInputType ? getDefaultValueAnnotationValue(field) : null; // only make sense for input types
                NonNull nonNullAnnotation = field.getAnnotation(NonNull.class);
                isArrayReturnTypeMandatory = getAnnotationValue(getFieldAnnotations(field, 0), NonNull.class) != null;

                if (isInputType) {
                    Method writeMethod = pd.getWriteMethod();
                    if (writeMethod != null) { // retrieve the setter method and check the description
                        String methodDescription = getDescription(writeMethod.getAnnotation(Description.class));
                        if (methodDescription != null) {
                            description = methodDescription;
                        }
                        String writeMethodDefaultValue = getDefaultValueAnnotationValue(writeMethod);
                        if (writeMethodDefaultValue != null) {
                            defaultValue = writeMethodDefaultValue;
                        }

                        NonNull methodAnnotation = writeMethod.getAnnotation(NonNull.class); // for an input type the
                        if (methodAnnotation != null) {                                      // method annotation will override
                            nonNullAnnotation = methodAnnotation;
                        }

                        // the annotation on the set method parameter will override for the input type if it's present
                        boolean isSetArrayMandatory =
                                getAnnotationValue(getParameterAnnotations(writeMethod.getParameters()[0], 0),
                                                   NonNull.class) != null;
                        if (isSetArrayMandatory && !isArrayReturnTypeMandatory) {
                            isArrayReturnTypeMandatory = true;
                        }

                        // if the set method has a format then this should overwrite any formatting as this is an InputType
                        String[] writeMethodFormat = getFormattingAnnotation(writeMethod);
                        if (!isFormatEmpty(writeMethodFormat)) {
                            format = writeMethodFormat;
                            isJsonbFormat = isJsonbAnnotationPresent(writeMethod);
                            isJsonbProperty = writeMethod.getAnnotation(JsonbProperty.class) != null;
                        }

                        Parameter[] parameters = writeMethod.getParameters();
                        if (parameters.length == 1) {
                            String[] argumentTypeFormat = FormattingHelper.getMethodParameterFormat(parameters[0], 0);
                            if (!isFormatEmpty(argumentTypeFormat)) {
                                format = argumentTypeFormat;
                                isJsonbFormat = isJsonbAnnotationPresent(parameters[0]);
                                isJsonbProperty = parameters[0].getAnnotation(JsonbProperty.class) != null;
                            }
                        }
                    }
                } else {
                    NonNull methodAnnotation = method.getAnnotation(NonNull.class);
                    if (methodAnnotation != null) {
                        nonNullAnnotation = methodAnnotation;
                    }
                    if (!isArrayReturnTypeMandatory) {
                        isArrayReturnTypeMandatory =
                                getAnnotationValue(getMethodAnnotations(method, 0), NonNull.class) != null;
                    }
                }

                isReturnTypeMandatory = (isPrimitive(returnClazzName) && defaultValue == null)
                        || nonNullAnnotation != null && defaultValue == null;

            } catch (NoSuchFieldException ignored) {
                LOGGER.fine("No such field " + pd.getName() + " on class " + clazz.getName());
            }

            if (fieldHasIdAnnotation || method.getAnnotation(Id.class) != null) {
                validateIDClass(returnClazz);
                returnClazzName = ID;
            }

            if (field != null && isFormatEmpty(format)) {   // check for format on the property
                format = getFormattingAnnotation(field);    // but only override if it is null
                if (isFormatEmpty(format)) { // check format of the inner most class. E.g. List<@DateFormat("DD/MM") String>
                    format = FormattingHelper.getFieldFormat(field, 0);
                }
                isJsonbFormat = isJsonbAnnotationPresent(field);
            }
        } else {  // pd is null which means this is for query or mutation
            defaultValue = getDefaultValueAnnotationValue(method);
            isReturnTypeMandatory = isPrimitive(returnClazzName) && defaultValue == null
                    || method.getAnnotation(NonNull.class) != null && defaultValue == null;
            if (method.getAnnotation(Id.class) != null) {
                validateIDClass(returnClazz);
                returnClazzName = ID;
            }
        }

        // check for method return type number format
        String[] methodFormat = getFormattingAnnotation(method);
        if (methodFormat[0] != null && !isInputType) {
            format = methodFormat;
        }

        DiscoveredMethod discoveredMethod = DiscoveredMethod.builder().name(varName).method(method).format(format)
                .defaultValue(defaultValue).jsonbFormat(isJsonbFormat).jsonbProperty(isJsonbProperty)
                .propertyName(pd != null ? pd.getName() : null).build();

        if (description == null && !isInputType) {
            description = getDescription(method.getAnnotation(Description.class));
        }

        processMethodParameters(method, discoveredMethod, annotatedName);
        ReturnType realReturnType = getReturnType(returnClazz, method.getGenericReturnType(), -1, method);
        processReturnType(discoveredMethod, realReturnType, returnClazzName, isInputType, varName, method);

        discoveredMethod.returnTypeMandatory(isReturnTypeMandatory);
        discoveredMethod.arrayReturnTypeMandatory(isArrayReturnTypeMandatory
                                                          || realReturnType.isReturnTypeMandatory && !isInputType);
        discoveredMethod.description(description);

        return discoveredMethod;
    }

    /**
     * Ensure that the query or mutation does not return void.
     * @param returnClazzName  return class name
     * @param method  {@link Method} being processed
     * @param clazz   {@link Class} being processed
     */
    private void ensureNonVoidQueryOrMutation(String returnClazzName, Method method, Class clazz) {
        if ("void".equals(returnClazzName)) {
            ensureConfigurationException(LOGGER, "void is not a valid return type for a Query or Mutation method '"
                    + method.getName() + "' on class " + clazz.getName());
        }
    }

    /**
     * Process the {@link ReturnType} and update {@link DiscoveredMethod} as required.
     * @param discoveredMethod  {@link DiscoveredMethod}
     * @param realReturnType    {@link ReturnType} with details of the return types
     * @param returnClazzName   return class name
     * @param isInputType       indicates if the method is part of an input type
     * @param varName           name of the variable
     * @param method            {@link Method} being processed
     *
     * @throws ClassNotFoundException if any class is not found
     */
    private void processReturnType(DiscoveredMethod discoveredMethod, ReturnType realReturnType,
                                   String returnClazzName, boolean isInputType,
                                   String varName, Method method) throws ClassNotFoundException {
        if (realReturnType.returnClass() != null && !ID.equals(returnClazzName)) {
            discoveredMethod.arrayReturnType(realReturnType.isArrayType());
            discoveredMethod.collectionType(realReturnType.collectionType());
            discoveredMethod.map(realReturnType.isMap());
            SchemaScalar dateScalar = getScalar(realReturnType.returnClass());
            if (dateScalar != null && isDateTimeScalar(dateScalar.name())) {
                // only set the original array type if it's a date/time
                discoveredMethod.originalArrayType(Class.forName(realReturnType.returnClass));
            } else if (discoveredMethod.isArrayReturnType()) {
                Class originalArrayType = getSafeClass(realReturnType.returnClass);
                if (originalArrayType != null) {
                    discoveredMethod.originalArrayType(originalArrayType);
                }
            }
            discoveredMethod.returnType(realReturnType.returnClass());
            // only override if this is not an input type
            if (!isInputType && !isFormatEmpty(realReturnType.format())) {
                discoveredMethod.format(realReturnType.format);
            }
        } else {
            discoveredMethod.name(varName);
            discoveredMethod.returnType(returnClazzName);
            discoveredMethod.method(method);
        }

        discoveredMethod.arrayLevels(realReturnType.arrayLevels());
    }

    /**
     * Process parameters for the given method.
     *
     * @param method           {@link Method} to process
     * @param discoveredMethod {@link DiscoveredMethod} to update
     * @param annotatedName    annotated name or null
     */
    private void processMethodParameters(Method method, DiscoveredMethod discoveredMethod, String annotatedName) {
        Parameter[] parameters = method.getParameters();
        if (parameters != null && parameters.length > 0) {
            java.lang.reflect.Type[] genericParameterTypes = method.getGenericParameterTypes();
            int i = 0;
            for (Parameter parameter : parameters) {
                boolean isID = false;
                Name paramNameAnnotation = parameter.getAnnotation(Name.class);
                String parameterName = paramNameAnnotation != null
                        && !paramNameAnnotation.value().isBlank()
                        ? paramNameAnnotation.value()
                        : parameter.getName();

                Class paramType = parameter.getType();

                ReturnType returnType = getReturnType(paramType, genericParameterTypes[i], i, method);

                if (parameter.getAnnotation(Id.class) != null) {
                    validateIDClass(returnType.returnClass());
                    returnType.returnClass(ID);
                    isID = true;
                }

                String argumentDefaultValue = getDefaultValueAnnotationValue(parameter);

                boolean isMandatory =
                        (isPrimitive(paramType) && argumentDefaultValue == null)
                                || (parameter.getAnnotation(NonNull.class) != null && argumentDefaultValue == null);
                SchemaArgument argument = SchemaArgument.builder()
                        .argumentName(parameterName)
                        .argumentType(returnType.returnClass())
                        .mandatory(isMandatory)
                        .defaultValue(argumentDefaultValue)
                        .originalType(paramType)
                        .description(getDescription(parameter.getAnnotation(Description.class)))
                        .dataFetchingEnvironment(paramType.equals(DataFetchingEnvironment.class))
                        .build();

                String[] argumentFormat = getFormattingAnnotation(parameter);
                String[] argumentTypeFormat = FormattingHelper.getMethodParameterFormat(parameter, 0);

                // The argument type format overrides any argument format. E.g. NumberFormat should apply below
                // E.g.  public List getListAsString(@Name("arg1")
                //                                           @JsonbNumberFormat("ignore 00.0000000")
                //                                           List> values)
                argumentFormat = !isFormatEmpty(argumentTypeFormat) ? argumentTypeFormat : argumentFormat;

                if (argumentFormat[0] != null) {
                    argument.format(new String[] {argumentFormat[1], argumentFormat[2]});
                    argument.argumentType(String.class.getName());
                }

                Source sourceAnnotation = parameter.getAnnotation(Source.class);
                if (sourceAnnotation != null) {
                    // set the method name to the correct property name as it will currently be incorrect
                    discoveredMethod.name(annotatedName != null ? annotatedName : stripMethodName(method, false));
                    discoveredMethod.source(returnType.returnClass());
                    discoveredMethod.queryAnnotated(method.getAnnotation(Query.class) != null);
                    argument.sourceArgument(true);
                }

                if (!isID) {
                    SchemaScalar dateScalar = getScalar(returnType.returnClass());
                    if (dateScalar != null && isDateTimeScalar(dateScalar.name())) {
                        // only set the original array type if it's a date/time
                        discoveredMethod.originalArrayType(getSafeClass(returnType.returnClass));
                    }
                    argument.arrayReturnTypeMandatory(returnType.isReturnTypeMandatory);
                    argument.arrayReturnType(returnType.isArrayType);
                    if (returnType.isArrayType) {
                        argument.originalArrayType(getSafeClass(returnType.returnClass));
                    }
                    argument.arrayLevels(returnType.arrayLevels());
                }

                discoveredMethod.addArgument(argument);
                i++;
            }
        }
    }

    /**
     * Return the {@link ReturnType} for this return class and method.
     *
     * @param returnClazz       return type
     * @param genericReturnType generic return {@link java.lang.reflect.Type} may be null
     * @param parameterNumber   the parameter number for the parameter
     * @param method            {@link Method} to find parameter for
     * @return a {@link ReturnType}
     */
    protected ReturnType getReturnType(Class returnClazz, java.lang.reflect.Type genericReturnType,
                                       int parameterNumber, Method method) {
        ReturnType actualReturnType = ReturnType.create();
        RootTypeResult rootTypeResult;
        String returnClazzName = returnClazz.getName();
        boolean isCollection = Collection.class.isAssignableFrom(returnClazz);
        boolean isMap = Map.class.isAssignableFrom(returnClazz);
        // deal with Collection or Map
        if (isCollection || isMap) {
            if (isCollection) {
                actualReturnType.collectionType(returnClazzName);
            }

            actualReturnType.map(isMap);
            // index is 0 for Collection and 1 for Map which assumes we are not
            // interested in the map K, just the map V which is what our implementation will do
            rootTypeResult = getRootTypeName(genericReturnType, isCollection ? 0 : 1, parameterNumber, method);
            String rootType = rootTypeResult.rootTypeName();

            // set the initial number of array levels to the levels of the root type
            int arrayLevels = rootTypeResult.levels();

            if (isArrayType(rootType)) {
                actualReturnType.returnClass(getRootArrayClass(rootType));
                arrayLevels += getArrayLevels(rootType);
            } else {
                actualReturnType.returnClass(rootType);
            }
            actualReturnType.arrayLevels(arrayLevels);
            actualReturnType.returnTypeMandatory(rootTypeResult.isArrayReturnTypeMandatory());
            actualReturnType.format(rootTypeResult.format);
            actualReturnType.arrayType(true);
        } else if (!returnClazzName.isEmpty() && returnClazzName.startsWith("[")) {
            // return type is array of either primitives or Objects/Interface/Enum.
            actualReturnType.arrayType(true);
            actualReturnType.arrayLevels(getArrayLevels(returnClazzName));
            actualReturnType.returnClass(getRootArrayClass(returnClazzName));
        } else {
            // primitive or type
            actualReturnType.returnClass(returnClazzName);
        }
        return actualReturnType;
    }

    /**
     * Return the inner most root type such as {@link String} for a List of List of String.
     *
     * @param genericReturnType the {@link java.lang.reflect.Type}
     * @param index             the index to use, either 0 for {@link Collection} or 1 for {@link Map}
     * @param parameterNumber   parameter number or -1 if parameter not being checked
     * @param method            {@link Method} being checked
     * @return the inner most root type
     */
    protected RootTypeResult getRootTypeName(java.lang.reflect.Type genericReturnType, int index,
                                             int parameterNumber, Method method) {
        int level = 1;
        boolean isParameter = parameterNumber != -1;
        String[] format = NO_FORMATTING;
        RootTypeResult.Builder builder = RootTypeResult.builder();

        boolean isReturnTypeMandatory;
        if (genericReturnType instanceof ParameterizedType) {
            ParameterizedType paramReturnType = (ParameterizedType) genericReturnType;
            // loop until we get the actual return type in the case we have List>
            java.lang.reflect.Type actualTypeArgument = paramReturnType.getActualTypeArguments()[index];
            while (actualTypeArgument instanceof ParameterizedType) {
                level++;
                ParameterizedType parameterizedType2 = (ParameterizedType) actualTypeArgument;
                actualTypeArgument = parameterizedType2.getActualTypeArguments()[index];
            }

            Class clazz = actualTypeArgument.getClass();
            boolean hasAnnotation = false;
            if (isParameter) {
                // check for the NonNull
                Parameter parameter = method.getParameters()[parameterNumber];
                hasAnnotation = getAnnotationValue(getParameterAnnotations(parameter, 0), NonNull.class) != null;
            } else {
                format = FormattingHelper.getMethodFormat(method, 0);
            }

            isReturnTypeMandatory = hasAnnotation || isPrimitive(clazz.getName());
            return builder.rootTypeName(((Class) actualTypeArgument).getName())
                    .levels(level)
                    .arrayReturnTypeMandatory(isReturnTypeMandatory)
                    .format(format)
                    .build();
        } else {
            Class clazz = genericReturnType.getClass();
            isReturnTypeMandatory = clazz.getAnnotation(NonNull.class) != null
                    || isPrimitive(clazz.getName());
            return builder.rootTypeName(((Class) genericReturnType).getName())
                    .levels(level)
                    .arrayReturnTypeMandatory(isReturnTypeMandatory)
                    .format(format)
                    .build();
        }
    }

    /**
     * Return the {@link JandexUtils} instance.
     *
     * @return the {@link JandexUtils} instance.
     */
    protected JandexUtils getJandexUtils() {
        return jandexUtils;
    }

    /**
     * A fluent API {@link io.helidon.common.Builder} to build instances of {@link SchemaGenerator}.
     */
    public static class Builder implements io.helidon.common.Builder {

        private final Set> collectedApis = new HashSet<>();

        /**
         * Build the instance from this builder.
         *
         * @return instance of the built type
         */
        @Override
        public SchemaGenerator build() {
            return new SchemaGenerator(this);
        }

        public Builder classes(Set> collectedApis) {
            this.collectedApis.addAll(collectedApis);
            return this;
        }
    }

    /**
     * Defines return types for methods or parameters.
     */
    public static class ReturnType {

        /**
         * Return class.
         */
        private String returnClass;

        /**
         * Indicates if this is an array type.
         */
        private boolean isArrayType = false;

        /**
         * Indicates if this is a {@link Map}.
         */
        private boolean isMap = false;

        /**
         * Return the type of collection.
         */
        private String collectionType;

        /**
         * Number of levels in the Array.
         */
        private int arrayLevels = 0;

        /**
         * Indicates id the return type is mandatory.
         */
        private boolean isReturnTypeMandatory;

        /**
         * The format of the return type if any.
         */
        private String[] format;

        /**
         * Default constructor.
         */
        private ReturnType() {
        }

        /**
         * Create a new {@link ReturnType}.
         * @return a new {@link ReturnType}
         */
        public static ReturnType create() {
            return new ReturnType();
        }

        /**
         * Return the return class.
         *
         * @return the return class
         */
        public String returnClass() {
            return returnClass;
        }

        /**
         * Sets the return class.
         *
         * @param returnClass the return class
         */
        public void returnClass(String returnClass) {
            this.returnClass = returnClass;
        }

        /**
         * Indicates if this is an array type.
         *
         * @return if this is an array type
         */
        public boolean isArrayType() {
            return isArrayType;
        }

        /**
         * Set if this is an array type.
         *
         * @param arrayType if this is an array type
         */
        public void arrayType(boolean arrayType) {
            isArrayType = arrayType;
        }

        /**
         * Indicates if this is a {@link Map}.
         *
         * @return if this is a {@link Map}
         */
        public boolean isMap() {
            return isMap;
        }

        /**
         * Set if this is a {@link Map}.
         *
         * @param map if this is a {@link Map}
         */
        public void map(boolean map) {
            isMap = map;
        }

        /**
         * Return the type of collection.
         *
         * @return the type of collection
         */
        public String collectionType() {
            return collectionType;
        }

        /**
         * Set the type of collection.
         *
         * @param collectionType the type of collection
         */
        public void collectionType(String collectionType) {
            this.collectionType = collectionType;
        }

        /**
         * Return the level of arrays or 0 if not an array.
         *
         * @return the level of arrays
         */
        public int arrayLevels() {
            return arrayLevels;
        }

        /**
         * Set the level of arrays or 0 if not an array.
         *
         * @param arrayLevels the level of arrays or 0 if not an array
         */
        public void arrayLevels(int arrayLevels) {
            this.arrayLevels = arrayLevels;
        }

        /**
         * Indicates if the return type is mandatory.
         *
         * @return if the return type is mandatory
         */
        public boolean isReturnTypeMandatory() {
            return isReturnTypeMandatory;
        }

        /**
         * Set if the return type is mandatory.
         *
         * @param returnTypeMandatory if the return type is mandatory
         */
        public void returnTypeMandatory(boolean returnTypeMandatory) {
            isReturnTypeMandatory = returnTypeMandatory;
        }

        /**
         * Return the format of the result class.
         *
         * @return the format of the result class
         */
        public String[] format() {
            if (format == null) {
                return null;
            }
            String[] copy = new String[format.length];
            System.arraycopy(format, 0, copy, 0, copy.length);
            return copy;
        }

        /**
         * Set the format of the result class.
         *
         * @param format the format of the result class
         */
        public void format(String[] format) {
            if (format == null) {
                this.format = null;
            } else {
                this.format = new String[format.length];
                System.arraycopy(format, 0, this.format, 0, this.format.length);
            }
        }
    }

    /**
     * Represents a result for the method getRootTypeName.
     */
    public static class RootTypeResult {

        /**
         * The root type of the {@link Collection} or {@link Map}.
         */
        private final String rootTypeName;

        /**
         * The number of levels in total.
         */
        private final int levels;

        /**
         * Indicates if the array return type is mandatory.
         */
        private boolean isArrayReturnTypeMandatory;

        /**
         * The format of the result class.
         */
        private final String[] format;

        /**
         * Construct a {@link RootTypeResult}.
         *
         * @param builder the {@link Builder} to construct from
         */
        private RootTypeResult(Builder builder) {
            this.rootTypeName = builder.rootTypeName;
            this.levels = builder.levels;
            this.isArrayReturnTypeMandatory = builder.isArrayReturnTypeMandatory;
            this.format = builder.format;
        }

        /**
         * Fluent API builder to create {@link RootTypeResult}.
         *
         * @return new builder instance
         */
        public static Builder builder() {
            return new Builder();
        }

        /**
         * Return the root type of the {@link Collection} or {@link Map}.
         *
         * @return root type of the {@link Collection} or {@link Map}
         */
        public String rootTypeName() {
            return rootTypeName;
        }

        /**
         * Return the number of levels in total.
         *
         * @return the number of levels in total
         */
        public int levels() {
            return levels;
        }

        /**
         * Indicates if the return type is mandatory.
         *
         * @return if the return type is mandatory
         */
        public boolean isArrayReturnTypeMandatory() {
            return isArrayReturnTypeMandatory;
        }

        /**
         * Return the format of the result class.
         *
         * @return the format of the result class
         */
        public String[] format() {
            if (format == null) {
                return null;
            }
            String[] copy = new String[format.length];
            System.arraycopy(format, 0, copy, 0, copy.length);
            return copy;
        }

        /**
         * A fluent API {@link io.helidon.common.Builder} to build instances of {@link DiscoveredMethod}.
         */
        public static class Builder implements io.helidon.common.Builder {

            private String rootTypeName;
            private int levels;
            private boolean isArrayReturnTypeMandatory;
            private String[] format;

            /**
             * Set the root type of the {@link Collection} or {@link Map}.
             *
             * @param rootTypeName root type of the {@link Collection} or {@link Map}
             * @return updated builder instance
             */
            public Builder rootTypeName(String rootTypeName) {
                this.rootTypeName = rootTypeName;
                return this;
            }

            /**
             * Set the number of array levels if return type is an array.
             *
             * @param levels the number of array levels if return type is an array
             * @return updated builder instance
             */
            public Builder levels(int levels) {
                this.levels = levels;
                return this;
            }

            /**
             * Set if the value of the array is mandatory.
             *
             * @param isArrayReturnTypeMandatory If the return type is an array then indicates if the value in the array is
             *                                   mandatory
             * @return updated builder instance
             */
            public Builder arrayReturnTypeMandatory(boolean isArrayReturnTypeMandatory) {
                this.isArrayReturnTypeMandatory = isArrayReturnTypeMandatory;
                return this;
            }

            /**
             * Set the format for a number or date.
             *
             * @param format the format for a number or date
             * @return updated builder instance
             */
            public Builder format(String[] format) {
                if (format == null) {
                    this.format = null;
                } else {
                    this.format = new String[format.length];
                    System.arraycopy(format, 0, this.format, 0, this.format.length);
                }

                return this;
            }

            /**
             * Build the instance from this builder.
             *
             * @return instance of the built type
             */
            @Override
            public RootTypeResult build() {
                return new RootTypeResult(this);
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy