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}