001/*- 002 * #%L 003 * HAPI FHIR JPA Server 004 * %% 005 * Copyright (C) 2014 - 2024 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.gson.Gson; 035import com.google.gson.GsonBuilder; 036import graphql.ExecutionResult; 037import graphql.GraphQL; 038import graphql.language.InterfaceTypeDefinition; 039import graphql.scalar.GraphqlStringCoercing; 040import graphql.schema.GraphQLScalarType; 041import graphql.schema.GraphQLSchema; 042import graphql.schema.TypeResolver; 043import graphql.schema.TypeResolverProxy; 044import graphql.schema.idl.RuntimeWiring; 045import graphql.schema.idl.SchemaGenerator; 046import graphql.schema.idl.SchemaParser; 047import graphql.schema.idl.TypeDefinitionRegistry; 048import graphql.schema.idl.TypeRuntimeWiring; 049import jakarta.annotation.Nonnull; 050import jakarta.annotation.Nullable; 051import org.apache.commons.io.output.StringBuilderWriter; 052import org.apache.commons.lang3.Validate; 053import org.hl7.fhir.common.hapi.validation.validator.VersionSpecificWorkerContextWrapper; 054import org.hl7.fhir.instance.model.api.IIdType; 055import org.hl7.fhir.r5.model.Enumerations; 056import org.hl7.fhir.r5.model.SearchParameter; 057import org.hl7.fhir.r5.model.StructureDefinition; 058import org.hl7.fhir.r5.utils.GraphQLSchemaGenerator; 059import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; 060import org.slf4j.Logger; 061import org.slf4j.LoggerFactory; 062 063import java.io.IOException; 064import java.io.Writer; 065import java.util.ArrayList; 066import java.util.Collection; 067import java.util.Collections; 068import java.util.EnumSet; 069import java.util.HashSet; 070import java.util.List; 071import java.util.Map; 072import java.util.stream.Collectors; 073 074import static ca.uhn.fhir.util.MessageSupplier.msg; 075 076public class GraphQLProviderWithIntrospection extends GraphQLProvider { 077 078 private static final Logger ourLog = LoggerFactory.getLogger(GraphQLProviderWithIntrospection.class); 079 private final GraphQLSchemaGenerator myGenerator; 080 private final ISearchParamRegistry mySearchParamRegistry; 081 private final VersionSpecificWorkerContextWrapper myContext; 082 private final IDaoRegistry myDaoRegistry; 083 084 /** 085 * Constructor 086 */ 087 public GraphQLProviderWithIntrospection( 088 FhirContext theFhirContext, 089 IValidationSupport theValidationSupport, 090 IGraphQLStorageServices theIGraphQLStorageServices, 091 ISearchParamRegistry theSearchParamRegistry, 092 IDaoRegistry theDaoRegistry) { 093 super(theFhirContext, theValidationSupport, theIGraphQLStorageServices); 094 095 mySearchParamRegistry = theSearchParamRegistry; 096 myDaoRegistry = theDaoRegistry; 097 098 myContext = VersionSpecificWorkerContextWrapper.newVersionSpecificWorkerContextWrapper(theValidationSupport); 099 myGenerator = new GraphQLSchemaGenerator(myContext, VersionUtil.getVersion()); 100 } 101 102 @Override 103 public String processGraphQlGetRequest(ServletRequestDetails theRequestDetails, IIdType theId, String theQueryUrl) { 104 return super.processGraphQlGetRequest(theRequestDetails, theId, theQueryUrl); 105 } 106 107 @Override 108 public String processGraphQlPostRequest( 109 ServletRequestDetails theServletRequestDetails, 110 RequestDetails theRequestDetails, 111 IIdType theId, 112 String theQueryBody) { 113 if (theQueryBody.contains("__schema")) { 114 EnumSet<GraphQLSchemaGenerator.FHIROperationType> operations; 115 if (theId != null) { 116 throw new InvalidRequestException( 117 Msg.code(2035) 118 + "GraphQL introspection not supported at instance level. Please try at server- or instance- level."); 119 } 120 121 operations = EnumSet.of( 122 GraphQLSchemaGenerator.FHIROperationType.READ, GraphQLSchemaGenerator.FHIROperationType.SEARCH); 123 124 Collection<String> resourceTypes; 125 if (theRequestDetails.getResourceName() != null) { 126 resourceTypes = Collections.singleton(theRequestDetails.getResourceName()); 127 } else { 128 resourceTypes = new HashSet<>(); 129 for (String next : myContext.getResourceNames()) { 130 if (myDaoRegistry.isResourceTypeSupported(next)) { 131 resourceTypes.add(next); 132 } 133 } 134 resourceTypes = resourceTypes.stream().sorted().collect(Collectors.toList()); 135 } 136 137 return generateSchema(theQueryBody, resourceTypes, operations); 138 } else { 139 return super.processGraphQlPostRequest(theServletRequestDetails, theRequestDetails, theId, theQueryBody); 140 } 141 } 142 143 private String generateSchema( 144 String theQueryBody, 145 Collection<String> theResourceTypes, 146 EnumSet<GraphQLSchemaGenerator.FHIROperationType> theOperations) { 147 148 final StringBuilder schemaBuilder = new StringBuilder(); 149 try (Writer writer = new StringBuilderWriter(schemaBuilder)) { 150 151 // Generate FHIR base types schemas 152 myGenerator.generateTypes(writer, theOperations); 153 154 // Fix up a few things that are missing from the generated schema 155 writer.append("\ninterface Element {") 156 .append("\n id: ID") 157 .append("\n}") 158 .append("\n"); 159 // writer 160 // .append("\ninterface Quantity {\n") 161 // .append("id: String\n") 162 // .append("extension: [Extension]\n") 163 // .append("value: decimal _value: ElementBase\n") 164 // .append("comparator: code _comparator: ElementBase\n") 165 // .append("unit: String _unit: ElementBase\n") 166 // .append("system: uri _system: ElementBase\n") 167 // .append("code: code _code: ElementBase\n") 168 // .append("\n}") 169 // .append("\n"); 170 171 // writer 172 // .append("\ntype Resource {") 173 // .append("\n id: [token]" + "\n}") 174 // .append("\n"); 175 // writer 176 // .append("\ninput ResourceInput {") 177 // .append("\n id: [token]" + "\n}") 178 // .append("\n"); 179 180 // Generate schemas for the resource types 181 for (String nextResourceType : theResourceTypes) { 182 StructureDefinition sd = fetchStructureDefinition(nextResourceType); 183 List<SearchParameter> parameters = toR5SearchParams(mySearchParamRegistry 184 .getActiveSearchParams( 185 nextResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH) 186 .values()); 187 myGenerator.generateResource(writer, sd, parameters, theOperations); 188 } 189 190 // Generate queries 191 writer.append("\ntype Query {"); 192 for (String nextResourceType : theResourceTypes) { 193 if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.READ)) { 194 writer.append("\n ") 195 .append(nextResourceType) 196 .append("(id: String): ") 197 .append(nextResourceType) 198 .append("\n"); 199 } 200 if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.SEARCH)) { 201 List<SearchParameter> parameters = toR5SearchParams(mySearchParamRegistry 202 .getActiveSearchParams( 203 nextResourceType, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH) 204 .values()); 205 myGenerator.generateListAccessQuery(writer, parameters, nextResourceType); 206 myGenerator.generateConnectionAccessQuery(writer, parameters, nextResourceType); 207 } 208 } 209 writer.append("\n}"); 210 211 writer.flush(); 212 } catch (IOException e) { 213 throw new InternalErrorException(Msg.code(2036) + e.getMessage(), e); 214 } 215 216 String schema = schemaBuilder.toString().replace("\r", ""); 217 218 // Set these to INFO if you're testing, then set back before committing 219 ourLog.debug("Schema generated: {} chars", schema.length()); 220 ourLog.debug("Schema generated: {}", msg(() -> StringUtil.prependLineNumbers(schema))); 221 222 SchemaParser schemaParser = new SchemaParser(); 223 TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); 224 225 SchemaGenerator schemaGenerator = new SchemaGenerator(); 226 RuntimeWiring.Builder runtimeWiringBuilder = RuntimeWiring.newRuntimeWiring(); 227 for (String next : typeDefinitionRegistry.scalars().keySet()) { 228 if (Character.isUpperCase(next.charAt(0))) { 229 // Skip GraphQL built-in types 230 continue; 231 } 232 runtimeWiringBuilder.scalar(new GraphQLScalarType.Builder() 233 .name(next) 234 .coercing(new GraphqlStringCoercing()) 235 .build()); 236 } 237 238 for (InterfaceTypeDefinition next : typeDefinitionRegistry.getTypes(InterfaceTypeDefinition.class)) { 239 TypeResolver resolver = new TypeResolverProxy(); 240 TypeRuntimeWiring wiring = TypeRuntimeWiring.newTypeWiring(next.getName()) 241 .typeResolver(resolver) 242 .build(); 243 runtimeWiringBuilder.type(wiring); 244 } 245 246 RuntimeWiring runtimeWiring = runtimeWiringBuilder.build(); 247 GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); 248 249 GraphQL build = GraphQL.newGraphQL(graphQLSchema).build(); 250 ExecutionResult executionResult = build.execute(theQueryBody); 251 252 Map<String, Object> data = executionResult.toSpecification(); 253 Gson gson = new GsonBuilder().create(); 254 return gson.toJson(data); 255 } 256 257 @Nonnull 258 private List<SearchParameter> toR5SearchParams(Collection<RuntimeSearchParam> searchParams) { 259 List<SearchParameter> parameters = new ArrayList<>(); 260 for (RuntimeSearchParam next : searchParams) { 261 SearchParameter sp = toR5SearchParam(next); 262 if (sp != null) { 263 parameters.add(sp); 264 } 265 } 266 return parameters; 267 } 268 269 @Nullable 270 private SearchParameter toR5SearchParam(RuntimeSearchParam next) { 271 SearchParameter sp = new SearchParameter(); 272 sp.setUrl(next.getUri()); 273 sp.setCode(next.getName()); 274 sp.setName(next.getName()); 275 276 switch (next.getParamType()) { 277 case NUMBER: 278 sp.setType(Enumerations.SearchParamType.NUMBER); 279 break; 280 case DATE: 281 sp.setType(Enumerations.SearchParamType.DATE); 282 break; 283 case STRING: 284 sp.setType(Enumerations.SearchParamType.STRING); 285 break; 286 case TOKEN: 287 sp.setType(Enumerations.SearchParamType.TOKEN); 288 break; 289 case REFERENCE: 290 sp.setType(Enumerations.SearchParamType.REFERENCE); 291 break; 292 case COMPOSITE: 293 sp.setType(Enumerations.SearchParamType.COMPOSITE); 294 break; 295 case QUANTITY: 296 sp.setType(Enumerations.SearchParamType.QUANTITY); 297 break; 298 case URI: 299 sp.setType(Enumerations.SearchParamType.URI); 300 break; 301 case HAS: 302 case SPECIAL: 303 default: 304 return null; 305 } 306 return sp; 307 } 308 309 @Nonnull 310 private StructureDefinition fetchStructureDefinition(String resourceName) { 311 StructureDefinition retVal = myContext.fetchResource( 312 StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); 313 Validate.notNull(retVal); 314 return retVal; 315 } 316}