diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsAutoConfiguration.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsAutoConfiguration.java index 02a9e059..61fe0dfe 100644 --- a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsAutoConfiguration.java +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsAutoConfiguration.java @@ -17,10 +17,12 @@ 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; @@ -179,11 +181,15 @@ 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) + reflections.getSubTypesOf(graphQLInterface).stream() + .filter(implementationQualifiesForInclusion) .forEach(implementation -> { log.info("Registering {} as an implementation of GraphQL interface {}", implementation, diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsProperties.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsProperties.java index 6460c057..36c4fc25 100644 --- a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsProperties.java +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/main/java/graphql/kickstart/graphql/annotations/GraphQLAnnotationsProperties.java @@ -27,4 +27,11 @@ public class GraphQLAnnotationsProperties { */ @Builder.Default private boolean alwaysPrettify = true; + + /** + * If set to true abstract classes implementing a GraphQL interface will not be added to the schema. + * Defaults to false for backward compatibility. + */ + @Builder.Default + private boolean ignoreAbstractInterfaceImplementations = false; } diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryIgnoreAbstractInterfaceImplementationsTest.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryIgnoreAbstractInterfaceImplementationsTest.java new file mode 100644 index 00000000..2c24b6bf --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryIgnoreAbstractInterfaceImplementationsTest.java @@ -0,0 +1,64 @@ +package graphql.kickstart.graphql.annotations; + +import com.graphql.spring.boot.test.GraphQLResponse; +import com.graphql.spring.boot.test.GraphQLTestTemplate; +import graphql.kickstart.graphql.annotations.test.interfaces.Car; +import graphql.kickstart.graphql.annotations.test.interfaces.Truck; +import graphql.schema.GraphQLNamedType; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.io.IOException; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Testing interface handling (ignore abstract implementations).") +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = "graphql.annotations.ignore-abstract-interface-implementations=true") +@ActiveProfiles({"test", "interface-test"}) +class GraphQLInterfaceQueryIgnoreAbstractInterfaceImplementationsTest { + + @Autowired + private GraphQLTestTemplate graphQLTestTemplate; + + @Autowired + private GraphQLSchema graphQLSchema; + + @Test + @DisplayName("Assert that GraphQL interfaces and their implementations are registered correctly.") + void testInterfaceQuery() throws IOException { + // WHEN + final GraphQLResponse actual = graphQLTestTemplate + .postForResource("queries/test-interface-query.graphql"); + // THEN + assertThat(actual.get("$.data.vehicles[0]", Car.class)) + .usingRecursiveComparison().ignoringAllOverriddenEquals() + .isEqualTo(Car.builder().numberOfSeats(4).registrationNumber("ABC-123").build()); + assertThat(actual.get("$.data.vehicles[1]", Truck.class)) + .usingRecursiveComparison().ignoringAllOverriddenEquals() + .isEqualTo(Truck.builder().cargoWeightCapacity(12).registrationNumber("CBA-321").build()); + } + + @Test + @DisplayName("Assert that abstract GraphQL interface implementations are excluded from the schema.") + void testInterfaceImplementationDetection() { + // THEN + Set vehicleDomainTypes = graphQLSchema.getAllTypesAsList().stream() + .filter(type -> !(type instanceof GraphQLScalarType)) + .map(GraphQLNamedType::getName) + .filter(name -> !name.startsWith("__")) + .filter(name -> !"PageInfo".equals(name)) + .collect(Collectors.toSet()); + // Must not contain "AbstractVehicle" + assertThat(vehicleDomainTypes) + .containsExactlyInAnyOrder("InterfaceQuery", "Vehicle", "Car", "Truck"); + } +} + diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryTest.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryTest.java index e7aaf680..9ed3039b 100644 --- a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryTest.java +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/GraphQLInterfaceQueryTest.java @@ -7,6 +7,11 @@ import graphql.kickstart.graphql.annotations.test.interfaces.Car; import graphql.kickstart.graphql.annotations.test.interfaces.Truck; import java.io.IOException; +import java.util.Set; +import java.util.stream.Collectors; +import graphql.schema.GraphQLNamedType; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -21,6 +26,9 @@ class GraphQLInterfaceQueryTest { @Autowired private GraphQLTestTemplate graphQLTestTemplate; + @Autowired + private GraphQLSchema graphQLSchema; + @Test @DisplayName("Assert that GraphQL interfaces and their implementations are registered correctly.") void testInterfaceQuery() throws IOException { @@ -35,5 +43,20 @@ void testInterfaceQuery() throws IOException { .usingRecursiveComparison().ignoringAllOverriddenEquals() .isEqualTo(Truck.builder().cargoWeightCapacity(12).registrationNumber("CBA-321").build()); } + + @Test + @DisplayName("Assert that abstract GraphQL interface implementations are added to the schema.") + void testInterfaceImplementationDetection() { + // THEN + Set vehicleDomainTypes = graphQLSchema.getAllTypesAsList().stream() + .filter(type -> !(type instanceof GraphQLScalarType)) + .map(GraphQLNamedType::getName) + .filter(name -> !name.startsWith("__")) + .filter(name -> !"PageInfo".equals(name)) + .collect(Collectors.toSet()); + // Should contain "AbstractVehicle" + assertThat(vehicleDomainTypes) + .containsExactlyInAnyOrder("InterfaceQuery", "Vehicle", "AbstractVehicle", "Car", "Truck"); + } } diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/AbstractVehicle.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/AbstractVehicle.java new file mode 100644 index 00000000..dad3a501 --- /dev/null +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/AbstractVehicle.java @@ -0,0 +1,22 @@ +package graphql.kickstart.graphql.annotations.test.interfaces; + +import graphql.annotations.annotationTypes.GraphQLField; +import graphql.annotations.annotationTypes.GraphQLNonNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; + +@Data +@SuperBuilder +@AllArgsConstructor +@NoArgsConstructor +public abstract class AbstractVehicle implements Vehicle { + + /** + * Note that you have to repeat the annotations from the interface method! + */ + @GraphQLField + @GraphQLNonNull + private String registrationNumber; +} diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Car.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Car.java index 0f684229..5c836a5c 100644 --- a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Car.java +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Car.java @@ -3,24 +3,26 @@ import graphql.annotations.annotationTypes.GraphQLField; import graphql.annotations.annotationTypes.GraphQLNonNull; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; @Data -@Builder +@SuperBuilder @AllArgsConstructor @NoArgsConstructor -public class Car implements Vehicle { - - /** - * Note that you have to repeat the annotations from the interface method! - */ - @GraphQLField - @GraphQLNonNull - private String registrationNumber; +@EqualsAndHashCode(callSuper = true) +// “implements Vehicle” has to be repeated here although already inherited from AbstractVehicle +// because otherwise GraphQL-Java Annotations would not find this class. +public class Car extends AbstractVehicle implements Vehicle { @GraphQLField @GraphQLNonNull private int numberOfSeats; + + public Car(String registrationNumber, int numberOfSeats) { + super(registrationNumber); + this.numberOfSeats = numberOfSeats; + } } diff --git a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Truck.java b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Truck.java index 9bc43003..f1f7fbf8 100644 --- a/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Truck.java +++ b/graphql-kickstart-spring-boot-autoconfigure-graphql-annotations/src/test/java/graphql/kickstart/graphql/annotations/test/interfaces/Truck.java @@ -11,6 +11,8 @@ @Builder @NoArgsConstructor @AllArgsConstructor +// Truck intentionally does not extend AbstractVehicle in order to have one inheritance +// hierarchy free from abstract classes. public class Truck implements Vehicle { /**