Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit e560e6f

Browse files
feat(#5): add GraphQL Annotations support
1 parent 5e198a7 commit e560e6f

File tree

88 files changed

+1374
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+1374
-2
lines changed

gradle.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ LIB_GRAPHQL_JAVA_VER = 14.1
4343
LIB_SPRING_BOOT_VER = 2.3.1.RELEASE
4444
LIB_GRAPHQL_SERVLET_VER = 9.2.0
4545
LIB_GRAPHQL_JAVA_TOOLS_VER = 6.1.0
46-
46+
LIB_GRAPHQL_ANNOTATIONS_VER = 8.0
47+
LIB_REFLECTIONS_VER = 0.9.12
4748
LIB_APACHE_COMMONS_TEXT=1.8
4849
LIB_JSOUP_VER=1.13.1
4950
kotlin.version=1.3.72
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
dependencies {
2+
api project(":graphql-kickstart-spring-boot-autoconfigure-tools")
3+
api "io.github.graphql-java:graphql-java-annotations:$LIB_GRAPHQL_ANNOTATIONS_VER"
4+
implementation "org.reflections:reflections:$LIB_REFLECTIONS_VER"
5+
implementation "org.springframework.boot:spring-boot-autoconfigure"
6+
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
7+
8+
testImplementation "org.springframework.boot:spring-boot-starter-test"
9+
testRuntimeOnly "org.springframework.boot:spring-boot-starter-web"
10+
testImplementation project(":graphql-spring-boot-starter-test")
11+
testImplementation "org.reflections:reflections:$LIB_REFLECTIONS_VER"
12+
testImplementation "org.mockito:mockito-core"
13+
testImplementation "io.reactivex.rxjava2:rxjava"
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package graphql.kickstart.graphql.annotations;
2+
3+
import graphql.annotations.AnnotationsSchemaCreator;
4+
import graphql.annotations.annotationTypes.GraphQLTypeExtension;
5+
import graphql.annotations.annotationTypes.directives.definition.GraphQLDirectiveDefinition;
6+
import graphql.annotations.processor.GraphQLAnnotations;
7+
import graphql.annotations.processor.typeFunctions.TypeFunction;
8+
import graphql.kickstart.graphql.annotations.exceptions.MissingQueryResolverException;
9+
import graphql.kickstart.graphql.annotations.exceptions.MultipleMutationResolversException;
10+
import graphql.kickstart.graphql.annotations.exceptions.MultipleQueryResolversException;
11+
import graphql.kickstart.graphql.annotations.exceptions.MultipleSubscriptionResolversException;
12+
import graphql.kickstart.tools.GraphQLMutationResolver;
13+
import graphql.kickstart.tools.GraphQLQueryResolver;
14+
import graphql.kickstart.tools.GraphQLSubscriptionResolver;
15+
import graphql.kickstart.tools.boot.GraphQLJavaToolsAutoConfiguration;
16+
import graphql.relay.Relay;
17+
import graphql.schema.GraphQLScalarType;
18+
import graphql.schema.GraphQLSchema;
19+
import lombok.RequiredArgsConstructor;
20+
import lombok.extern.slf4j.Slf4j;
21+
import org.reflections.Reflections;
22+
import org.reflections.ReflectionsException;
23+
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
24+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.Configuration;
27+
28+
import java.lang.annotation.Annotation;
29+
import java.util.Collections;
30+
import java.util.List;
31+
import java.util.Optional;
32+
import java.util.Set;
33+
34+
import static graphql.annotations.AnnotationsSchemaCreator.newAnnotationsSchema;
35+
import static java.util.Objects.isNull;
36+
37+
@Configuration
38+
@AutoConfigureBefore({GraphQLJavaToolsAutoConfiguration.class})
39+
@EnableConfigurationProperties(GraphQLAnnotationsProperties.class)
40+
@RequiredArgsConstructor
41+
@Slf4j
42+
public class GraphQLAnnotationsAutoConfiguration {
43+
44+
private final GraphQLAnnotationsProperties graphQLAnnotationsProperties;
45+
private final Optional<Relay> relay;
46+
private final Optional<GraphQLAnnotations> customAnnotationProcessor;
47+
private final List<TypeFunction> typeFunctions;
48+
private final List<GraphQLScalarType> customScalarTypes;
49+
50+
@Bean
51+
public GraphQLSchema graphQLSchema() {
52+
log.info("Using GraphQL Annotations library to build the schema. Schema definition files will be ignored.");
53+
log.info("GraphQL classes are searched in the following package (including subpackages): {}",
54+
graphQLAnnotationsProperties.getBasePackage());
55+
final AnnotationsSchemaCreator.Builder builder = newAnnotationsSchema();
56+
final Reflections reflections = new Reflections(graphQLAnnotationsProperties.getBasePackage());
57+
builder.setAlwaysPrettify(graphQLAnnotationsProperties.isAlwaysPrettify());
58+
setQueryResolverClass(builder, reflections);
59+
setMutationResolverClass(builder, reflections);
60+
setSubscriptionResolverClass(builder, reflections);
61+
getTypesAnnotatedWith(reflections, GraphQLDirectiveDefinition.class).forEach(directive -> {
62+
log.info("Registering directive {}", directive);
63+
builder.directive(directive);
64+
});
65+
getTypesAnnotatedWith(reflections, GraphQLTypeExtension.class).forEach(typeExtension -> {
66+
log.info("Registering type extension {}", typeExtension);
67+
builder.typeExtension(typeExtension);
68+
});
69+
typeFunctions.forEach(typeFunction -> {
70+
log.info("Registering type function {}", typeFunction.getClass());
71+
builder.typeFunction(typeFunction);
72+
});
73+
if (!customScalarTypes.isEmpty()) {
74+
builder.typeFunction(new GraphQLScalarTypeFunction(customScalarTypes));
75+
}
76+
customAnnotationProcessor.ifPresent(graphQLAnnotations -> {
77+
log.info("Registering custom GraphQL annotations processor {}", graphQLAnnotations.getClass());
78+
builder.setAnnotationsProcessor(graphQLAnnotations);
79+
});
80+
if (isNull(builder.getGraphQLAnnotations())) {
81+
// before setting a relay, we have to set the annotation processor, otherwise a NPE will occur
82+
builder.setAnnotationsProcessor(new GraphQLAnnotations());
83+
}
84+
relay.ifPresent(r -> {
85+
log.info("Registering relay {}", r.getClass());
86+
builder.setRelay(r);
87+
});
88+
return builder.build();
89+
}
90+
91+
private void setSubscriptionResolverClass(
92+
final AnnotationsSchemaCreator.Builder builder,
93+
final Reflections reflections
94+
) {
95+
final Set<Class<? extends GraphQLSubscriptionResolver>> subscriptionResolvers
96+
= getSubTypesOf(reflections, GraphQLSubscriptionResolver.class);
97+
if (subscriptionResolvers.size() > 1) {
98+
throw new MultipleSubscriptionResolversException();
99+
}
100+
subscriptionResolvers.stream().findFirst().ifPresent(subscriptionClass -> {
101+
log.info("Registering subscription resolver class: {}", subscriptionClass);
102+
builder.subscription(subscriptionClass);
103+
});
104+
}
105+
106+
private void setMutationResolverClass(
107+
final AnnotationsSchemaCreator.Builder builder,
108+
final Reflections reflections
109+
) {
110+
final Set<Class<? extends GraphQLMutationResolver>> mutationResolvers
111+
= getSubTypesOf(reflections, GraphQLMutationResolver.class);
112+
if (mutationResolvers.size() > 1) {
113+
throw new MultipleMutationResolversException();
114+
}
115+
mutationResolvers.stream().findFirst().ifPresent(mutationClass -> {
116+
log.info("Registering mutation resolver class: {}", mutationClass);
117+
builder.mutation(mutationClass);
118+
});
119+
}
120+
121+
private void setQueryResolverClass(
122+
final AnnotationsSchemaCreator.Builder builder,
123+
final Reflections reflections
124+
) {
125+
final Set<Class<? extends GraphQLQueryResolver>> queryResolvers
126+
= getSubTypesOf(reflections, GraphQLQueryResolver.class);
127+
if (queryResolvers.size() == 0) {
128+
throw new MissingQueryResolverException();
129+
}
130+
if (queryResolvers.size() > 1) {
131+
throw new MultipleQueryResolversException();
132+
}
133+
queryResolvers.stream().findFirst().ifPresent(queryClass -> {
134+
log.info("Registering query resolver class: {}", queryClass);
135+
builder.query(queryClass);
136+
});
137+
}
138+
139+
/**
140+
* Workaround for a bug in Reflections - {@link Reflections#getTypesAnnotatedWith)} will throw a
141+
* {@link ReflectionsException} if there are no types with annotations in the specified package.
142+
* @param reflections the {@link Reflections} instance
143+
* @param annotation the annotation class
144+
* @return The set of classes annotated with that annotations, or empty set if no annotations found.
145+
*/
146+
private Set<Class<?>> getTypesAnnotatedWith(
147+
final Reflections reflections,
148+
final Class<? extends Annotation> annotation
149+
) {
150+
try {
151+
return reflections.getTypesAnnotatedWith(annotation);
152+
} catch (ReflectionsException e) {
153+
return Collections.emptySet();
154+
}
155+
}
156+
157+
/**
158+
* Workaround for a bug in Reflections - {@link Reflections#getSubTypesOf(Class)} )} will throw a
159+
* {@link ReflectionsException} if there are no class in the specified package.
160+
* @param reflections the {@link Reflections} instance
161+
* @param aClass a class
162+
* @return The set of classes that are subclasses of the specified class, or empty set if no annotations found.
163+
* @see <a href="https://github.com/ronmamo/reflections/issues/273">Issue #273</>
164+
*/
165+
private <T> Set<Class<? extends T>> getSubTypesOf(
166+
final Reflections reflections,
167+
final Class<T> aClass
168+
) {
169+
try {
170+
return reflections.getSubTypesOf(aClass);
171+
} catch (ReflectionsException e) {
172+
return Collections.emptySet();
173+
}
174+
}
175+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package graphql.kickstart.graphql.annotations;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
import org.springframework.boot.context.properties.ConfigurationProperties;
8+
import org.springframework.validation.annotation.Validated;
9+
10+
import javax.validation.constraints.NotBlank;
11+
12+
@Data
13+
@Builder
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
@Validated
17+
@ConfigurationProperties(prefix = "graphql.annotations")
18+
public class GraphQLAnnotationsProperties {
19+
20+
/**
21+
* The base package where GraphQL definitions (resolvers, types etc.) are searched for.
22+
*/
23+
private @NotBlank String basePackage;
24+
25+
/**
26+
* Set if fields should be globally prettified (removes get/set/is prefixes from names). Defaults to true.
27+
*/
28+
@Builder.Default
29+
private boolean alwaysPrettify = true;
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package graphql.kickstart.graphql.annotations;
2+
3+
import graphql.annotations.processor.ProcessingElementsContainer;
4+
import graphql.annotations.processor.typeFunctions.TypeFunction;
5+
import graphql.schema.GraphQLScalarType;
6+
import graphql.schema.GraphQLType;
7+
import lombok.RequiredArgsConstructor;
8+
import lombok.extern.slf4j.Slf4j;
9+
10+
import java.lang.reflect.AnnotatedType;
11+
import java.lang.reflect.ParameterizedType;
12+
import java.lang.reflect.Type;
13+
import java.util.List;
14+
import java.util.Optional;
15+
16+
import static java.util.Objects.nonNull;
17+
18+
/**
19+
* Maps Java classes to the corresponding scalar definitions.
20+
*/
21+
@RequiredArgsConstructor
22+
@Slf4j
23+
public class GraphQLScalarTypeFunction implements TypeFunction {
24+
25+
private final List<GraphQLScalarType> customScalarTypes;
26+
27+
@Override
28+
public boolean canBuildType(final Class<?> aClass, final AnnotatedType annotatedType) {
29+
return getMatchingScalarDefinition(aClass).isPresent();
30+
}
31+
32+
@Override
33+
public GraphQLType buildType(
34+
final boolean input,
35+
final Class<?> aClass,
36+
final AnnotatedType annotatedType,
37+
final ProcessingElementsContainer container
38+
) {
39+
final GraphQLScalarType graphQLScalarType = getMatchingScalarDefinition(aClass).orElse(null);
40+
if (nonNull(graphQLScalarType)) {
41+
log.info("Registering scalar type {} for Java class {}", graphQLScalarType.getName(), aClass);
42+
}
43+
return graphQLScalarType;
44+
}
45+
46+
private Optional<GraphQLScalarType> getMatchingScalarDefinition(final Class<?> aClass) {
47+
return customScalarTypes.stream().filter(scalarType -> {
48+
final Type[] genericInterfaces = scalarType.getCoercing().getClass().getGenericInterfaces();
49+
return genericInterfaces.length > 0
50+
&& genericInterfaces[0] instanceof ParameterizedType
51+
&& ((ParameterizedType) genericInterfaces[0]).getActualTypeArguments().length > 0
52+
&& ((ParameterizedType) genericInterfaces[0]).getActualTypeArguments()[0].equals(aClass);
53+
}).findFirst();
54+
}
55+
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package graphql.kickstart.graphql.annotations.exceptions;
2+
3+
public class MissingQueryResolverException extends RuntimeException {
4+
5+
private static final long serialVersionUID = 1L;
6+
7+
public MissingQueryResolverException() {
8+
super("No query resolver provided. When using GraphQL Annotations, one query resolver must be provided.");
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package graphql.kickstart.graphql.annotations.exceptions;
2+
3+
public class MultipleMutationResolversException extends RuntimeException {
4+
5+
private static final long serialVersionUID = 1L;
6+
7+
public MultipleMutationResolversException() {
8+
super("Multiple mutation resolvers provided. GraphQL Annotations allows only one resolver.");
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package graphql.kickstart.graphql.annotations.exceptions;
2+
3+
public class MultipleQueryResolversException extends RuntimeException {
4+
5+
private static final long serialVersionUID = 1L;
6+
7+
public MultipleQueryResolversException() {
8+
super("Multiple query resolvers provided. GraphQL Annotations allows only one resolver.");
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package graphql.kickstart.graphql.annotations.exceptions;
2+
3+
public class MultipleSubscriptionResolversException extends RuntimeException {
4+
5+
private static final long serialVersionUID = 1L;
6+
7+
public MultipleSubscriptionResolversException() {
8+
super("Multiple subscription resolvers provided. GraphQL Annotations allows only one resolver.");
9+
}
10+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
2+
graphql.kickstart.graphql.annotations.GraphQLAnnotationsAutoConfiguration

0 commit comments

Comments
 (0)