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}