001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.graphql; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeSearchParam; 024import ca.uhn.fhir.context.support.IValidationSupport; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.jpa.api.IDaoRegistry; 027import ca.uhn.fhir.rest.api.server.RequestDetails; 028import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 029import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 030import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 031import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 032import ca.uhn.fhir.util.StringUtil; 033import ca.uhn.fhir.util.VersionUtil; 034import com.google.common.base.Supplier; 035import com.google.common.base.Suppliers; 036import com.google.gson.Gson; 037import com.google.gson.GsonBuilder; 038import com.google.gson.JsonArray; 039import com.google.gson.JsonSerializer; 040import graphql.ExecutionResult; 041import graphql.GraphQL; 042import graphql.language.InterfaceTypeDefinition; 043import graphql.scalar.GraphqlStringCoercing; 044import graphql.schema.GraphQLScalarType; 045import graphql.schema.GraphQLSchema; 046import graphql.schema.TypeResolver; 047import graphql.schema.TypeResolverProxy; 048import graphql.schema.idl.RuntimeWiring; 049import graphql.schema.idl.SchemaGenerator; 050import graphql.schema.idl.SchemaParser; 051import graphql.schema.idl.TypeDefinitionRegistry; 052import graphql.schema.idl.TypeRuntimeWiring; 053import jakarta.annotation.Nonnull; 054import jakarta.annotation.Nullable; 055import org.apache.commons.io.output.StringBuilderWriter; 056import org.apache.commons.lang3.Validate; 057import org.hl7.fhir.common.hapi.validation.validator.VersionSpecificWorkerContextWrapper; 058import org.hl7.fhir.instance.model.api.IIdType; 059import org.hl7.fhir.r5.model.Enumerations; 060import org.hl7.fhir.r5.model.SearchParameter; 061import org.hl7.fhir.r5.model.StructureDefinition; 062import org.hl7.fhir.r5.utils.GraphQLSchemaGenerator; 063import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; 064import org.slf4j.Logger; 065import org.slf4j.LoggerFactory; 066 067import java.io.IOException; 068import java.io.Writer; 069import java.util.ArrayList; 070import java.util.Collection; 071import java.util.Collections; 072import java.util.EnumSet; 073import java.util.HashSet; 074import java.util.List; 075import java.util.Map; 076import java.util.stream.Collectors; 077 078import static ca.uhn.fhir.util.MessageSupplier.msg; 079 080public class GraphQLProviderWithIntrospection extends GraphQLProvider { 081 082 private static final Logger ourLog = LoggerFactory.getLogger(GraphQLProviderWithIntrospection.class); 083 private final Supplier<GraphQLSchemaGenerator> myGenerator; 084 private final ISearchParamRegistry mySearchParamRegistry; 085 private final VersionSpecificWorkerContextWrapper myContext; 086 private final IDaoRegistry myDaoRegistry; 087 private final Gson myGson; 088 089 /** 090 * Constructor 091 */ 092 public GraphQLProviderWithIntrospection( 093 FhirContext theFhirContext, 094 IValidationSupport theValidationSupport, 095 IGraphQLStorageServices theIGraphQLStorageServices, 096 ISearchParamRegistry theSearchParamRegistry, 097 IDaoRegistry theDaoRegistry) { 098 super(theFhirContext, theValidationSupport, theIGraphQLStorageServices); 099 100 mySearchParamRegistry = theSearchParamRegistry; 101 myDaoRegistry = theDaoRegistry; 102 103 myContext = VersionSpecificWorkerContextWrapper.newVersionSpecificWorkerContextWrapper(theValidationSupport); 104 105 GsonBuilder gsonBuilder = new GsonBuilder(); 106 gsonBuilder.registerTypeAdapter(Collections.emptyList().getClass(), (JsonSerializer<Object>) 107 (src, typeOfSrc, context) -> new JsonArray()); 108 myGson = gsonBuilder.create(); 109 110 // Lazy-load this because it's expensive, but more importantly because it makes a bunch of 111 // calls for StructureDefinitions and other such things during startup so we want to be sure 112 // that everything else is initialized first 113 myGenerator = Suppliers.memoize(() -> new GraphQLSchemaGenerator(myContext, VersionUtil.getVersion())); 114 } 115 116 @Override 117 public String processGraphQlGetRequest(ServletRequestDetails theRequestDetails, IIdType theId, String theQueryUrl) { 118 return super.processGraphQlGetRequest(theRequestDetails, theId, theQueryUrl); 119 } 120 121 @Override 122 public String processGraphQlPostRequest( 123 ServletRequestDetails theServletRequestDetails, 124 RequestDetails theRequestDetails, 125 IIdType theId, 126 String theQueryBody) { 127 if (theQueryBody.contains("__schema")) { 128 EnumSet<GraphQLSchemaGenerator.FHIROperationType> operations; 129 if (theId != null) { 130 throw new InvalidRequestException( 131 Msg.code(2035) 132 + "GraphQL introspection not supported at instance level. Please try at server- or instance- level."); 133 } 134 135 operations = EnumSet.of( 136 GraphQLSchemaGenerator.FHIROperationType.READ, GraphQLSchemaGenerator.FHIROperationType.SEARCH); 137 138 Collection<String> resourceTypes; 139 if (theRequestDetails.getResourceName() != null) { 140 resourceTypes = Collections.singleton(theRequestDetails.getResourceName()); 141 } else { 142 resourceTypes = new HashSet<>(); 143 for (String next : myContext.getResourceNames()) { 144 if (myDaoRegistry.isResourceTypeSupported(next)) { 145 resourceTypes.add(next); 146 } 147 } 148 resourceTypes = resourceTypes.stream().sorted().collect(Collectors.toList()); 149 } 150 151 return generateSchema(theQueryBody, resourceTypes, operations); 152 } else { 153 return super.processGraphQlPostRequest(theServletRequestDetails, theRequestDetails, theId, theQueryBody); 154 } 155 } 156 157 private String generateSchema( 158 String theQueryBody, 159 Collection<String> theResourceTypes, 160 EnumSet<GraphQLSchemaGenerator.FHIROperationType> theOperations) { 161 162 GraphQLSchemaGenerator generator = myGenerator.get(); 163 164 final StringBuilder schemaBuilder = new StringBuilder(); 165 try (Writer writer = new StringBuilderWriter(schemaBuilder)) { 166 167 // Generate FHIR base types schemas 168 generator.generateTypes(writer, theOperations); 169 170 // Fix up a few things that are missing from the generated schema 171 writer.append("\ninterface Element {") 172 .append("\n id: ID") 173 .append("\n}") 174 .append("\n"); 175 176 // Generate schemas for the resource types 177 for (String nextResourceType : theResourceTypes) { 178 StructureDefinition sd = fetchStructureDefinition(nextResourceType); 179 List<SearchParameter> parameters = toR5SearchParams(mySearchParamRegistry 180 .getActiveSearchParams( 181 nextResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH) 182 .values()); 183 generator.generateResource(writer, sd, parameters, theOperations); 184 } 185 186 // Generate queries 187 writer.append("\ntype Query {"); 188 for (String nextResourceType : theResourceTypes) { 189 if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.READ)) { 190 writer.append("\n ") 191 .append(nextResourceType) 192 .append("(id: String): ") 193 .append(nextResourceType) 194 .append("\n"); 195 } 196 if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.SEARCH)) { 197 List<SearchParameter> parameters = toR5SearchParams(mySearchParamRegistry 198 .getActiveSearchParams( 199 nextResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH) 200 .values()); 201 generator.generateListAccessQuery(writer, parameters, nextResourceType); 202 generator.generateConnectionAccessQuery(writer, parameters, nextResourceType); 203 } 204 } 205 writer.append("\n}"); 206 207 writer.flush(); 208 } catch (IOException e) { 209 throw new InternalErrorException(Msg.code(2036) + e.getMessage(), e); 210 } 211 212 String schema = schemaBuilder.toString().replace("\r", ""); 213 214 // Set these to INFO if you're testing, then set back before committing 215 ourLog.debug("Schema generated: {} chars", schema.length()); 216 ourLog.debug("Schema generated: {}", msg(() -> StringUtil.prependLineNumbers(schema))); 217 218 SchemaParser schemaParser = new SchemaParser(); 219 TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); 220 221 SchemaGenerator schemaGenerator = new SchemaGenerator(); 222 RuntimeWiring.Builder runtimeWiringBuilder = RuntimeWiring.newRuntimeWiring(); 223 for (String next : typeDefinitionRegistry.scalars().keySet()) { 224 if (Character.isUpperCase(next.charAt(0))) { 225 // Skip GraphQL built-in types 226 continue; 227 } 228 runtimeWiringBuilder.scalar(new GraphQLScalarType.Builder() 229 .name(next) 230 .coercing(new GraphqlStringCoercing()) 231 .build()); 232 } 233 234 for (InterfaceTypeDefinition next : typeDefinitionRegistry.getTypes(InterfaceTypeDefinition.class)) { 235 TypeResolver resolver = new TypeResolverProxy(); 236 TypeRuntimeWiring wiring = TypeRuntimeWiring.newTypeWiring(next.getName()) 237 .typeResolver(resolver) 238 .build(); 239 runtimeWiringBuilder.type(wiring); 240 } 241 242 RuntimeWiring runtimeWiring = runtimeWiringBuilder.build(); 243 GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); 244 245 GraphQL build = GraphQL.newGraphQL(graphQLSchema).build(); 246 ExecutionResult executionResult = build.execute(theQueryBody); 247 248 Map<String, Object> data = executionResult.toSpecification(); 249 return myGson.toJson(data); 250 } 251 252 @Nonnull 253 private List<SearchParameter> toR5SearchParams(Collection<RuntimeSearchParam> searchParams) { 254 List<SearchParameter> parameters = new ArrayList<>(); 255 for (RuntimeSearchParam next : searchParams) { 256 SearchParameter sp = toR5SearchParam(next); 257 if (sp != null) { 258 parameters.add(sp); 259 } 260 } 261 return parameters; 262 } 263 264 @Nullable 265 private SearchParameter toR5SearchParam(RuntimeSearchParam next) { 266 SearchParameter sp = new SearchParameter(); 267 sp.setUrl(next.getUri()); 268 sp.setCode(next.getName()); 269 sp.setName(next.getName()); 270 271 switch (next.getParamType()) { 272 case NUMBER: 273 sp.setType(Enumerations.SearchParamType.NUMBER); 274 break; 275 case DATE: 276 sp.setType(Enumerations.SearchParamType.DATE); 277 break; 278 case STRING: 279 sp.setType(Enumerations.SearchParamType.STRING); 280 break; 281 case TOKEN: 282 sp.setType(Enumerations.SearchParamType.TOKEN); 283 break; 284 case REFERENCE: 285 sp.setType(Enumerations.SearchParamType.REFERENCE); 286 break; 287 case COMPOSITE: 288 sp.setType(Enumerations.SearchParamType.COMPOSITE); 289 break; 290 case QUANTITY: 291 sp.setType(Enumerations.SearchParamType.QUANTITY); 292 break; 293 case URI: 294 sp.setType(Enumerations.SearchParamType.URI); 295 break; 296 case HAS: 297 case SPECIAL: 298 default: 299 return null; 300 } 301 return sp; 302 } 303 304 @Nonnull 305 private StructureDefinition fetchStructureDefinition(String resourceName) { 306 StructureDefinition retVal = myContext.fetchResource( 307 StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); 308 Validate.notNull(retVal); 309 return retVal; 310 } 311}