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