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