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

io.smallrye.graphql.schema.SchemaBuilder Maven / Gradle / Ivy

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

import static io.smallrye.graphql.schema.Annotations.CUSTOM_SCALAR;
import static io.smallrye.graphql.schema.Annotations.DIRECTIVE;
import static io.smallrye.graphql.schema.Annotations.ONE_OF;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.logging.Logger;

import io.smallrye.graphql.schema.creator.ArgumentCreator;
import io.smallrye.graphql.schema.creator.DirectiveTypeCreator;
import io.smallrye.graphql.schema.creator.FieldCreator;
import io.smallrye.graphql.schema.creator.OperationCreator;
import io.smallrye.graphql.schema.creator.ReferenceCreator;
import io.smallrye.graphql.schema.creator.type.Creator;
import io.smallrye.graphql.schema.creator.type.CustomScalarCreator;
import io.smallrye.graphql.schema.creator.type.EnumCreator;
import io.smallrye.graphql.schema.creator.type.InputTypeCreator;
import io.smallrye.graphql.schema.creator.type.InterfaceCreator;
import io.smallrye.graphql.schema.creator.type.TypeCreator;
import io.smallrye.graphql.schema.creator.type.UnionCreator;
import io.smallrye.graphql.schema.helper.BeanValidationDirectivesHelper;
import io.smallrye.graphql.schema.helper.DescriptionHelper;
import io.smallrye.graphql.schema.helper.Directives;
import io.smallrye.graphql.schema.helper.GroupHelper;
import io.smallrye.graphql.schema.helper.RolesAllowedDirectivesHelper;
import io.smallrye.graphql.schema.helper.TypeAutoNameStrategy;
import io.smallrye.graphql.schema.model.DirectiveType;
import io.smallrye.graphql.schema.model.ErrorInfo;
import io.smallrye.graphql.schema.model.Group;
import io.smallrye.graphql.schema.model.Operation;
import io.smallrye.graphql.schema.model.OperationType;
import io.smallrye.graphql.schema.model.Reference;
import io.smallrye.graphql.schema.model.ReferenceType;
import io.smallrye.graphql.schema.model.Schema;

/**
 * This builds schema model using Jandex.
 * 

* It starts scanning all queries and mutation, building the operations for those. * The operation reference some types (via Reference) that should be created and added to the schema. *

* The creation of these type them self create more references to types (via Reference) that should be created and added to the * scheme. *

* It does above recursively until there is no more things to create. * * @author Phillip Kruger ([email protected]) */ public class SchemaBuilder { private static final Logger LOG = Logger.getLogger(SchemaBuilder.class.getName()); private final InputTypeCreator inputTypeCreator; private final TypeCreator typeCreator; private final FieldCreator fieldCreator; private final ArgumentCreator argumentCreator; private final InterfaceCreator interfaceCreator; private final EnumCreator enumCreator; private final ReferenceCreator referenceCreator; private final OperationCreator operationCreator; private final DirectiveTypeCreator directiveTypeCreator; private final UnionCreator unionCreator; private final CustomScalarCreator customScalarCreator; private final DotName FEDERATION_ANNOTATIONS_PACKAGE = DotName.createSimple("io.smallrye.graphql.api.federation"); /** * This builds the Schema from Jandex * * @param index the Jandex index * @return the Schema */ public static Schema build(IndexView index) { return build(index, TypeAutoNameStrategy.Default); } /** * This builds the Schema from Jandex * * @param index the Jandex index * @param autoNameStrategy the naming strategy * @return the Schema */ public static Schema build(IndexView index, TypeAutoNameStrategy autoNameStrategy) { ScanningContext.register(index); return new SchemaBuilder(autoNameStrategy).generateSchema(); } private SchemaBuilder(TypeAutoNameStrategy autoNameStrategy) { enumCreator = new EnumCreator(autoNameStrategy); referenceCreator = new ReferenceCreator(autoNameStrategy); fieldCreator = new FieldCreator(referenceCreator); argumentCreator = new ArgumentCreator(referenceCreator); inputTypeCreator = new InputTypeCreator(fieldCreator); operationCreator = new OperationCreator(referenceCreator, argumentCreator); typeCreator = new TypeCreator(referenceCreator, fieldCreator, operationCreator); interfaceCreator = new InterfaceCreator(referenceCreator, fieldCreator, operationCreator); directiveTypeCreator = new DirectiveTypeCreator(referenceCreator); unionCreator = new UnionCreator(referenceCreator); customScalarCreator = new CustomScalarCreator(referenceCreator); } private Schema generateSchema() { // Get all the @GraphQLAPI annotations Collection graphQLApiAnnotations = ScanningContext.getIndex() .getAnnotations(Annotations.GRAPHQL_API); final Schema schema = new Schema(); addDirectiveTypes(schema); List allDirectiveTypes = schema.getDirectiveTypes(); graphQLJavaDirectives().forEach(graphqlJavaDirectiveDotName -> { ClassInfo graphqlJavaDirectiveClazz = ScanningContext.getIndex().getClassByName(graphqlJavaDirectiveDotName); if (graphqlJavaDirectiveClazz != null) { allDirectiveTypes.add(directiveTypeCreator .create(ScanningContext.getIndex().getClassByName(graphqlJavaDirectiveDotName))); } }); Directives directivesHelper = new Directives(allDirectiveTypes); setupDirectives(directivesHelper); // add AppliedSchemaDirectives and Schema Description setUpSchemaDirectivesAndDescription(schema, graphQLApiAnnotations, directivesHelper); addCustomScalarTypes(schema); for (AnnotationInstance graphQLApiAnnotation : graphQLApiAnnotations) { ClassInfo apiClass = graphQLApiAnnotation.target().asClass(); List methods = getAllMethodsIncludingFromSuperClasses(apiClass); Optional group = GroupHelper.getGroup(graphQLApiAnnotation); addOperations(group, schema, methods); } // The above queries and mutations reference some models (input / type / interfaces / enum), let's create those addTypesToSchema(schema); // We might have missed something addOutstandingTypesToSchema(schema); // Add all annotated errors (Exceptions) addErrors(schema); // Add all custom datafetchers addDataFetchers(schema); // Reset the maps. referenceCreator.clear(); return schema; } private List getAllMethodsIncludingFromSuperClasses(ClassInfo classInfo) { ClassInfo current = classInfo; IndexView index = ScanningContext.getIndex(); List methods = new ArrayList<>(); while (current != null) { current.methods().stream().filter(methodInfo -> !methodInfo.isSynthetic()).forEach(methods::add); DotName superName = classInfo.superName(); if (superName != null) { current = index.getClassByName(current.superName()); } else { current = null; } } return methods; } private void addDirectiveTypes(Schema schema) { // custom directives from annotations for (AnnotationInstance annotationInstance : ScanningContext.getIndex().getAnnotations(DIRECTIVE)) { ClassInfo classInfo = annotationInstance.target().asClass(); boolean federationEnabled = Boolean.getBoolean("smallrye.graphql.federation.enabled"); // only add federation-related directive types to the schema if federation is enabled DotName packageName = classInfo.name().packagePrefixName(); if ((packageName == null || !packageName.toString().startsWith(FEDERATION_ANNOTATIONS_PACKAGE.toString()) || federationEnabled) && !isGraphQLJavaDirective(classInfo)) { schema.addDirectiveType(directiveTypeCreator.create(classInfo)); } } // bean validation directives schema.addDirectiveType(BeanValidationDirectivesHelper.CONSTRAINT_DIRECTIVE_TYPE); // rolesAllowed directive schema.addDirectiveType(RolesAllowedDirectivesHelper.ROLES_ALLOWED_DIRECTIVE_TYPE); } private void addCustomScalarTypes(Schema schema) { Collection annotations = ScanningContext.getIndex().getAnnotations(CUSTOM_SCALAR); for (AnnotationInstance annotationInstance : annotations) { schema.addCustomScalarType(customScalarCreator.create( annotationInstance.target().asClass(), annotationInstance.value().asString())); } } private void setupDirectives(Directives directives) { customScalarCreator.setDirectives(directives); inputTypeCreator.setDirectives(directives); typeCreator.setDirectives(directives); interfaceCreator.setDirectives(directives); enumCreator.setDirectives(directives); fieldCreator.setDirectives(directives); argumentCreator.setDirectives(directives); operationCreator.setDirectives(directives); unionCreator.setDirectives(directives); } private void addTypesToSchema(Schema schema) { // Add the input types createAndAddToSchema(ReferenceType.INPUT, inputTypeCreator, schema::addInput); // Add the output types createAndAddToSchema(ReferenceType.TYPE, typeCreator, schema::addType); // Add the interface types createAndAddToSchema(ReferenceType.INTERFACE, interfaceCreator, schema::addInterface); // Add the union types createAndAddToSchema(ReferenceType.UNION, unionCreator, schema::addUnion); // Add the enum types createAndAddToSchema(ReferenceType.ENUM, enumCreator, schema::addEnum); } private void addOutstandingTypesToSchema(Schema schema) { boolean keepGoing = false; // See if there is any inputs we missed if (findOutstandingAndAddToSchema(ReferenceType.INPUT, inputTypeCreator, schema::containsInput, schema::addInput)) { keepGoing = true; } // See if there is any types we missed if (findOutstandingAndAddToSchema(ReferenceType.TYPE, typeCreator, schema::containsType, schema::addType)) { keepGoing = true; } // See if there is any interfaces we missed if (findOutstandingAndAddToSchema(ReferenceType.INTERFACE, interfaceCreator, schema::containsInterface, schema::addInterface)) { keepGoing = true; } // See if there is any unions we missed if (findOutstandingAndAddToSchema(ReferenceType.UNION, unionCreator, schema::containsUnion, schema::addUnion)) { keepGoing = true; } // See if there is any enums we missed if (findOutstandingAndAddToSchema(ReferenceType.ENUM, enumCreator, schema::containsEnum, schema::addEnum)) { keepGoing = true; } // If we missed something, that something might have created types we do not know about yet, so continue until we have everything if (keepGoing) { addOutstandingTypesToSchema(schema); } } private void addErrors(Schema schema) { Collection errorAnnotations = ScanningContext.getIndex().getAnnotations(Annotations.ERROR_CODE); if (errorAnnotations != null && !errorAnnotations.isEmpty()) { for (AnnotationInstance errorAnnotation : errorAnnotations) { AnnotationTarget annotationTarget = errorAnnotation.target(); if (annotationTarget.kind().equals(AnnotationTarget.Kind.CLASS)) { ClassInfo exceptionClass = annotationTarget.asClass(); AnnotationValue value = errorAnnotation.value(); if (value != null && value.asString() != null && !value.asString().isEmpty()) { schema.addError(new ErrorInfo(exceptionClass.name().toString(), value.asString())); } else { LOG.warn("Ignoring @ErrorCode on " + annotationTarget + " - Annotation value is not set"); } } else { LOG.warn("Ignoring @ErrorCode on " + annotationTarget + " - Wrong target, only apply to CLASS [" + annotationTarget.kind().toString() + "]"); } } } } private void addDataFetchers(Schema schema) { Collection datafetcherAnnotations = ScanningContext.getIndex() .getAnnotations(Annotations.DATAFETCHER); if (datafetcherAnnotations != null && !datafetcherAnnotations.isEmpty()) { for (AnnotationInstance datafetcherAnnotation : datafetcherAnnotations) { AnnotationTarget annotationTarget = datafetcherAnnotation.target(); if (annotationTarget.kind().equals(AnnotationTarget.Kind.CLASS)) { ClassInfo datafetcherClass = annotationTarget.asClass(); AnnotationValue forClass = datafetcherAnnotation.value("forClass"); AnnotationValue isWrapped = datafetcherAnnotation.value("isWrapped"); LOG.info("Adding custom datafetcher for " + forClass.asClass().name().toString() + " [" + datafetcherClass.simpleName() + "]"); if (isWrapped != null && isWrapped.asBoolean()) { // Wrapped schema.addWrappedDataFetcher(forClass.asClass().name().toString(), datafetcherClass.simpleName()); } else { // Field schema.addFieldDataFetcher(forClass.asClass().name().toString(), datafetcherClass.simpleName()); } } } } } private void createAndAddToSchema(ReferenceType referenceType, Creator creator, Consumer consumer) { Queue queue = referenceCreator.values(referenceType); while (!queue.isEmpty()) { Reference reference = queue.poll(); ClassInfo classInfo = ScanningContext.getIndex().getClassByName(DotName.createSimple(reference.getClassName())); consumer.accept(creator.create(classInfo, reference)); } } private boolean findOutstandingAndAddToSchema(ReferenceType referenceType, Creator creator, Predicate contains, Consumer consumer) { boolean keepGoing = false; // Let's see what still needs to be done. Queue values = referenceCreator.values(referenceType); while (!values.isEmpty()) { Reference reference = values.poll(); ClassInfo classInfo = ScanningContext.getIndex().getClassByName(DotName.createSimple(reference.getClassName())); if (!contains.test(reference.getName())) { consumer.accept(creator.create(classInfo, reference)); keepGoing = true; } } return keepGoing; } /** * This inspect all method, looking for Query and Mutation annotations, * to create those Operations. * * @param schema the schema to add the operation to. * @param methodInfoList the java methods. */ private void addOperations(Optional group, Schema schema, List methodInfoList) { for (MethodInfo methodInfo : methodInfoList) { Annotations annotationsForMethod = Annotations.getAnnotationsForMethod(methodInfo); if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.QUERY)) { Operation query = operationCreator.createOperation(methodInfo, OperationType.QUERY, null); if (group.isPresent()) { schema.addGroupedQuery(group.get(), query); } else { schema.addQuery(query); } } else if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.MUTATION)) { Operation mutation = operationCreator.createOperation(methodInfo, OperationType.MUTATION, null); if (group.isPresent()) { schema.addGroupedMutation(group.get(), mutation); } else { schema.addMutation(mutation); } } else if (annotationsForMethod.containsOneOfTheseAnnotations(Annotations.SUBCRIPTION)) { Operation subscription = operationCreator.createOperation(methodInfo, OperationType.SUBSCRIPTION, null); if (group.isPresent()) { schema.addGroupedSubscription(group.get(), subscription); } else { schema.addSubscription(subscription); } } } } private void setUpSchemaDirectivesAndDescription(Schema schema, Collection graphQLApiAnnotations, Directives directivesHelper) { Set directiveClassNames = new HashSet<>(); for (AnnotationInstance graphQLApiAnnotation : graphQLApiAnnotations) { Annotations annotations = Annotations.getAnnotationsForClass(graphQLApiAnnotation.target().asClass()); getSchemaDirectives(schema, directiveClassNames, annotations, directivesHelper, String.valueOf(graphQLApiAnnotation.target().asClass().name())); getDescription(annotations).ifPresent(description -> { if (schema.getDescription() == null) { schema.setDescription(description); } else { LOG.warn("Duplicate @description annotation for @GraphQLApi class"); } }); } } private Optional getDescription(Annotations annotations) { return DescriptionHelper.getDescriptionForType(annotations); } private void getSchemaDirectives(Schema schema, Set directiveClassNames, Annotations annotations, Directives directivesHelper, String schemaClassName) { schema.getDirectiveInstances() .addAll(directivesHelper .buildDirectiveInstances(annotations, "SCHEMA", schemaClassName) .stream() .map(directiveInstance -> { String directiveClassName = directiveInstance.getType().getClassName(); if (!directiveInstance.getType().isRepeatable() && !directiveClassNames.add(directiveClassName)) { throw new SchemaBuilderException("The @" + directiveInstance.getType().getName() + " directive is not repeatable, but was used more than once in the GraphQL" + " schema."); } return directiveInstance; }).collect(Collectors.toList())); } private boolean isGraphQLJavaDirective(ClassInfo classOfDirective) { return graphQLJavaDirectives().anyMatch(classOfDirective.name()::equals); } private Stream graphQLJavaDirectives() { // if in future there will be more directives supported by graphql-java return Stream.of(ONE_OF); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy