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

graphql.annotations.GraphQLAnnotations Maven / Gradle / Ivy

There is a newer version: 3.0.3
Show newest version
/**
 * Copyright 2016 Yurii Rashkovskii
 *
 * 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
 */
package graphql.annotations;

import graphql.relay.Relay;
import graphql.schema.*;
import graphql.schema.GraphQLNonNull;
import lombok.SneakyThrows;

import javax.validation.constraints.NotNull;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static graphql.schema.GraphQLArgument.newArgument;
import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
import static graphql.schema.GraphQLInputObjectField.newInputObjectField;
import static graphql.schema.GraphQLInterfaceType.newInterface;
import static graphql.schema.GraphQLObjectType.newObject;
import static graphql.schema.GraphQLUnionType.newUnionType;

/**
 * A utility class for extracting GraphQL data structures from annotated
 * elements.
 */
public class GraphQLAnnotations {

    /**
     * Extract GraphQLInterfaceType from an interface
     * @param iface interface
     * @return
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws IllegalArgumentException if iface is not an interface or doesn't have @GraphTypeResolver annotation
     */
    public static graphql.schema.GraphQLType iface(Class iface) throws IllegalAccessException, InstantiationException {
        if (iface.getAnnotation(GraphQLUnion.class) != null) {
            return unionBuilder(iface).build();
        } else {
            return ifaceBuilder(iface).build();
        }
    }

    public static GraphQLUnionType.Builder unionBuilder(Class iface) throws InstantiationException, IllegalAccessException {
        if (!iface.isInterface()) {
            throw new IllegalArgumentException(iface + " is not an interface");
        }
        GraphQLUnionType.Builder builder = newUnionType();

        GraphQLUnion unionAnnotation = iface.getAnnotation(GraphQLUnion.class);
        GraphQLName name = iface.getAnnotation(GraphQLName.class);
        builder.name(name == null ? iface.getSimpleName() : name.value());
        GraphQLDescription description = iface.getAnnotation(GraphQLDescription.class);
        if (description != null) {
            builder.description(description.value());
        }
        GraphQLType typeAnnotation = iface.getAnnotation(GraphQLType.class);
        if (typeAnnotation == null) {
            typeAnnotation = new defaultGraphQLType();
        }
        TypeFunction typeFunction = typeAnnotation.value().newInstance();
        Arrays.asList(unionAnnotation.possibleTypes()).stream().map(new Function, graphql.schema.GraphQLType>() {
            @Override
            @SneakyThrows
            public graphql.schema.GraphQLType apply(Class aClass) {
                return typeFunction.apply(aClass, null);
            }
        }).forEach(builder::possibleType);
        builder.typeResolver(new UnionTypeResolver(unionAnnotation.possibleTypes()));
        return builder;
    }

    public static GraphQLInterfaceType.Builder ifaceBuilder(Class iface) throws InstantiationException, IllegalAccessException {
        if (!iface.isInterface()) {
            throw new IllegalArgumentException(iface + " is not an interface");
        }
        GraphQLInterfaceType.Builder builder = newInterface();

        GraphQLName name = iface.getAnnotation(GraphQLName.class);
        builder.name(name == null ? iface.getSimpleName() : name.value());
        GraphQLDescription description = iface.getAnnotation(GraphQLDescription.class);
        if (description != null) {
            builder.description(description.value());
        }
        for (Method method : iface.getMethods()) {
            boolean valid = !Modifier.isStatic(method.getModifiers()) &&
                    method.getAnnotation(GraphQLField.class) != null;
            if (valid) {
                builder.field(field(method));
            }
        }
        GraphQLTypeResolver typeResolver = iface.getAnnotation(GraphQLTypeResolver.class);
        if (typeResolver == null) {
            throw new IllegalArgumentException(iface + " should have @GraphQLTypeResolver annotation defined");
        }
        builder.typeResolver(typeResolver.value().newInstance());
        return builder;
    }

    private static Class getDeclaringClass(Method method) {
        Class object = method.getDeclaringClass();
        Class declaringClass = object;
        for (Class iface : object.getInterfaces()) {
            try {
                iface.getMethod(method.getName(), method.getParameterTypes());
                declaringClass = iface;
            } catch (NoSuchMethodException e) {
            }
        }

        try {
            if (object.getSuperclass() != null) {
                object.getSuperclass().getMethod(method.getName(), method.getParameterTypes());
                declaringClass = object.getSuperclass();
            }
        } catch (NoSuchMethodException e) {
        }
        return declaringClass;

    }

    /**
     * Extract GraphQLObjectType from a class
     * @param object
     * @return
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws NoSuchMethodException
     */
    public static GraphQLObjectType object(Class object) throws IllegalAccessException, InstantiationException, NoSuchMethodException {
        GraphQLObjectType.Builder builder = objectBuilder(object);

        return new GraphQLObjectTypeWrapper(builder.build());
    }

    public static class GraphQLObjectTypeWrapper extends GraphQLObjectType {

        public GraphQLObjectTypeWrapper(GraphQLObjectType objectType) {
            super(objectType.getName(), objectType.getDescription(), objectType.getFieldDefinitions(),
                    objectType.getInterfaces());
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof GraphQLObjectType &&
                   ((GraphQLObjectType) obj).getName().contentEquals(getName()) &&
                   ((GraphQLObjectType) obj).getFieldDefinitions().equals(getFieldDefinitions());
        }
    }

    public static class GraphQLFieldDefinitionWrapper extends GraphQLFieldDefinition {

        public GraphQLFieldDefinitionWrapper(GraphQLFieldDefinition fieldDefinition) {
            super(fieldDefinition.getName(), fieldDefinition.getDescription(), fieldDefinition.getType(),
                    fieldDefinition.getDataFetcher(),fieldDefinition.getArguments(), fieldDefinition.getDeprecationReason());
        }

        @Override
        public boolean equals(Object obj) {
            return obj instanceof GraphQLFieldDefinition &&
                    ((GraphQLFieldDefinition) obj).getName().contentEquals(getName());
        }
    }

    public static GraphQLObjectType.Builder objectBuilder(Class object) throws NoSuchMethodException, InstantiationException, IllegalAccessException {
        GraphQLObjectType.Builder builder = newObject();
        GraphQLName name = object.getAnnotation(GraphQLName.class);
        builder.name(name == null ? object.getSimpleName() : name.value());
        GraphQLDescription description = object.getAnnotation(GraphQLDescription.class);
        if (description != null) {
            builder.description(description.value());
        }
        for (Method method : object.getMethods()) {

            Class declaringClass = getDeclaringClass(method);

            boolean valid = (method.getAnnotation(GraphQLField.class) != null ||
                     declaringClass.getMethod(method.getName(), method.getParameterTypes()).getAnnotation(GraphQLField.class) != null);

            if (valid) {
                builder.field(field(method));
            }
        }
        for (Field field : object.getFields()) {
            boolean valid = !Modifier.isStatic(field.getModifiers()) &&
                    field.getAnnotation(GraphQLField.class) != null;
            if (valid) {
                builder.field(field(field));
            }
        }
        for (Class iface : object.getInterfaces()) {
            if (iface.getAnnotation(GraphQLTypeResolver.class) != null) {
                builder.withInterface((GraphQLInterfaceType) iface(iface));
            }
        }
        return builder;
    }


    protected static GraphQLFieldDefinition field(Field field) throws IllegalAccessException, InstantiationException {
        GraphQLFieldDefinition.Builder builder = newFieldDefinition();
        GraphQLName name = field.getAnnotation(GraphQLName.class);
        builder.name(name == null ? field.getName() : name.value());
        GraphQLType annotation = field.getAnnotation(GraphQLType.class);
        if (annotation == null) {
            annotation = new defaultGraphQLType();
        }

        TypeFunction typeFunction = annotation.value().newInstance();
        GraphQLOutputType type = (GraphQLOutputType) typeFunction.apply(field.getType(), field.getAnnotatedType());

        GraphQLOutputType outputType = field.getAnnotation(NotNull.class) == null ? type : new GraphQLNonNull(type);

        boolean isConnection = isConnection(field, field.getType(), type);
        outputType = getGraphQLConnection(isConnection, field, type, outputType, builder);

        builder.type(outputType);

        GraphQLDescription description = field.getAnnotation(GraphQLDescription.class);
        if (description != null) {
            builder.description(description.value());
        }

        GraphQLDeprecate deprecate = field.getAnnotation(GraphQLDeprecate.class);
        if (deprecate != null) {
            builder.deprecate(deprecate.value());
        }
        if (field.getAnnotation(Deprecated.class) != null) {
            builder.deprecate("Deprecated");
        }

        GraphQLDataFetcher dataFetcher = field.getAnnotation(GraphQLDataFetcher.class);
        DataFetcher actualDataFetcher = dataFetcher == null ? new FieldDataFetcher(field.getName()) : dataFetcher.value().newInstance();


        if (isConnection) {
            actualDataFetcher = new ConnectionDataFetcher(field.getAnnotation(GraphQLConnection.class).connection(), actualDataFetcher);
        }

        builder.dataFetcher(actualDataFetcher);

        return new GraphQLFieldDefinitionWrapper(builder.build());
    }

    private static GraphQLOutputType getGraphQLConnection(boolean isConnection, AccessibleObject field, GraphQLOutputType type, GraphQLOutputType outputType, GraphQLFieldDefinition.Builder builder) {
        if (isConnection) {
            if (type instanceof GraphQLList) {
                graphql.schema.GraphQLType wrappedType = ((GraphQLList) type).getWrappedType();
                assert wrappedType instanceof GraphQLObjectType;
                String annValue = field.getAnnotation(GraphQLConnection.class).name();
                String connectionName = annValue.isEmpty() ? wrappedType.getName() : annValue;
                Relay relay = new Relay();
                GraphQLObjectType edgeType = relay.edgeType(connectionName, (GraphQLOutputType) wrappedType, null, Collections.emptyList());
                outputType = relay.connectionType(connectionName, edgeType, Collections.emptyList());
                builder.argument(relay.getConnectionFieldArguments());
            }
        }
        return outputType;
    }

    private static boolean isConnection(AccessibleObject obj, Class klass, GraphQLOutputType type) {
        return obj.isAnnotationPresent(GraphQLConnection.class) &&
                               type instanceof GraphQLList &&
                               ((GraphQLList) type).getWrappedType() instanceof GraphQLObjectType;
    }

    protected static GraphQLFieldDefinition field(Method method) throws InstantiationException, IllegalAccessException {
        GraphQLFieldDefinition.Builder builder = newFieldDefinition();

        String name = method.getName().replaceFirst("^(is|get|set)(.+)", "$2");
        name = Character.toLowerCase(name.charAt(0)) + name.substring(1);
        GraphQLName nameAnn = method.getAnnotation(GraphQLName.class);
        builder.name(nameAnn == null ? name : nameAnn.value());

        GraphQLType annotation = method.getAnnotation(GraphQLType.class);
        if (annotation == null) {
            annotation = new defaultGraphQLType();
        }
        TypeFunction typeFunction = annotation.value().newInstance();
        AnnotatedType annotatedReturnType = method.getAnnotatedReturnType();

        GraphQLOutputType type = (GraphQLOutputType) typeFunction.apply(method.getReturnType(), annotatedReturnType);

        GraphQLOutputType outputType = method.getAnnotation(NotNull.class) == null ? type : new GraphQLNonNull(type);

        boolean isConnection = isConnection(method, method.getReturnType(), type);
        outputType = getGraphQLConnection(isConnection, method, type, outputType, builder);

        builder.type(outputType);


        List args = Arrays.asList(method.getParameters()).stream().
                filter(p -> !DataFetchingEnvironment.class.isAssignableFrom(p.getType())).
                map(new Function() {
                    @Override @SneakyThrows
                    public GraphQLArgument apply(Parameter parameter) {
                        Class t = parameter.getType();
                        graphql.schema.GraphQLType graphQLType = typeFunction.apply(t, parameter.getAnnotatedType());
                        if (graphQLType instanceof GraphQLObjectType) {
                            GraphQLInputObjectType inputObject = inputObject((GraphQLObjectType) graphQLType);
                            graphQLType = inputObject;
                        }
                        return argument(parameter, graphQLType);
                    }
                }).collect(Collectors.toList());

        GraphQLFieldDefinition relay = null;
        if (method.isAnnotationPresent(GraphQLRelayMutation.class)) {
            if (!(outputType instanceof GraphQLObjectType || outputType instanceof GraphQLInterfaceType)) {
                throw new RuntimeException("outputType should be an object or an interface");
            }
            StringBuffer titleBuffer = new StringBuffer(method.getName());
            titleBuffer.setCharAt(0, Character.toUpperCase(titleBuffer.charAt(0)));
            String title = titleBuffer.toString();
            List fieldDefinitions = outputType instanceof GraphQLObjectType ?
                    ((GraphQLObjectType) outputType).getFieldDefinitions() :
                    ((GraphQLInterfaceType) outputType).getFieldDefinitions();
            relay = new Relay().mutationWithClientMutationId(title, method.getName(),
                     args.stream().
                             map(t -> newInputObjectField().name(t.getName()).type(t.getType()).description(t.getDescription()).build()).
                            collect(Collectors.toList()), fieldDefinitions, null);
            builder.argument(relay.getArguments());
            builder.type(relay.getType());
        } else {
            builder.argument(args);
        }

        GraphQLDescription description = method.getAnnotation(GraphQLDescription.class);
        if (description != null) {
            builder.description(description.value());
        }

        GraphQLDeprecate deprecate = method.getAnnotation(GraphQLDeprecate.class);
        if (deprecate != null) {
            builder.deprecate(deprecate.value());
        }
        if (method.getAnnotation(Deprecated.class) != null) {
            builder.deprecate("Deprecated");
        }

        GraphQLDataFetcher dataFetcher = method.getAnnotation(GraphQLDataFetcher.class);
        DataFetcher actualDataFetcher = dataFetcher == null ? new MethodDataFetcher(method) : dataFetcher.value().newInstance();

        if (method.isAnnotationPresent(GraphQLRelayMutation.class) && relay != null) {
            actualDataFetcher = new RelayMutationMethodDataFetcher(method, args, relay.getArgument("input").getType(), relay.getType());
        }

        if (isConnection) {
            actualDataFetcher = new ConnectionDataFetcher(method.getAnnotation(GraphQLConnection.class).connection(), actualDataFetcher);
        }

        builder.dataFetcher(actualDataFetcher);

        return new GraphQLFieldDefinitionWrapper(builder.build());
    }

    public static GraphQLInputObjectType inputObject(GraphQLObjectType graphQLType) {
        GraphQLObjectType object = graphQLType;
        return new GraphQLInputObjectType(object.getName(), object.getDescription(),
                object.getFieldDefinitions().stream().
                        map(field -> {
                            GraphQLOutputType type = field.getType();
                            GraphQLInputType inputType;
                            if (type instanceof GraphQLObjectType) {
                                inputType = inputObject((GraphQLObjectType) type);
                            } else {
                                inputType = (GraphQLInputType) type;
                            }

                            return new GraphQLInputObjectField(field.getName(), field.getDescription(), inputType, null);
                        }).
                        collect(Collectors.toList()));
    }

    protected static GraphQLArgument argument(Parameter parameter, graphql.schema.GraphQLType t) throws IllegalAccessException, InstantiationException {
        GraphQLArgument.Builder builder = newArgument();
        builder.type(parameter.getAnnotation(NotNull.class) == null ? (GraphQLInputType) t : new graphql.schema.GraphQLNonNull(t));
        GraphQLDescription description = parameter.getAnnotation(GraphQLDescription.class);
        if (description != null) {
            builder.description(description.value());
        }
        GraphQLDefaultValue defaultValue = parameter.getAnnotation(GraphQLDefaultValue.class);
        if (defaultValue != null) {
            builder.defaultValue(defaultValue.value().newInstance().get());
        }
        GraphQLName name = parameter.getAnnotation(GraphQLName.class);
        if ( name != null ) {
            builder.name(name.value());
        } else {
            builder.name(parameter.getName());
        }
        return builder.build();
    }

    private static class defaultGraphQLType implements GraphQLType {

        @Override
        public Class annotationType() {
            return GraphQLType.class;
        }

        @Override
        public Class value() {
            return DefaultTypeFunction.class;
        }
    }

    private static class ConnectionDataFetcher implements DataFetcher {
        private final Class connection;
        private final DataFetcher actualDataFetcher;
        private final Constructor constructor;

        public ConnectionDataFetcher(Class connection, DataFetcher actualDataFetcher) {
            this.connection = connection;
            Optional> constructor =
                    Arrays.asList(connection.getConstructors()).stream().
                            filter(c -> c.getParameterCount() == 1).
                            map(c -> (Constructor) c).
                            findFirst();
            if (constructor.isPresent()) {
                this.constructor = constructor.get();
            } else {
                throw new IllegalArgumentException(connection + " doesn't have a single argument constructor");
            }
            this.actualDataFetcher = actualDataFetcher;
        }

        @Override @SneakyThrows
        public Object get(DataFetchingEnvironment environment) {
            // Exclude arguments
            DataFetchingEnvironment env = new DataFetchingEnvironment(environment.getSource(), new HashMap<>(), environment.getContext(),
                    environment.getFields(), environment.getFieldType(), environment.getParentType(), environment.getGraphQLSchema());
            Connection conn = constructor.newInstance(actualDataFetcher.get(env));
            return conn.get(environment);
        }
    }

    private static class UnionTypeResolver implements TypeResolver {
        private final Map, graphql.schema.GraphQLType> types = new HashMap<>();

        public UnionTypeResolver(Class[] classes) {
            Arrays.asList(classes).stream().
                    forEach(c -> types.put(c, DefaultTypeFunction.instance.apply(c, null)));
        }

        @Override @SneakyThrows
        public GraphQLObjectType getType(Object object) {
            Optional, graphql.schema.GraphQLType>> maybeType = types.entrySet().
                    stream().filter(e -> e.getKey().isAssignableFrom(object.getClass())).findFirst();
            if (maybeType.isPresent()) {
                return (GraphQLObjectType) maybeType.get().getValue();
            } else {
                throw new RuntimeException("Unknown type " + object.getClass());
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy