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(nextResourceType) 185 .values()); 186 myGenerator.generateResource(writer, sd, parameters, theOperations); 187 } 188 189 // Generate queries 190 writer.append("\ntype Query {"); 191 for (String nextResourceType : theResourceTypes) { 192 if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.READ)) { 193 writer.append("\n ") 194 .append(nextResourceType) 195 .append("(id: String): ") 196 .append(nextResourceType) 197 .append("\n"); 198 } 199 if (theOperations.contains(GraphQLSchemaGenerator.FHIROperationType.SEARCH)) { 200 List<SearchParameter> parameters = toR5SearchParams(mySearchParamRegistry 201 .getActiveSearchParams(nextResourceType) 202 .values()); 203 myGenerator.generateListAccessQuery(writer, parameters, nextResourceType); 204 myGenerator.generateConnectionAccessQuery(writer, parameters, nextResourceType); 205 } 206 } 207 writer.append("\n}"); 208 209 writer.flush(); 210 } catch (IOException e) { 211 throw new InternalErrorException(Msg.code(2036) + e.getMessage(), e); 212 } 213 214 String schema = schemaBuilder.toString().replace("\r", ""); 215 216 // Set these to INFO if you're testing, then set back before committing 217 ourLog.debug("Schema generated: {} chars", schema.length()); 218 ourLog.debug("Schema generated: {}", msg(() -> StringUtil.prependLineNumbers(schema))); 219 220 SchemaParser schemaParser = new SchemaParser(); 221 TypeDefinitionRegistry typeDefinitionRegistry = schemaParser.parse(schema); 222 223 SchemaGenerator schemaGenerator = new SchemaGenerator(); 224 RuntimeWiring.Builder runtimeWiringBuilder = RuntimeWiring.newRuntimeWiring(); 225 for (String next : typeDefinitionRegistry.scalars().keySet()) { 226 if (Character.isUpperCase(next.charAt(0))) { 227 // Skip GraphQL built-in types 228 continue; 229 } 230 runtimeWiringBuilder.scalar(new GraphQLScalarType.Builder() 231 .name(next) 232 .coercing(new GraphqlStringCoercing()) 233 .build()); 234 } 235 236 for (InterfaceTypeDefinition next : typeDefinitionRegistry.getTypes(InterfaceTypeDefinition.class)) { 237 TypeResolver resolver = new TypeResolverProxy(); 238 TypeRuntimeWiring wiring = TypeRuntimeWiring.newTypeWiring(next.getName()) 239 .typeResolver(resolver) 240 .build(); 241 runtimeWiringBuilder.type(wiring); 242 } 243 244 RuntimeWiring runtimeWiring = runtimeWiringBuilder.build(); 245 GraphQLSchema graphQLSchema = schemaGenerator.makeExecutableSchema(typeDefinitionRegistry, runtimeWiring); 246 247 GraphQL build = GraphQL.newGraphQL(graphQLSchema).build(); 248 ExecutionResult executionResult = build.execute(theQueryBody); 249 250 Map<String, Object> data = executionResult.toSpecification(); 251 Gson gson = new GsonBuilder().create(); 252 return gson.toJson(data); 253 } 254 255 @Nonnull 256 private List<SearchParameter> toR5SearchParams(Collection<RuntimeSearchParam> searchParams) { 257 List<SearchParameter> parameters = new ArrayList<>(); 258 for (RuntimeSearchParam next : searchParams) { 259 SearchParameter sp = toR5SearchParam(next); 260 if (sp != null) { 261 parameters.add(sp); 262 } 263 } 264 return parameters; 265 } 266 267 @Nullable 268 private SearchParameter toR5SearchParam(RuntimeSearchParam next) { 269 SearchParameter sp = new SearchParameter(); 270 sp.setUrl(next.getUri()); 271 sp.setCode(next.getName()); 272 sp.setName(next.getName()); 273 274 switch (next.getParamType()) { 275 case NUMBER: 276 sp.setType(Enumerations.SearchParamType.NUMBER); 277 break; 278 case DATE: 279 sp.setType(Enumerations.SearchParamType.DATE); 280 break; 281 case STRING: 282 sp.setType(Enumerations.SearchParamType.STRING); 283 break; 284 case TOKEN: 285 sp.setType(Enumerations.SearchParamType.TOKEN); 286 break; 287 case REFERENCE: 288 sp.setType(Enumerations.SearchParamType.REFERENCE); 289 break; 290 case COMPOSITE: 291 sp.setType(Enumerations.SearchParamType.COMPOSITE); 292 break; 293 case QUANTITY: 294 sp.setType(Enumerations.SearchParamType.QUANTITY); 295 break; 296 case URI: 297 sp.setType(Enumerations.SearchParamType.URI); 298 break; 299 case HAS: 300 case SPECIAL: 301 default: 302 return null; 303 } 304 return sp; 305 } 306 307 @Nonnull 308 private StructureDefinition fetchStructureDefinition(String resourceName) { 309 StructureDefinition retVal = myContext.fetchResource( 310 StructureDefinition.class, "http://hl7.org/fhir/StructureDefinition/" + resourceName); 311 Validate.notNull(retVal); 312 return retVal; 313 } 314}