001// -------------------------------------------------------------------------------- 002// Copyright 2002-2026 Echo Three, LLC 003// 004// Licensed under the Apache License, Version 2.0 (the "License"); 005// you may not use this file except in compliance with the License. 006// You may obtain a copy of the License at 007// 008// http://www.apache.org/licenses/LICENSE-2.0 009// 010// Unless required by applicable law or agreed to in writing, software 011// distributed under the License is distributed on an "AS IS" BASIS, 012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013// See the License for the specific language governing permissions and 014// limitations under the License. 015// -------------------------------------------------------------------------------- 016 017package com.echothree.control.user.graphql.server.schema.util; 018 019import com.echothree.control.user.graphql.server.schema.GraphQlMutations; 020import com.echothree.control.user.graphql.server.schema.GraphQlQueries; 021import com.echothree.model.control.graphql.server.graphql.count.RelayWithCounting; 022import graphql.annotations.AnnotationsSchemaCreator; 023import graphql.annotations.annotationTypes.GraphQLUnion; 024import graphql.annotations.processor.GraphQLAnnotations; 025import graphql.schema.GraphQLSchema; 026import graphql.schema.GraphQLUnionType; 027import io.github.classgraph.ClassGraph; 028import java.util.List; 029 030public class GraphQlSchemaUtils { 031 032 private GraphQLSchema readOnlySchema; 033 private GraphQLSchema schema; 034 035 private GraphQlSchemaUtils() { 036 buildSchema(); 037 } 038 039 private static class GraphQlSchemaUtilsHolder { 040 static GraphQlSchemaUtils instance = new GraphQlSchemaUtils(); 041 } 042 043 public static GraphQlSchemaUtils getInstance() { 044 return GraphQlSchemaUtilsHolder.instance; 045 } 046 047 private GraphQLAnnotations createAnnotationsProcessor(List<Class<?>> unionTypes) { 048 var annotations = new GraphQLAnnotations(); 049 050 annotations.setRelay(new RelayWithCounting()); 051 annotations.getObjectHandler().getTypeRetriever().getGraphQLFieldRetriever().setAlwaysPrettify(true); 052 053 // Build unions before query roots so recursive member fields resolve to type references. 054 // The annotations library's UnionBuilder requires concrete possible types when the union completes. 055 for(var unionType : unionTypes) { 056 annotations.getObjectHandler().getGraphQLType(unionType, annotations.getContainer()); 057 } 058 059 return annotations; 060 } 061 062 private GraphQLSchema buildSchema(boolean readOnly, List<Class<?>> unionTypes) { 063 var annotations = createAnnotationsProcessor(unionTypes); 064 var schemaBuilder = GraphQLSchema.newSchema(); 065 066 annotations.getTypeRegistry().values().stream() 067 .filter(GraphQLUnionType.class::isInstance) 068 .forEach(schemaBuilder::additionalType); 069 070 var builder = AnnotationsSchemaCreator.newAnnotationsSchema() 071 .setAnnotationsProcessor(annotations) 072 .setGraphQLSchemaBuilder(schemaBuilder) 073 .setRelay(new RelayWithCounting()) 074 .query(GraphQlQueries.class) 075 .setAlwaysPrettify(true); 076 077 if(!readOnly) { 078 builder.mutation(GraphQlMutations.class); 079 } 080 081 return builder.build(); 082 } 083 084 private void buildSchema() { 085 try(var scan = new ClassGraph() 086 .acceptPackages("com.echothree.model.control") 087 .enableAnnotationInfo() 088 .scan()) { 089 var unionTypes = scan.getClassesWithAnnotation(GraphQLUnion.class.getName()).loadClasses(); 090 091 readOnlySchema = buildSchema(true, unionTypes); 092 schema = buildSchema(false, unionTypes); 093 } 094 } 095 096 public GraphQLSchema getReadOnlySchema() { 097 return readOnlySchema; 098 } 099 100 public GraphQLSchema getSchema() { 101 return schema; 102 } 103 104}