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