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.context.FhirContext;
024import ca.uhn.fhir.context.RuntimeResourceDefinition;
025import ca.uhn.fhir.context.RuntimeSearchParam;
026import ca.uhn.fhir.i18n.Msg;
027import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
028import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
029import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
030import ca.uhn.fhir.model.api.IQueryParameterOr;
031import ca.uhn.fhir.rest.api.server.IBundleProvider;
032import ca.uhn.fhir.rest.api.server.RequestDetails;
033import ca.uhn.fhir.rest.param.DateOrListParam;
034import ca.uhn.fhir.rest.param.DateParam;
035import ca.uhn.fhir.rest.param.NumberOrListParam;
036import ca.uhn.fhir.rest.param.NumberParam;
037import ca.uhn.fhir.rest.param.QuantityOrListParam;
038import ca.uhn.fhir.rest.param.QuantityParam;
039import ca.uhn.fhir.rest.param.ReferenceOrListParam;
040import ca.uhn.fhir.rest.param.ReferenceParam;
041import ca.uhn.fhir.rest.param.SpecialOrListParam;
042import ca.uhn.fhir.rest.param.SpecialParam;
043import ca.uhn.fhir.rest.param.StringOrListParam;
044import ca.uhn.fhir.rest.param.StringParam;
045import ca.uhn.fhir.rest.param.TokenOrListParam;
046import ca.uhn.fhir.rest.param.TokenParam;
047import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
048import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
049import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
050import org.apache.commons.lang3.Validate;
051import org.hl7.fhir.exceptions.FHIRException;
052import org.hl7.fhir.instance.model.api.IBaseBundle;
053import org.hl7.fhir.instance.model.api.IBaseReference;
054import org.hl7.fhir.instance.model.api.IBaseResource;
055import org.hl7.fhir.instance.model.api.IIdType;
056import org.hl7.fhir.utilities.graphql.Argument;
057import org.hl7.fhir.utilities.graphql.IGraphQLStorageServices;
058import org.hl7.fhir.utilities.graphql.Value;
059import org.springframework.beans.factory.annotation.Autowired;
060import org.springframework.transaction.annotation.Propagation;
061import org.springframework.transaction.annotation.Transactional;
062
063import java.util.List;
064import java.util.Map;
065import java.util.Set;
066import java.util.TreeSet;
067import java.util.stream.Collectors;
068
069import static ca.uhn.fhir.rest.api.Constants.PARAM_FILTER;
070
071public class DaoRegistryGraphQLStorageServices implements IGraphQLStorageServices {
072
073        private static final int MAX_SEARCH_SIZE = 500;
074        @Autowired
075        private FhirContext myContext;
076        @Autowired
077        private DaoRegistry myDaoRegistry;
078        @Autowired
079        private ISearchParamRegistry mySearchParamRegistry;
080
081        private IFhirResourceDao<? extends IBaseResource> getDao(String theResourceType) {
082                RuntimeResourceDefinition typeDef = myContext.getResourceDefinition(theResourceType);
083                return myDaoRegistry.getResourceDaoOrNull(typeDef.getImplementingClass());
084        }
085
086        private String graphqlArgumentToSearchParam(String name) {
087                if (name.startsWith("_")) {
088                        return name;
089                } else {
090                        return name.replaceAll("_", "-");
091                }
092        }
093
094        private String searchParamToGraphqlArgument(String name) {
095                return name.replaceAll("-", "_");
096        }
097
098        @Transactional(propagation = Propagation.NEVER)
099        @Override
100        public void listResources(Object theAppInfo, String theType, List<Argument> theSearchParams, List<IBaseResource> theMatches) throws FHIRException {
101                FhirContext fhirContext = myContext;
102                RuntimeResourceDefinition typeDef = fhirContext.getResourceDefinition(theType);
103                IFhirResourceDao<? extends IBaseResource> dao = myDaoRegistry.getResourceDao(typeDef.getImplementingClass());
104
105                SearchParameterMap params = new SearchParameterMap();
106                params.setLoadSynchronousUpTo(MAX_SEARCH_SIZE);
107
108                Map<String, RuntimeSearchParam> searchParams = mySearchParamRegistry.getActiveSearchParams(typeDef.getName());
109
110                for (Argument nextArgument : theSearchParams) {
111
112                        if (nextArgument.getName().equals(PARAM_FILTER)) {
113                                String value = nextArgument.getValues().get(0).getValue();
114                                params.add(PARAM_FILTER, new StringParam(value));
115                                continue;
116                        }
117
118                        String searchParamName = graphqlArgumentToSearchParam(nextArgument.getName());
119                        RuntimeSearchParam searchParam = searchParams.get(searchParamName);
120                        if (searchParam == null) {
121                                Set<String> graphqlArguments = searchParams.keySet().stream()
122                                        .map(this::searchParamToGraphqlArgument)
123                                        .collect(Collectors.toSet());
124                                String msg = myContext.getLocalizer().getMessageSanitized(DaoRegistryGraphQLStorageServices.class, "invalidGraphqlArgument", nextArgument.getName(), new TreeSet<>(graphqlArguments));
125                                throw new InvalidRequestException(Msg.code(1275) + msg);
126                        }
127
128                        IQueryParameterOr<?> queryParam;
129
130                        switch (searchParam.getParamType()) {
131                                case NUMBER:
132                                        NumberOrListParam numberOrListParam = new NumberOrListParam();
133                                        for (Value value : nextArgument.getValues()) {
134                                                numberOrListParam.addOr(new NumberParam(value.getValue()));
135                                        }
136                                        queryParam = numberOrListParam;
137                                        break;
138                                case DATE:
139                                        DateOrListParam dateOrListParam = new DateOrListParam();
140                                        for (Value value : nextArgument.getValues()) {
141                                                dateOrListParam.addOr(new DateParam(value.getValue()));
142                                        }
143                                        queryParam = dateOrListParam;
144                                        break;
145                                case STRING:
146                                        StringOrListParam stringOrListParam = new StringOrListParam();
147                                        for (Value value : nextArgument.getValues()) {
148                                                stringOrListParam.addOr(new StringParam(value.getValue()));
149                                        }
150                                        queryParam = stringOrListParam;
151                                        break;
152                                case TOKEN:
153                                        TokenOrListParam tokenOrListParam = new TokenOrListParam();
154                                        for (Value value : nextArgument.getValues()) {
155                                                TokenParam tokenParam = new TokenParam();
156                                                tokenParam.setValueAsQueryToken(fhirContext, searchParamName, null, value.getValue());
157                                                tokenOrListParam.addOr(tokenParam);
158                                        }
159                                        queryParam = tokenOrListParam;
160                                        break;
161                                case REFERENCE:
162                                        ReferenceOrListParam referenceOrListParam = new ReferenceOrListParam();
163                                        for (Value value : nextArgument.getValues()) {
164                                                referenceOrListParam.addOr(new ReferenceParam(value.getValue()));
165                                        }
166                                        queryParam = referenceOrListParam;
167                                        break;
168                                case QUANTITY:
169                                        QuantityOrListParam quantityOrListParam = new QuantityOrListParam();
170                                        for (Value value : nextArgument.getValues()) {
171                                                quantityOrListParam.addOr(new QuantityParam(value.getValue()));
172                                        }
173                                        queryParam = quantityOrListParam;
174                                        break;
175                                case SPECIAL:
176                                        SpecialOrListParam specialOrListParam = new SpecialOrListParam();
177                                        for (Value value : nextArgument.getValues()) {
178                                                specialOrListParam.addOr(new SpecialParam().setValue(value.getValue()));
179                                        }
180                                        queryParam = specialOrListParam;
181                                        break;
182                                case COMPOSITE:
183                                case URI:
184                                case HAS:
185                                default:
186                                        throw new InvalidRequestException(Msg.code(1276) + String.format("%s parameters are not yet supported in GraphQL", searchParam.getParamType()));
187                        }
188
189                        params.add(searchParamName, queryParam);
190                }
191
192                RequestDetails requestDetails = (RequestDetails) theAppInfo;
193                IBundleProvider response = dao.search(params, requestDetails);
194                Integer size = response.size();
195                //We set size to null in SearchCoordinatorSvcImpl.executeQuery() if matching results exceeds count
196                //so don't throw here
197                if ((response.preferredPageSize() != null && size != null && response.preferredPageSize() < size) ||
198                        size == null) {
199                        size = response.preferredPageSize();
200                }
201
202                Validate.notNull(size, "size is null");
203                theMatches.addAll(response.getResources(0, size));
204
205        }
206
207        @Transactional(propagation = Propagation.REQUIRED)
208        @Override
209        public IBaseResource lookup(Object theAppInfo, String theType, String theId) throws FHIRException {
210                IIdType refId = myContext.getVersion().newIdType();
211                refId.setValue(theType + "/" + theId);
212                return lookup(theAppInfo, refId);
213        }
214
215        private IBaseResource lookup(Object theAppInfo, IIdType theRefId) {
216                IFhirResourceDao<? extends IBaseResource> dao = getDao(theRefId.getResourceType());
217                RequestDetails requestDetails = (RequestDetails) theAppInfo;
218                return dao.read(theRefId, requestDetails, false);
219        }
220
221        @Transactional(propagation = Propagation.REQUIRED)
222        @Override
223        public ReferenceResolution lookup(Object theAppInfo, IBaseResource theContext, IBaseReference theReference) throws FHIRException {
224                IBaseResource outcome = lookup(theAppInfo, theReference.getReferenceElement());
225                if (outcome == null) {
226                        return null;
227                }
228                return new ReferenceResolution(theContext, outcome);
229        }
230
231        @Transactional(propagation = Propagation.NEVER)
232        @Override
233        public IBaseBundle search(Object theAppInfo, String theType, List<Argument> theSearchParams) throws FHIRException {
234                throw new NotImplementedOperationException(Msg.code(1277) + "Not yet able to handle this GraphQL request");
235        }
236
237}