graphql.kickstart.autoconfigure.annotations.GraphQLAnnotationsAutoConfiguration Maven / Gradle / Ivy
package graphql.kickstart.autoconfigure.annotations;
import static java.util.Objects.nonNull;
import graphql.annotations.AnnotationsSchemaCreator;
import graphql.annotations.annotationTypes.GraphQLField;
import graphql.annotations.annotationTypes.GraphQLTypeExtension;
import graphql.annotations.annotationTypes.directives.definition.GraphQLDirectiveDefinition;
import graphql.annotations.processor.GraphQLAnnotations;
import graphql.annotations.processor.typeFunctions.TypeFunction;
import graphql.kickstart.annotations.GraphQLInterfaceTypeResolver;
import graphql.kickstart.annotations.GraphQLMutationResolver;
import graphql.kickstart.annotations.GraphQLQueryResolver;
import graphql.kickstart.annotations.GraphQLSubscriptionResolver;
import graphql.kickstart.autoconfigure.annotations.exceptions.MissingQueryResolverException;
import graphql.kickstart.autoconfigure.annotations.exceptions.MultipleMutationResolversException;
import graphql.kickstart.autoconfigure.annotations.exceptions.MultipleQueryResolversException;
import graphql.kickstart.autoconfigure.annotations.exceptions.MultipleSubscriptionResolversException;
import graphql.relay.Relay;
import graphql.schema.GraphQLScalarType;
import graphql.schema.GraphQLSchema;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import org.reflections.ReflectionsException;
import org.reflections.scanners.MethodAnnotationsScanner;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
@Slf4j
@AutoConfiguration
@RequiredArgsConstructor
@ConditionalOnProperty(value = "graphql.schema-strategy", havingValue = "ANNOTATIONS")
@EnableConfigurationProperties(GraphQLAnnotationsProperties.class)
public class GraphQLAnnotationsAutoConfiguration {
private final GraphQLAnnotationsProperties graphQLAnnotationsProperties;
private final Optional relay;
private final List typeFunctions;
private final List customScalarTypes;
@Bean
public GraphQLInterfaceTypeResolver graphQLInterfaceTypeResolver() {
return new GraphQLInterfaceTypeResolver();
}
@Bean
@ConditionalOnMissingBean
public GraphQLAnnotations graphQLAnnotations() {
GraphQLAnnotations graphQLAnnotations = new GraphQLAnnotations();
if (nonNull(graphQLAnnotationsProperties.getInputPrefix())) {
graphQLAnnotations
.getContainer()
.setInputPrefix(graphQLAnnotationsProperties.getInputPrefix());
}
if (nonNull(graphQLAnnotationsProperties.getInputSuffix())) {
graphQLAnnotations
.getContainer()
.setInputSuffix(graphQLAnnotationsProperties.getInputSuffix());
}
return graphQLAnnotations;
}
@Bean
public GraphQLSchema graphQLSchema(final GraphQLAnnotations graphQLAnnotations) {
log.info(
"Using GraphQL Annotations library to build the schema. Schema definition files will be ignored.");
log.info(
"GraphQL classes are searched in the following package (including subpackages): {}",
graphQLAnnotationsProperties.getBasePackage());
final AnnotationsSchemaCreator.Builder builder =
AnnotationsSchemaCreator.newAnnotationsSchema();
final Reflections reflections =
new Reflections(
graphQLAnnotationsProperties.getBasePackage(),
new MethodAnnotationsScanner(),
new SubTypesScanner(),
new TypeAnnotationsScanner());
builder.setAlwaysPrettify(graphQLAnnotationsProperties.isAlwaysPrettify());
setQueryResolverClass(builder, reflections);
setMutationResolverClass(builder, reflections);
setSubscriptionResolverClass(builder, reflections);
getTypesAnnotatedWith(reflections, GraphQLDirectiveDefinition.class)
.forEach(
directive -> {
log.info("Registering directive {}", directive);
builder.directive(directive);
});
getTypesAnnotatedWith(reflections, GraphQLTypeExtension.class)
.forEach(
typeExtension -> {
log.info("Registering type extension {}", typeExtension);
builder.typeExtension(typeExtension);
});
typeFunctions.forEach(
typeFunction -> {
log.info("Registering type function {}", typeFunction.getClass());
builder.typeFunction(typeFunction);
});
if (!customScalarTypes.isEmpty()) {
builder.typeFunction(new GraphQLScalarTypeFunction(customScalarTypes));
}
if (graphQLAnnotations.getClass().equals(GraphQLAnnotations.class)) {
log.info("Using default GraphQL Annotation processor.");
} else {
log.info("Using custom annotation process of type {}", graphQLAnnotations.getClass());
}
builder.setAnnotationsProcessor(graphQLAnnotations);
relay.ifPresent(
r -> {
log.info("Registering relay {}", r.getClass());
builder.setRelay(r);
});
registerGraphQLInterfaceImplementations(reflections, builder);
return builder.build();
}
private void setSubscriptionResolverClass(
final AnnotationsSchemaCreator.Builder builder, final Reflections reflections) {
final Set> subscriptionResolvers =
getTypesAnnotatedWith(reflections, GraphQLSubscriptionResolver.class);
if (subscriptionResolvers.size() > 1) {
throw new MultipleSubscriptionResolversException();
}
subscriptionResolvers.stream()
.findFirst()
.ifPresent(
subscriptionClass -> {
log.info("Registering subscription resolver class: {}", subscriptionClass);
builder.subscription(subscriptionClass);
});
}
private void setMutationResolverClass(
final AnnotationsSchemaCreator.Builder builder, final Reflections reflections) {
final Set> mutationResolvers =
getTypesAnnotatedWith(reflections, GraphQLMutationResolver.class);
if (mutationResolvers.size() > 1) {
throw new MultipleMutationResolversException();
}
mutationResolvers.stream()
.findFirst()
.ifPresent(
mutationClass -> {
log.info("Registering mutation resolver class: {}", mutationClass);
builder.mutation(mutationClass);
});
}
private void setQueryResolverClass(
final AnnotationsSchemaCreator.Builder builder, final Reflections reflections) {
final Set> queryResolvers =
getTypesAnnotatedWith(reflections, GraphQLQueryResolver.class);
if (queryResolvers.isEmpty()) {
throw new MissingQueryResolverException();
}
if (queryResolvers.size() > 1) {
throw new MultipleQueryResolversException();
}
queryResolvers.stream()
.findFirst()
.ifPresent(
queryClass -> {
log.info("Registering query resolver class: {}", queryClass);
builder.query(queryClass);
});
}
/**
* Workaround for a bug in Reflections - {@link Reflections#getTypesAnnotatedWith)} will throw a
* {@link ReflectionsException} if there are no types with annotations in the specified package.
*
* @param reflections the {@link Reflections} instance
* @param annotation the annotation class
* @return The set of classes annotated with the specified annotation, or an empty set if no
* annotated classes found.
*/
private Set> getTypesAnnotatedWith(
final Reflections reflections, final Class extends Annotation> annotation) {
try {
return reflections.getTypesAnnotatedWith(annotation);
} catch (ReflectionsException e) {
return Collections.emptySet();
}
}
/**
* This is required, because normally implementations of interfaces are not explicitly returned by
* any resolver method, and therefor not added to the schema automatically.
*
* All interfaces are considered GraphQL interfaces if they are declared in the configured
* package and have at least one {@link GraphQLField}-annotated methods.
*
* @param reflections the reflections instance.
* @param builder the schema builder instance.
*/
private void registerGraphQLInterfaceImplementations(
final Reflections reflections, final AnnotationsSchemaCreator.Builder builder) {
Predicate> implementationQualifiesForInclusion =
type ->
!(graphQLAnnotationsProperties.isIgnoreAbstractInterfaceImplementations()
&& Modifier.isAbstract(type.getModifiers()));
reflections.getMethodsAnnotatedWith(GraphQLField.class).stream()
.map(Method::getDeclaringClass)
.filter(Class::isInterface)
.forEach(
graphQLInterface ->
reflections.getSubTypesOf(graphQLInterface).stream()
.filter(implementationQualifiesForInclusion)
.forEach(
implementation -> {
log.info(
"Registering {} as an implementation of GraphQL interface {}",
implementation,
graphQLInterface);
builder.additionalType(implementation);
}));
}
}