001package ca.uhn.fhir.jpa.graphql; 002 003/*- 004 * #%L 005 * HAPI FHIR Storage api 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import ca.uhn.fhir.i18n.Msg; 024import ca.uhn.fhir.context.ConfigurationException; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.context.FhirVersionEnum; 027import ca.uhn.fhir.context.support.IValidationSupport; 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.server.RestfulServer; 036import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException; 037import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 038import ca.uhn.fhir.rest.server.exceptions.UnclassifiedServerFailureException; 039import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 040import org.apache.commons.lang3.ObjectUtils; 041import org.apache.commons.lang3.Validate; 042import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; 043import org.hl7.fhir.instance.model.api.IBaseResource; 044import org.hl7.fhir.instance.model.api.IIdType; 045import org.hl7.fhir.utilities.graphql.IGraphQLEngine; 046import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices; 047import org.hl7.fhir.utilities.graphql.ObjectValue; 048import org.hl7.fhir.utilities.graphql.Parser; 049import org.slf4j.Logger; 050import org.slf4j.LoggerFactory; 051 052import javax.annotation.Nonnull; 053import javax.annotation.Nullable; 054import java.util.function.Supplier; 055 056public class GraphQLProvider { 057 private final Supplier<IGraphQLEngine> engineFactory; 058 private final IGraphQLStorageServices myStorageServices; 059 private Logger ourLog = LoggerFactory.getLogger(GraphQLProvider.class); 060 061 /** 062 * Constructor which uses a default context and validation support object 063 * 064 * @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine) 065 */ 066 public GraphQLProvider(IGraphQLStorageServices theStorageServices) { 067 this(FhirContext.forR4(), null, theStorageServices); 068 } 069 070 /** 071 * Constructor which uses the given worker context 072 * 073 * @param theFhirContext The HAPI FHIR Context object 074 * @param theValidationSupport The HAPI Validation Support object, or null 075 * @param theStorageServices The storage services (this object will be used to retrieve various resources as required by the GraphQL engine) 076 */ 077 public GraphQLProvider(@Nonnull FhirContext theFhirContext, @Nullable IValidationSupport theValidationSupport, @Nonnull IGraphQLStorageServices theStorageServices) { 078 Validate.notNull(theFhirContext, "theFhirContext must not be null"); 079 Validate.notNull(theStorageServices, "theStorageServices must not be null"); 080 081 switch (theFhirContext.getVersion().getVersion()) { 082 case DSTU3: { 083 IValidationSupport validationSupport = theValidationSupport; 084 validationSupport = ObjectUtils.defaultIfNull(validationSupport, new DefaultProfileValidationSupport(theFhirContext)); 085 org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext workerContext = new org.hl7.fhir.dstu3.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport); 086 engineFactory = () -> new org.hl7.fhir.dstu3.utils.GraphQLEngine(workerContext); 087 break; 088 } 089 case R4: { 090 IValidationSupport validationSupport = theValidationSupport; 091 validationSupport = ObjectUtils.defaultIfNull(validationSupport, new DefaultProfileValidationSupport(theFhirContext)); 092 org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext workerContext = new org.hl7.fhir.r4.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport); 093 engineFactory = () -> new org.hl7.fhir.r4.utils.GraphQLEngine(workerContext); 094 break; 095 } 096 case R5: { 097 IValidationSupport validationSupport = theValidationSupport; 098 validationSupport = ObjectUtils.defaultIfNull(validationSupport, new DefaultProfileValidationSupport(theFhirContext)); 099 org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext workerContext = new org.hl7.fhir.r5.hapi.ctx.HapiWorkerContext(theFhirContext, validationSupport); 100 engineFactory = () -> new org.hl7.fhir.r5.utils.GraphQLEngine(workerContext); 101 break; 102 } 103 case DSTU2: 104 case DSTU2_HL7ORG: 105 case DSTU2_1: 106 default: { 107 throw new UnsupportedOperationException(Msg.code(1143) + "GraphQL not supported for version: " + theFhirContext.getVersion().getVersion()); 108 } 109 } 110 111 myStorageServices = theStorageServices; 112 } 113 114 @Description(value="This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") 115 @GraphQL(type=RequestTypeEnum.GET) 116 public String processGraphQlGetRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryUrl String theQueryUrl) { 117 if (theQueryUrl != null) { 118 return processGraphQLRequest(theRequestDetails, theId, theQueryUrl); 119 } 120 throw new InvalidRequestException(Msg.code(1144) + "Unable to parse empty GraphQL expression"); 121 } 122 123 @Description(value="This operation invokes a GraphQL expression for fetching an joining a graph of resources, returning them in a custom format.") 124 @GraphQL(type=RequestTypeEnum.POST) 125 public String processGraphQlPostRequest(ServletRequestDetails theRequestDetails, @IdParam IIdType theId, @GraphQLQueryBody String theQueryBody) { 126 if (theQueryBody != null) { 127 return processGraphQLRequest(theRequestDetails, theId, theQueryBody); 128 } 129 throw new InvalidRequestException(Msg.code(1145) + "Unable to parse empty GraphQL expression"); 130 } 131 132 public String processGraphQLRequest(ServletRequestDetails theRequestDetails, IIdType theId, String theQuery) { 133 IGraphQLEngine engine = engineFactory.get(); 134 engine.setAppInfo(theRequestDetails); 135 engine.setServices(myStorageServices); 136 try { 137 engine.setGraphQL(Parser.parse(theQuery)); 138 } catch (Exception theE) { 139 throw new InvalidRequestException(Msg.code(1146) + "Unable to parse GraphQL Expression: " + theE.toString()); 140 } 141 142 try { 143 144 if (theId != null) { 145 IBaseResource focus = myStorageServices.lookup(theRequestDetails, theId.getResourceType(), theId.getIdPart()); 146 engine.setFocus(focus); 147 } 148 engine.execute(); 149 150 StringBuilder outputBuilder = new StringBuilder(); 151 ObjectValue output = engine.getOutput(); 152 output.write(outputBuilder, 0, "\n"); 153 154 return outputBuilder.toString(); 155 156 } catch (Exception e) { 157 StringBuilder b = new StringBuilder(); 158 b.append("Unable to execute GraphQL Expression: "); 159 int statusCode = 500; 160 if (e instanceof BaseServerResponseException) { 161 b.append("HTTP "); 162 statusCode = ((BaseServerResponseException) e).getStatusCode(); 163 b.append(statusCode); 164 b.append(" "); 165 } else { 166 // This means it's a bug, so let's log 167 ourLog.error("Failure during GraphQL processing", e); 168 } 169 b.append(e.getMessage()); 170 throw new UnclassifiedServerFailureException(statusCode, Msg.code(1147) + b.toString()); 171 } 172 } 173 174 @Initialize 175 public void initialize(RestfulServer theServer) { 176 ourLog.trace("Initializing GraphQL provider"); 177 if (!theServer.getFhirContext().getVersion().getVersion().isEqualOrNewerThan(FhirVersionEnum.DSTU3)) { 178 throw new ConfigurationException(Msg.code(1148) + "Can not use " + getClass().getName() + " provider on server with FHIR " + theServer.getFhirContext().getVersion().getVersion().name() + " context"); 179 } 180 } 181 182 183} 184