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

io.smallrye.graphql.schema.creator.OperationCreator Maven / Gradle / Ivy

The newest version!
package io.smallrye.graphql.schema.creator;

import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.ParameterizedType;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;

import io.smallrye.graphql.schema.Annotations;
import io.smallrye.graphql.schema.Classes;
import io.smallrye.graphql.schema.SchemaBuilderException;
import io.smallrye.graphql.schema.helper.DeprecatedDirectivesHelper;
import io.smallrye.graphql.schema.helper.Direction;
import io.smallrye.graphql.schema.helper.MethodHelper;
import io.smallrye.graphql.schema.helper.RolesAllowedDirectivesHelper;
import io.smallrye.graphql.schema.model.Argument;
import io.smallrye.graphql.schema.model.Execute;
import io.smallrye.graphql.schema.model.Operation;
import io.smallrye.graphql.schema.model.OperationType;
import io.smallrye.graphql.schema.model.Reference;
import kotlinx.metadata.Flag;
import kotlinx.metadata.KmClassifier;
import kotlinx.metadata.KmFunction;
import kotlinx.metadata.KmType;
import kotlinx.metadata.KmTypeProjection;
import kotlinx.metadata.KmValueParameter;
import kotlinx.metadata.jvm.KotlinClassHeader;
import kotlinx.metadata.jvm.KotlinClassMetadata;

/**
 * Creates a Operation object
 *
 * @author Phillip Kruger ([email protected])
 */
public class OperationCreator extends ModelCreator {

    private final ArgumentCreator argumentCreator;

    private final RolesAllowedDirectivesHelper rolesAllowedHelper;
    private final DeprecatedDirectivesHelper deprecatedHelper;

    private final Logger logger = Logger.getLogger(OperationCreator.class.getName());

    public OperationCreator(ReferenceCreator referenceCreator, ArgumentCreator argumentCreator) {
        super(referenceCreator);
        this.argumentCreator = argumentCreator;
        this.rolesAllowedHelper = new RolesAllowedDirectivesHelper();
        this.deprecatedHelper = new DeprecatedDirectivesHelper();
    }

    /**
     * This creates a single operation.
     * It translate to one entry under a query / mutation in the schema or
     * one method in the Java class annotated with Query or Mutation
     *
     * @param methodInfo the java method
     * @param operationType the type of operation (Query / Mutation)
     * @param type
     * @return a Operation that defines this GraphQL Operation
     */
    public Operation createOperation(MethodInfo methodInfo, OperationType operationType,
            final io.smallrye.graphql.schema.model.Type type) {
        if (!Modifier.isPublic(methodInfo.flags())) {
            throw new IllegalArgumentException(
                    "Method " + methodInfo.declaringClass().name().toString() + "#" + methodInfo.name()
                            + " is used as an operation, but is not public");
        }

        Annotations annotationsForMethod = Annotations.getAnnotationsForMethod(methodInfo);
        Annotations annotationsForClass = Annotations.getAnnotationsForClass(methodInfo.declaringClass());

        Type fieldType = getReturnType(methodInfo);

        // Name
        String name = getOperationName(methodInfo, operationType, annotationsForMethod);

        // Field Type
        validateFieldType(methodInfo, operationType);

        // Execution
        Execute execute = getExecution(annotationsForMethod, annotationsForClass);

        Reference reference = referenceCreator.createReferenceForOperationField(fieldType, annotationsForMethod);
        Operation operation = new Operation(methodInfo.declaringClass().name().toString(),
                methodInfo.name(),
                MethodHelper.getPropertyName(Direction.OUT, methodInfo.name()),
                name,
                reference,
                operationType,
                execute);
        if (type != null) {
            operation.setSourceFieldOn(new Reference.Builder().reference(type).build());
        }

        // Arguments
        List parameters = methodInfo.parameterTypes();
        for (short i = 0; i < parameters.size(); i++) {
            Optional maybeArgument = argumentCreator.createArgument(operation, methodInfo, i);
            maybeArgument.ifPresent(operation::addArgument);
        }
        addDirectivesForRolesAllowed(annotationsForMethod, annotationsForClass, operation, reference);
        addDirectiveForDeprecated(annotationsForMethod, operation);
        populateField(Direction.OUT, operation, fieldType, annotationsForMethod);

        checkWrappedTypeKotlinNullability(methodInfo, annotationsForClass, operation);
        return operation;
    }

    // If the operation return type is a wrapper and is written in Kotlin,
    // this checks whether the wrapped type is nullable.
    // Nullability metadata is stored in the kotlin.Metadata annotation
    // on the class that contains the operation.
    private void checkWrappedTypeKotlinNullability(MethodInfo methodInfo,
            Annotations annotationsForClass,
            Operation operation) {
        Optional kotlinMetadataAnnotation = annotationsForClass
                .getOneOfTheseAnnotations(Annotations.KOTLIN_METADATA);
        if (kotlinMetadataAnnotation.isPresent()) {
            KotlinClassMetadata.Class kotlinClass = toKotlinClassMetadata(kotlinMetadataAnnotation.get());
            // We need to find the corresponding function inside
            // the KotlinClassMetadata to check its IS_NULLABLE flag.
            Optional function = kotlinClass.getKmClass().getFunctions()
                    .stream()
                    .filter(f -> f.getName().equals(methodInfo.name()))
                    .filter(f -> compareParameterLists(f.getValueParameters(), methodInfo.parameterTypes()))
                    .findAny();
            if (function.isPresent()) {
                // handle return type
                if (operation.hasWrapper()) {
                    KmType returnType = function.get().getReturnType();
                    var nullable = isKotlinWrappedTypeNullable(returnType);
                    if (operation.getWrapper().isCollectionOrArrayOrMap()) {
                        operation.getWrapper().setWrappedTypeNotNull(!nullable);
                        // workaround: consistent behavior to java: if wrapped type is non null, collection will be marked as non null
                        if (!nullable && !operation.isNotNull()) {
                            operation.setNotNull(true);
                        }
                    } else {
                        // Uni, etc
                        if (nullable) {
                            operation.setNotNull(false);
                        }
                    }
                }

                // handle arguments
                List arguments = operation.getArguments();
                List valueParameters = function.get().getValueParameters();
                if (arguments.size() == valueParameters.size()) {
                    for (int i = 0; i < arguments.size(); i++) {
                        Argument argument = arguments.get(i);
                        if (argument.hasWrapper() && argument.getWrapper().isCollectionOrArrayOrMap()) {
                            KmValueParameter typeParameter = valueParameters.get(i);
                            var paramNullable = isKotlinWrappedTypeNullable(typeParameter.getType());
                            argument.getWrapper().setWrappedTypeNotNull(!paramNullable);
                            // workaround:  consistent behavior to java: if wrapped type is not null, collection will be marked as not null
                            if (!paramNullable && !argument.isNotNull()) {
                                argument.setNotNull(true);
                            }
                        }
                    }
                }
            }
        }
    }

    private static boolean isKotlinWrappedTypeNullable(KmType kotlinType) {
        if (kotlinType.getArguments().isEmpty()) {
            return false;
        }
        KmTypeProjection arg = kotlinType.getArguments().get(0);
        int flags = arg.getType().getFlags();
        boolean nullable = Flag.Type.IS_NULLABLE.invoke(flags);
        return nullable;
    }

    private boolean compareParameterLists(List kotlinParameters,
            List jandexParameters) {
        if (kotlinParameters.size() != jandexParameters.size()) {
            return false;
        }
        for (int i = 0; i < kotlinParameters.size(); i++) {
            KmType kotlinType = kotlinParameters.get(i).getType();
            Type jandexType = jandexParameters.get(i);
            if (!compareJavaAndKotlinType(jandexType, kotlinType)) {
                return false;
            }
        }
        return true;
    }

    private boolean compareJavaAndKotlinType(Type javaType, kotlinx.metadata.KmType kotlinType) {
        if (kotlinType == null) {
            return false;
        }
        String kotlinTypeName = ((KmClassifier.Class) kotlinType.classifier).getName().replace("/", ".");

        boolean signatureIsEqual = false;
        if (kotlinTypeName.equals(javaType.name().toString())) {
            signatureIsEqual = true;
        } else if (Classes.isCollection(javaType)) {
            signatureIsEqual = compareKotlinCollectionType(javaType, kotlinType);
        }
        // TODO: the matching of parameter types could use some improvements
        // TODO: only some cases are handled, there are much more: see https://kotlinlang.org/docs/java-interop.html#mapped-types

        boolean parametersAreEqual = true;
        if (javaType instanceof ParameterizedType) {
            List arguments = javaType.asParameterizedType().arguments();
            for (int i = 0; i < arguments.size(); i++) {
                Type javaArg = arguments.get(i);
                KmType kotlinArg = kotlinType.getArguments().get(i).getType();

                boolean argTypeEqual = compareJavaAndKotlinType(javaArg, kotlinArg);
                if (!argTypeEqual) {
                    parametersAreEqual = false;
                    break;
                }
            }
        }

        return signatureIsEqual && parametersAreEqual;
    }

    private boolean compareKotlinCollectionType(Type javaType, KmType kotlinType) {
        String javaCollectionType = javaType.name().toString();
        String kotlinCollectionType = ((KmClassifier.Class) kotlinType.classifier).getName().replace("/", ".");
        // TODO: naive implementation.
        if (Collection.class.getName().equals(javaCollectionType)) {
            return kotlinCollectionType.equals("kotlin.collections.Collection") ||
                    kotlinCollectionType.equals("kotlin.collections.MutableCollection");
        }
        if (List.class.getName().equals(javaCollectionType)) {
            return kotlinCollectionType.equals("kotlin.collections.List") ||
                    kotlinCollectionType.equals("kotlin.collections.MutableList");
        }
        if (Set.class.getName().equals(javaCollectionType)) {
            return kotlinCollectionType.equals("kotlin.collections.Set") ||
                    kotlinCollectionType.equals("kotlin.collections.MutableSet");
        }
        if (Iterable.class.getName().equals(javaCollectionType)) {
            return kotlinCollectionType.equals("kotlin.collections.Iterable") ||
                    kotlinCollectionType.equals("kotlin.collections.MutableIterable");
        }
        return false;
    }

    private KotlinClassMetadata.Class toKotlinClassMetadata(AnnotationInstance metadata) {
        KotlinClassHeader classHeader = new KotlinClassHeader(
                metadata.value("k").asInt(),
                metadata.value("mv").asIntArray(),
                metadata.value("d1").asStringArray(),
                metadata.value("d2").asStringArray(),
                metadata.value("xs") != null ? metadata.value("xs").asString() : null,
                metadata.value("pn") != null ? metadata.value("pn").asString() : null,
                metadata.value("xi").asInt());
        return (KotlinClassMetadata.Class) KotlinClassMetadata.read(classHeader);
    }

    private static void validateFieldType(MethodInfo methodInfo, OperationType operationType) {
        Type returnType = methodInfo.returnType();
        if (!operationType.equals(OperationType.MUTATION) && returnType.kind().equals(Type.Kind.VOID)) {
            throw new SchemaBuilderException(
                    "Can not have a void return for [" + operationType.name()
                            + "] on method [" + methodInfo.name() + "]");
        }
    }

    /**
     * Get the name from annotation(s) or default.
     * This is for operations (query, mutation and source)
     *
     * @param methodInfo the java method
     * @param operationType the type (query, mutation)
     * @param annotations the annotations on this method
     * @return the operation name
     */
    private static String getOperationName(MethodInfo methodInfo, OperationType operationType, Annotations annotations) {
        DotName operationAnnotation = getOperationAnnotation(operationType);

        // If the @Query or @Mutation annotation has a value, use that, else use name or jsonb property
        return annotations.getOneOfTheseMethodAnnotationsValue(
                operationAnnotation,
                Annotations.NAME,
                Annotations.JAKARTA_JSONB_PROPERTY,
                Annotations.JAVAX_JSONB_PROPERTY,
                Annotations.JACKSON_PROPERTY)
                .orElse(getDefaultExecutionTypeName(methodInfo, operationType));

    }

    private static DotName getOperationAnnotation(OperationType operationType) {
        switch (operationType) {
            case QUERY:
                return Annotations.QUERY;
            case MUTATION:
                return Annotations.MUTATION;
            case SUBSCRIPTION:
                return Annotations.SUBCRIPTION;
            default:
                break;
        }
        return null;
    }

    private static String getDefaultExecutionTypeName(MethodInfo methodInfo, OperationType operationType) {
        String methodName = methodInfo.name();
        if (operationType.equals(OperationType.QUERY) || operationType.equals(OperationType.SUBSCRIPTION)) {
            methodName = MethodHelper.getPropertyName(Direction.OUT, methodName);
        } else if (operationType.equals(OperationType.MUTATION)) {
            methodName = MethodHelper.getPropertyName(Direction.IN, methodName);
        }
        return methodName;
    }

    private Execute getExecution(Annotations annotationsForMethod, Annotations annotationsForClass) {
        // first check annotation on method
        if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.BLOCKING)) {
            return Execute.BLOCKING;
        } else if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.NON_BLOCKING)) {
            return Execute.NON_BLOCKING;
        }

        // then check annotation on class
        if (annotationsForClass.containsOneOfTheseAnnotations(Annotations.BLOCKING)) {
            return Execute.BLOCKING;
        } else if (annotationsForClass.containsOneOfTheseAnnotations(Annotations.NON_BLOCKING)) {
            return Execute.NON_BLOCKING;
        }

        // lastly use default based on return type
        return Execute.DEFAULT;
    }

    private void addDirectivesForRolesAllowed(Annotations annotationsForOperation, Annotations classAnnotations,
            Operation operation,
            Reference parentObjectReference) {
        rolesAllowedHelper
                .transformRolesAllowedToDirectives(annotationsForOperation, classAnnotations)
                .ifPresent(rolesAllowedDirective -> {
                    logger.debug("Adding rolesAllowed directive " + rolesAllowedDirective + " to method '" + operation.getName()
                            + "'");
                    operation.addDirectiveInstance(rolesAllowedDirective);
                });
    }

    private void addDirectiveForDeprecated(Annotations annotationsForOperation, Operation operation) {
        if (deprecatedHelper != null && directives != null) {
            deprecatedHelper
                    .transformDeprecatedToDirective(annotationsForOperation,
                            directives.getDirectiveTypes().get(DotName.createSimple("io.smallrye.graphql.api.Deprecated")))
                    .ifPresent(deprecatedDirective -> {
                        logger.debug("Adding deprecated directive " + deprecatedDirective + " to method '" + operation.getName()
                                + "'");
                        operation.addDirectiveInstance(deprecatedDirective);
                    });
        }
    }

    @Override
    public String getDirectiveLocation() {
        return "FIELD_DEFINITION";
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy