
001/*- 002 * #%L 003 * HAPI FHIR Storage api 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.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.FhirVersionEnum; 025import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; 026import ca.uhn.fhir.context.support.IValidationSupport; 027import ca.uhn.fhir.i18n.Msg; 028import ca.uhn.fhir.model.api.annotation.Description; 029import ca.uhn.fhir.rest.annotation.GraphQL; 030import ca.uhn.fhir.rest.annotation.GraphQLQueryBody; 031import ca.uhn.fhir.rest.annotation.GraphQLQueryUrl; 032import ca.uhn.fhir.rest.annotation.IdParam; 033import ca.uhn.fhir.rest.annotation.Initialize; 034import ca.uhn.fhir.rest.api.RequestTypeEnum; 035import ca.uhn.fhir.rest.api.server.RequestDetails; 036import ca.uhn.fhir.rest.server.RestfulServer; 037import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 038import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 039import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; 040import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 041import jakarta.annotation.Nonnull; 042import jakarta.annotation.Nullable; 043import org.apache.commons.lang3.ObjectUtils; 044import org.apache.commons.lang3.Validate; 045import org.hl7.fhir.dstu3.support.utils.GraphQLEngine; 046import org.hl7.fhir.instance.model.api.IBaseResource; 047import org.hl7.fhir.instance.model.api.IIdType; 048import org.hl7.fhir.utilities.graphql.IGraphQLEngine; 049import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; 050import org.hl7.fhir.utilities.graphql.ObjectValue; 051import org.hl7.fhir.utilities.graphql.Package; 052import org.hl7.fhir.utilities.graphql.Parser; 053import org.slf4j.Logger; 054import org.slf4j.LoggerFactory; 055 056import java.util.function.Supplier; 057 058public class GraphQLProvider { 059 private static final Logger ourLog = LoggerFactory.getLogger(GraphQLProvider.class); 060 061 private final Supplier<IGraphQLEngine> myEngineFactory; 062 private final IGraphQLStorageServices myStorageServices; 063 064 /** 065 * Constructor which uses a default context and validation support object 066 * 067 * @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine) 068 */ 069 public GraphQLProvider(IGraphQLStorageServices theStorageServices) { 070 this(FhirContext.forR4(), null, theStorageServices); 071 } 072 073 /** 074 * Constructor which uses the given worker context 075 * 076 * @param theFhirContext The HAPI FHIR Context object 077 * @param theValidationSupport The HAPI Validation Support object, or null 078 * @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine) 079 */ 080 public GraphQLProvider( 081 @Nonnull FhirContext theFhirContext, 082 @Nullable IValidationSupport theValidationSupport, 083 @Nonnull IGraphQLStorageServices theStorageServices) { 084 Validate.notNull(theFhirContext, "theFhirContext must not be null"); 085 Validate.notNull(theStorageServices, "theStorageServices must not be null"); 086 087 switch (theFhirContext.getVersion().getVersion()) { 088 case DSTU3: { 089 IValidationSupport validationSupport = theValidationSupport; 090 validationSupport = ObjectUtils.defaultIfNull( 091 validationSupport, new DefaultProfileValidationSupport(theFhirContext)); 092 org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext workerContext = 093 new org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport); 094 myEngineFactory = () -> new GraphQLEngine(workerContext); 095 break; 096 } 097 case R4: { 098 IValidationSupport validationSupport = theValidationSupport; 099 validationSupport = ObjectUtils.defaultIfNull( 100 validationSupport, new DefaultProfileValidationSupport(theFhirContext)); 101 org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext workerContext = 102 new org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport); 103 myEngineFactory = () -> new org.hl7.fhir.r4.utils.GraphQLEngine(workerContext); 104 break; 105 } 106 case R4B: { 107 IValidationSupport validationSupport = theValidationSupport; 108 validationSupport = ObjectUtils.defaultIfNull( 109 validationSupport, new DefaultProfileValidationSupport(theFhirContext)); 110 org.hl7.fhir.r4b.hapi.ctx.HapiWorkerContext workerContext = 111 new org.hl7.fhir.r4b.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport); 112 myEngineFactory = () -> new org.hl7.fhir.r4b.utils.GraphQLEngine(workerContext); 113 break; 114 } 115 case R5: { 116 IValidationSupport validationSupport = theValidationSupport; 117 validationSupport = ObjectUtils.defaultIfNull( 118 validationSupport, new DefaultProfileValidationSupport(theFhirContext)); 119 org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext workerContext = 120 new org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport); 121 myEngineFactory = () -> new org.hl7.fhir.r5.utils.GraphQLEngine(workerContext); 122 break; 123 } 124 case DSTU2: 125 case DSTU2_HL7ORG: 126 case DSTU2_1: 127 default: { 128 throw new UnsupportedOperationException(Msg.code(1143) + "GraphQL not supported for version: " 129 + theFhirContext.getVersion().getVersion()); 130 } 131 } 132 133 myStorageServices = theStorageServices; 134 } 135 136 @Description( 137 value = 138 "This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") 139 @GraphQL(type = RequestTypeEnum.GET) 140 public String processGraphQlGetRequest( 141 ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String theQueryUrl) { 142 if (theQueryUrl != null) { 143 return processGraphQLRequest(theRequestDetails, theId, theQueryUrl); 144 } 145 throw new InvalidRequestException(Msg.code(1144) + "Unable to parse empty GraphQL expression"); 146 } 147 148 @Description( 149 value = 150 "This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") 151 @GraphQL(type = RequestTypeEnum.POST) 152 public String processGraphQlPostRequest( 153 ServletRequestDetails theServletRequestDetails, 154 RequestDetails theRequestDetails, 155 @IdParam IIdType theId, 156 @GraphQLQueryBody String theQueryBody) { 157 if (theQueryBody != null) { 158 return processGraphQLRequest(theServletRequestDetails, theId, theQueryBody); 159 } 160 throw new InvalidRequestException(Msg.code(1145) + "Unable to parse empty GraphQL expression"); 161 } 162 163 public String processGraphQLRequest(ServletRequestDetails theRequestDetails, IIdType theId, String theQuery) { 164 Package parsedGraphQLRequest; 165 try { 166 parsedGraphQLRequest = Parser.parse(theQuery); 167 } catch (Exception e) { 168 throw new InvalidRequestException(Msg.code(1146) + "Unable to parse GraphQL Expression: " + e); 169 } 170 171 return processGraphQLRequest(theRequestDetails, theId, parsedGraphQLRequest); 172 } 173 174 protected String processGraphQLRequest( 175 ServletRequestDetails theRequestDetails, IIdType theId, Package parsedGraphQLRequest) { 176 IGraphQLEngine engine = myEngineFactory.get(); 177 engine.setAppInfo(theRequestDetails); 178 engine.setServices(myStorageServices); 179 engine.setGraphQL(parsedGraphQLRequest); 180 181 try { 182 183 if (theId != null) { 184 IBaseResource focus = 185 myStorageServices.lookup(theRequestDetails, theId.getResourceType(), theId.getIdPart()); 186 engine.setFocus(focus); 187 } 188 engine.execute(); 189 190 StringBuilder outputBuilder = new StringBuilder(); 191 ObjectValue output = engine.getOutput(); 192 output.write(outputBuilder, 0, "\n"); 193 194 return outputBuilder.toString(); 195 196 } catch (Exception e) { 197 StringBuilder b = new StringBuilder(); 198 b.append("Unable to execute GraphQL Expression: "); 199 int statusCode = 500; 200 if (e instanceof BaseServerResponseException) { 201 b.append("HTTP "); 202 statusCode = ((BaseServerResponseException) e).getStatusCode(); 203 b.append(statusCode); 204 b.append(" "); 205 } else { 206 // This means it's a bug, so let's log 207 ourLog.error("Failure during GraphQL processing", e); 208 } 209 b.append(e.getMessage()); 210 throw new UnclassifiedServerFailureException(statusCode, Msg.code(1147) + b); 211 } 212 } 213 214 @Initialize 215 public void initialize(RestfulServer theServer) { 216 ourLog.trace("Initializing GraphQL provider"); 217 if (!theServer.getFhirContext().getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { 218 throw new ConfigurationException(Msg.code(1148) + "Can not use " 219 + getClass().getName() + " provider on server with FHIR " 220 + theServer.getFhirContext().getVersion().getVersion().name() + " context"); 221 } 222 } 223}