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.repository;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
025import ca.uhn.fhir.model.api.IQueryParameterType;
026import ca.uhn.fhir.model.api.Include;
027import ca.uhn.fhir.model.valueset.BundleTypeEnum;
028import ca.uhn.fhir.repository.IRepository;
029import ca.uhn.fhir.rest.api.Constants;
030import ca.uhn.fhir.rest.api.MethodOutcome;
031import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
032import ca.uhn.fhir.rest.api.server.IBundleProvider;
033import ca.uhn.fhir.rest.api.server.RequestDetails;
034import ca.uhn.fhir.rest.server.IPagingProvider;
035import ca.uhn.fhir.rest.server.RestfulServer;
036import ca.uhn.fhir.rest.server.RestfulServerUtils;
037import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
038import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
039import ca.uhn.fhir.rest.server.exceptions.NotImplementedOperationException;
040import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
041import ca.uhn.fhir.rest.server.method.ConformanceMethodBinding;
042import ca.uhn.fhir.rest.server.method.PageMethodBinding;
043import ca.uhn.fhir.util.UrlUtil;
044import com.google.common.collect.Multimap;
045import org.hl7.fhir.instance.model.api.IBaseBundle;
046import org.hl7.fhir.instance.model.api.IBaseConformance;
047import org.hl7.fhir.instance.model.api.IBaseParameters;
048import org.hl7.fhir.instance.model.api.IBaseResource;
049import org.hl7.fhir.instance.model.api.IIdType;
050
051import java.io.IOException;
052import java.util.HashSet;
053import java.util.List;
054import java.util.Map;
055import java.util.Set;
056
057import static ca.uhn.fhir.jpa.repository.RequestDetailsCloner.startWith;
058import static org.apache.commons.lang3.StringUtils.isNotBlank;
059
060/**
061 * This class leverages DaoRegistry from Hapi-fhir to implement CRUD FHIR API operations constrained to provide only the operations necessary for the cql-evaluator modules to function.
062 **/
063@SuppressWarnings("squid:S1135")
064public class HapiFhirRepository implements IRepository {
065        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HapiFhirRepository.class);
066        private final DaoRegistry myDaoRegistry;
067        private final RequestDetails myRequestDetails;
068        private final RestfulServer myRestfulServer;
069
070        public HapiFhirRepository(
071                        DaoRegistry theDaoRegistry, RequestDetails theRequestDetails, RestfulServer theRestfulServer) {
072                myDaoRegistry = theDaoRegistry;
073                myRequestDetails = theRequestDetails;
074                myRestfulServer = theRestfulServer;
075        }
076
077        @Override
078        public <T extends IBaseResource, I extends IIdType> T read(
079                        Class<T> theResourceType, I theId, Map<String, String> theHeaders) {
080                RequestDetails details = startWith(myRequestDetails)
081                                .setAction(RestOperationTypeEnum.READ)
082                                .addHeaders(theHeaders)
083                                .create();
084                return myDaoRegistry.getResourceDao(theResourceType).read(theId, details);
085        }
086
087        @Override
088        public <T extends IBaseResource> MethodOutcome create(T theResource, Map<String, String> theHeaders) {
089                RequestDetails details = startWith(myRequestDetails)
090                                .setAction(RestOperationTypeEnum.CREATE)
091                                .addHeaders(theHeaders)
092                                .create();
093                return myDaoRegistry.getResourceDao(theResource).create(theResource, details);
094        }
095
096        @Override
097        public <I extends IIdType, P extends IBaseParameters> MethodOutcome patch(
098                        I theId, P thePatchParameters, Map<String, String> theHeaders) {
099                RequestDetails details = startWith(myRequestDetails)
100                                .setAction(RestOperationTypeEnum.PATCH)
101                                .addHeaders(theHeaders)
102                                .create();
103                // TODO update FHIR patchType once FHIRPATCH bug has been fixed
104                return myDaoRegistry
105                                .getResourceDao(theId.getResourceType())
106                                .patch(theId, null, null, null, thePatchParameters, details);
107        }
108
109        @Override
110        public <T extends IBaseResource> MethodOutcome update(T theResource, Map<String, String> theHeaders) {
111                RequestDetails details = startWith(myRequestDetails)
112                                .setAction(RestOperationTypeEnum.UPDATE)
113                                .addHeaders(theHeaders)
114                                .create();
115
116                return myDaoRegistry.getResourceDao(theResource).update(theResource, details);
117        }
118
119        @Override
120        public <T extends IBaseResource, I extends IIdType> MethodOutcome delete(
121                        Class<T> theResourceType, I theId, Map<String, String> theHeaders) {
122                RequestDetails details = startWith(myRequestDetails)
123                                .setAction(RestOperationTypeEnum.DELETE)
124                                .addHeaders(theHeaders)
125                                .create();
126
127                return myDaoRegistry.getResourceDao(theResourceType).delete(theId, details);
128        }
129
130        @Override
131        public <B extends IBaseBundle, T extends IBaseResource> B search(
132                        Class<B> theBundleType,
133                        Class<T> theResourceType,
134                        Multimap<String, List<IQueryParameterType>> theSearchParameters,
135                        Map<String, String> theHeaders) {
136                RequestDetails details = startWith(myRequestDetails)
137                                .setAction(RestOperationTypeEnum.SEARCH_TYPE)
138                                .addHeaders(theHeaders)
139                                .create();
140                SearchConverter converter = new SearchConverter();
141                converter.convertParameters(theSearchParameters, fhirContext());
142                details.setParameters(converter.myResultParameters);
143                details.setResourceName(myRestfulServer.getFhirContext().getResourceType(theResourceType));
144                IBundleProvider bundleProvider =
145                                myDaoRegistry.getResourceDao(theResourceType).search(converter.mySearchParameterMap, details);
146
147                if (bundleProvider == null) {
148                        return null;
149                }
150
151                return createBundle(details, bundleProvider, null);
152        }
153
154        private <B extends IBaseBundle> B createBundle(
155                        RequestDetails theRequestDetails, IBundleProvider theBundleProvider, String thePagingAction) {
156                Integer count = RestfulServerUtils.extractCountParameter(theRequestDetails);
157                String linkSelf = RestfulServerUtils.createLinkSelf(theRequestDetails.getFhirServerBase(), theRequestDetails);
158
159                Set<Include> includes = new HashSet<>();
160                String[] reqIncludes = theRequestDetails.getParameters().get(Constants.PARAM_INCLUDE);
161                if (reqIncludes != null) {
162                        for (String nextInclude : reqIncludes) {
163                                includes.add(new Include(nextInclude));
164                        }
165                }
166
167                Integer offset = RestfulServerUtils.tryToExtractNamedParameter(theRequestDetails, Constants.PARAM_PAGINGOFFSET);
168                if (offset == null || offset < 0) {
169                        offset = 0;
170                }
171                int start = offset;
172                if (theBundleProvider.size() != null) {
173                        start = Math.max(0, Math.min(offset, theBundleProvider.size()));
174                }
175
176                BundleTypeEnum bundleType = null;
177                String[] bundleTypeValues = theRequestDetails.getParameters().get(Constants.PARAM_BUNDLETYPE);
178                if (bundleTypeValues != null) {
179                        bundleType = BundleTypeEnum.VALUESET_BINDER.fromCodeString(bundleTypeValues[0]);
180                } else {
181                        bundleType = BundleTypeEnum.SEARCHSET;
182                }
183
184                return unsafeCast(BundleProviderUtil.createBundleFromBundleProvider(
185                                myRestfulServer,
186                                theRequestDetails,
187                                count,
188                                linkSelf,
189                                includes,
190                                theBundleProvider,
191                                start,
192                                bundleType,
193                                thePagingAction));
194        }
195
196        // TODO: The main use case for this is paging through Bundles, but I suppose that technically
197        // we ought to handle any old link. Maybe this is also an escape hatch for "custom non-FHIR
198        // repository action"?
199        @Override
200        public <B extends IBaseBundle> B link(Class<B> theBundleType, String theUrl, Map<String, String> theHeaders) {
201                RequestDetails details = startWith(myRequestDetails)
202                                .setAction(RestOperationTypeEnum.GET_PAGE)
203                                .addHeaders(theHeaders)
204                                .create();
205                UrlUtil.UrlParts urlParts = UrlUtil.parseUrl(theUrl);
206                details.setCompleteUrl(theUrl);
207                details.setParameters(UrlUtil.parseQueryStrings(urlParts.getParams()));
208
209                IPagingProvider pagingProvider = myRestfulServer.getPagingProvider();
210                if (pagingProvider == null) {
211                        throw new InvalidRequestException(Msg.code(2638) + "This server does not support paging");
212                }
213
214                String pagingAction = details.getParameters().get(Constants.PARAM_PAGINGACTION)[0];
215
216                IBundleProvider bundleProvider;
217
218                String pageId = null;
219                String[] pageIdParams = details.getParameters().get(Constants.PARAM_PAGEID);
220                if (pageIdParams != null && pageIdParams.length > 0 && isNotBlank(pageIdParams[0])) {
221                        pageId = pageIdParams[0];
222                }
223
224                if (pageId != null) {
225                        // This is a page request by Search ID and Page ID
226                        bundleProvider = pagingProvider.retrieveResultList(details, pagingAction, pageId);
227                        validateHaveBundleProvider(pagingAction, bundleProvider);
228                } else {
229                        // This is a page request by Search ID and Offset
230                        bundleProvider = pagingProvider.retrieveResultList(details, pagingAction);
231                        validateHaveBundleProvider(pagingAction, bundleProvider);
232                }
233
234                return createBundle(details, bundleProvider, pagingAction);
235        }
236
237        private void validateHaveBundleProvider(String thePagingAction, IBundleProvider theBundleProvider) {
238                // Return an HTTP 410 if the search is not known
239                if (theBundleProvider == null) {
240                        ourLog.info("Client requested unknown paging ID[{}]", thePagingAction);
241                        String msg = fhirContext()
242                                        .getLocalizer()
243                                        .getMessage(PageMethodBinding.class, "unknownSearchId", thePagingAction);
244                        throw new ResourceGoneException(Msg.code(2639) + msg);
245                }
246        }
247
248        @Override
249        public <C extends IBaseConformance> C capabilities(
250                        Class<C> theCapabilityStatementType, Map<String, String> theHeaders) {
251                ConformanceMethodBinding method = myRestfulServer.getServerConformanceMethod();
252                if (method == null) {
253                        return null;
254                }
255                RequestDetails details = startWith(myRequestDetails)
256                                .setAction(RestOperationTypeEnum.METADATA)
257                                .addHeaders(theHeaders)
258                                .create();
259                return unsafeCast(method.provideCapabilityStatement(myRestfulServer, details));
260        }
261
262        @Override
263        @SuppressWarnings("unchecked")
264        public <B extends IBaseBundle> B transaction(B theBundle, Map<String, String> theHeaders) {
265                RequestDetails details = startWith(myRequestDetails)
266                                .setAction(RestOperationTypeEnum.TRANSACTION)
267                                .addHeaders(theHeaders)
268                                .create();
269                return unsafeCast(myDaoRegistry.getSystemDao().transaction(details, theBundle));
270        }
271
272        @Override
273        public <R extends IBaseResource, P extends IBaseParameters> R invoke(
274                        String theName, P theParameters, Class<R> theReturnType, Map<String, String> theHeaders) {
275                RequestDetails details = startWith(myRequestDetails)
276                                .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
277                                .addHeaders(theHeaders)
278                                .setOperation(theName)
279                                .setParameters(theParameters)
280                                .create();
281
282                return invoke(details);
283        }
284
285        @Override
286        public <P extends IBaseParameters> MethodOutcome invoke(
287                        String theName, P theParameters, Map<String, String> theHeaders) {
288                RequestDetails details = startWith(myRequestDetails)
289                                .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
290                                .addHeaders(theHeaders)
291                                .setOperation(theName)
292                                .setParameters(theParameters)
293                                .create();
294
295                return invoke(details);
296        }
297
298        @Override
299        public <R extends IBaseResource, P extends IBaseParameters, T extends IBaseResource> R invoke(
300                        Class<T> theResourceType,
301                        String theName,
302                        P theParameters,
303                        Class<R> theReturnType,
304                        Map<String, String> theHeaders) {
305                RequestDetails details = startWith(myRequestDetails)
306                                .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
307                                .addHeaders(theHeaders)
308                                .setOperation(theName)
309                                .setResourceType(theResourceType.getSimpleName())
310                                .setParameters(theParameters)
311                                .create();
312
313                return invoke(details);
314        }
315
316        @Override
317        public <P extends IBaseParameters, T extends IBaseResource> MethodOutcome invoke(
318                        Class<T> theResourceType, String theName, P theParameters, Map<String, String> theHeaders) {
319                RequestDetails details = startWith(myRequestDetails)
320                                .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
321                                .addHeaders(theHeaders)
322                                .setOperation(theName)
323                                .setResourceType(theResourceType.getSimpleName())
324                                .setParameters(theParameters)
325                                .create();
326
327                return invoke(details);
328        }
329
330        @Override
331        public <R extends IBaseResource, P extends IBaseParameters, I extends IIdType> R invoke(
332                        I theId, String theName, P theParameters, Class<R> theReturnType, Map<String, String> theHeaders) {
333                RequestDetails details = startWith(myRequestDetails)
334                                .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
335                                .addHeaders(theHeaders)
336                                .setOperation(theName)
337                                .setResourceType(theId.getResourceType())
338                                .setId(theId)
339                                .setParameters(theParameters)
340                                .create();
341
342                return invoke(details);
343        }
344
345        @Override
346        public <P extends IBaseParameters, I extends IIdType> MethodOutcome invoke(
347                        I theId, String theName, P theParameters, Map<String, String> theHeaders) {
348                RequestDetails details = startWith(myRequestDetails)
349                                .setAction(RestOperationTypeEnum.EXTENDED_OPERATION_SERVER)
350                                .addHeaders(theHeaders)
351                                .setOperation(theName)
352                                .setResourceType(theId.getResourceType())
353                                .setId(theId)
354                                .setParameters(theParameters)
355                                .create();
356
357                return invoke(details);
358        }
359
360        private void notImplemented() {
361                throw new NotImplementedOperationException(Msg.code(2640) + "history not yet implemented");
362        }
363
364        @Override
365        public <B extends IBaseBundle, P extends IBaseParameters> B history(
366                        P theParameters, Class<B> theBundleType, Map<String, String> theHeaders) {
367                notImplemented();
368
369                return null;
370        }
371
372        @Override
373        public <B extends IBaseBundle, P extends IBaseParameters, T extends IBaseResource> B history(
374                        Class<T> theResourceType, P theParameters, Class<B> theBundleType, Map<String, String> theHeaders) {
375                notImplemented();
376
377                return null;
378        }
379
380        @Override
381        public <B extends IBaseBundle, P extends IBaseParameters, I extends IIdType> B history(
382                        I theId, P theParameters, Class<B> theBundleType, Map<String, String> theHeaders) {
383                notImplemented();
384
385                return null;
386        }
387
388        @Override
389        public FhirContext fhirContext() {
390                return myRestfulServer.getFhirContext();
391        }
392
393        protected <R> R invoke(RequestDetails theDetails) {
394                try {
395                        return unsafeCast(myRestfulServer
396                                        .determineResourceMethod(theDetails, null)
397                                        .invokeServer(myRestfulServer, theDetails));
398                } catch (IOException exception) {
399                        throw new InternalErrorException(Msg.code(2641) + exception);
400                }
401        }
402
403        @SuppressWarnings("unchecked")
404        private static <T> T unsafeCast(Object theObject) {
405                return (T) theObject;
406        }
407}