001/*-
002 * #%L
003 * Smile CDR - CDR
004 * %%
005 * Copyright (C) 2016 - 2026 Smile CDR, Inc.
006 * %%
007 * All rights reserved.
008 * #L%
009 */
010package ca.cdr.test.app.clients;
011
012import ca.cdr.api.fhir.client.IExtendedGenericClient;
013import ca.cdr.api.model.json.oauth.SmartConfigurationJson;
014import ca.uhn.fhir.context.FhirContext;
015import ca.uhn.fhir.interceptor.api.IInterceptorService;
016import ca.uhn.fhir.jpa.bulk.export.model.BulkExportResponseJson;
017import ca.uhn.fhir.jpa.model.util.JpaConstants;
018import ca.uhn.fhir.model.primitive.IdDt;
019import ca.uhn.fhir.model.primitive.UriDt;
020import ca.uhn.fhir.rest.api.Constants;
021import ca.uhn.fhir.rest.api.EncodingEnum;
022import ca.uhn.fhir.rest.api.MethodOutcome;
023import ca.uhn.fhir.rest.api.RequestFormatParamStyleEnum;
024import ca.uhn.fhir.rest.api.SummaryEnum;
025import ca.uhn.fhir.rest.api.server.bulk.BulkExportJobParameters;
026import ca.uhn.fhir.rest.client.api.IGenericClient;
027import ca.uhn.fhir.rest.client.api.IHttpClient;
028import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
029import ca.uhn.fhir.rest.gclient.ICreate;
030import ca.uhn.fhir.rest.gclient.IDelete;
031import ca.uhn.fhir.rest.gclient.IFetchConformanceUntyped;
032import ca.uhn.fhir.rest.gclient.IGetPage;
033import ca.uhn.fhir.rest.gclient.IHistory;
034import ca.uhn.fhir.rest.gclient.IMeta;
035import ca.uhn.fhir.rest.gclient.IOperation;
036import ca.uhn.fhir.rest.gclient.IPatch;
037import ca.uhn.fhir.rest.gclient.IRawHttp;
038import ca.uhn.fhir.rest.gclient.IRead;
039import ca.uhn.fhir.rest.gclient.ITransaction;
040import ca.uhn.fhir.rest.gclient.IUntypedQuery;
041import ca.uhn.fhir.rest.gclient.IUpdate;
042import ca.uhn.fhir.rest.gclient.IValidate;
043import ca.uhn.fhir.rest.server.provider.ProviderConstants;
044import ca.uhn.fhir.test.utilities.HttpTestRequest;
045import ca.uhn.fhir.test.utilities.HttpTestResponse;
046import ca.uhn.fhir.util.ParametersUtil;
047import jakarta.annotation.Nonnull;
048import jakarta.annotation.Nullable;
049import org.hl7.fhir.instance.model.api.IBaseBundle;
050import org.hl7.fhir.instance.model.api.IBaseParameters;
051import org.hl7.fhir.instance.model.api.IBaseResource;
052import org.hl7.fhir.instance.model.api.IPrimitiveType;
053import tools.jackson.core.JacksonException;
054import tools.jackson.databind.json.JsonMapper;
055
056import java.time.Duration;
057import java.time.Instant;
058import java.util.Date;
059import java.util.Objects;
060import java.util.function.Function;
061
062public class CdrFhirClient implements IExtendedGenericClient {
063
064        private final IGenericClient myDelegate;
065        private final Function<String, HttpTestRequest> myPathRequests;
066        private final Function<String, HttpTestRequest> myServerUrlRequests;
067
068        public CdrFhirClient(
069                        @Nonnull IGenericClient theDelegate,
070                        @Nonnull Function<String, HttpTestRequest> thePathRequests,
071                        @Nonnull Function<String, HttpTestRequest> theServerUrlRequests) {
072                myDelegate = Objects.requireNonNull(theDelegate);
073                myPathRequests = Objects.requireNonNull(thePathRequests);
074                myServerUrlRequests = Objects.requireNonNull(theServerUrlRequests);
075        }
076
077        @Override
078        @Nonnull
079        public SmartConfigurationJson getSmartConfiguration() {
080                HttpTestResponse response =
081                                myPathRequests.apply("/.well-known/smart-configuration").get().assertStatus(200);
082                try {
083                        return JsonMapper.builderWithJackson2Defaults().build().readValue(response.getBody(), SmartConfigurationJson.class);
084                } catch (JacksonException e) {
085                        throw new IllegalStateException("Unparseable smart-configuration: " + response.getBody(), e);
086                }
087        }
088
089        @Override
090        @Nonnull
091        public String startBulkExport(@Nonnull BulkExportJobParameters theParameters) {
092                FhirContext fhirContext = getFhirContext();
093                IBaseParameters body = ParametersUtil.newInstance(fhirContext);
094                String outputFormat = theParameters.getOutputFormat();
095                ParametersUtil.addParameterToParametersString(
096                                fhirContext,
097                                body,
098                                JpaConstants.PARAM_EXPORT_OUTPUT_FORMAT,
099                                outputFormat == null ? Constants.CT_FHIR_NDJSON : outputFormat);
100                if (!theParameters.getResourceTypes().isEmpty()) {
101                        ParametersUtil.addParameterToParametersString(
102                                        fhirContext,
103                                        body,
104                                        JpaConstants.PARAM_EXPORT_TYPE,
105                                        String.join(",", theParameters.getResourceTypes()));
106                }
107                addInstant(fhirContext, body, JpaConstants.PARAM_EXPORT_SINCE, theParameters.getSince());
108                addInstant(fhirContext, body, JpaConstants.PARAM_EXPORT_UNTIL, theParameters.getUntil());
109                theParameters.getFilters().forEach(filter -> ParametersUtil.addParameterToParametersString(
110                                fhirContext, body, JpaConstants.PARAM_EXPORT_TYPE_FILTER, filter));
111                theParameters.getPostFetchFilterUrls().forEach(url -> ParametersUtil.addParameterToParametersString(
112                                fhirContext, body, JpaConstants.PARAM_EXPORT_TYPE_POST_FETCH_FILTER_URL, url));
113                theParameters.getPatientIds().forEach(patientId -> ParametersUtil.addParameterToParametersReference(
114                                fhirContext, body, JpaConstants.PARAM_EXPORT_PATIENT, patientId));
115                theParameters.getExpandMdm().ifPresent(expandMdm -> ParametersUtil.addParameterToParametersBoolean(
116                                fhirContext, body, JpaConstants.PARAM_EXPORT_MDM, expandMdm));
117                if (theParameters.getExportIdentifier() != null) {
118                        ParametersUtil.addParameterToParametersString(
119                                        fhirContext, body, JpaConstants.PARAM_EXPORT_IDENTIFIER, theParameters.getExportIdentifier());
120                }
121                if (theParameters.isIncludeHistory()) {
122                        ParametersUtil.addParameterToParametersBoolean(
123                                        fhirContext, body, JpaConstants.PARAM_EXPORT_INCLUDE_HISTORY, true);
124                }
125
126                HttpTestResponse response = myPathRequests
127                                .apply(bulkExportPath(theParameters))
128                                .withHeader("Prefer", "respond-async")
129                                .withHeader("Cache-Control", "no-cache")
130                                .post(body)
131                                .assertStatus(202);
132                String pollUrl = response.getHeader("Content-Location");
133                if (pollUrl == null || pollUrl.isBlank()) {
134                        throw new AssertionError("202 Accepted carried no Content-Location");
135                }
136                return pollUrl;
137        }
138
139        @Override
140        @Nonnull
141        public BulkExportResponseJson awaitBulkExport(@Nonnull String thePollUrl) {
142                return awaitBulkExport(thePollUrl, Duration.ofMinutes(5), Duration.ofSeconds(1));
143        }
144
145        @Override
146        @Nonnull
147        public BulkExportResponseJson awaitBulkExport(
148                        @Nonnull String thePollUrl, @Nonnull Duration theTimeout, @Nonnull Duration thePollInterval) {
149                Instant deadline = Instant.now().plus(theTimeout);
150                while (true) {
151                        HttpTestResponse response = myServerUrlRequests.apply(thePollUrl).get();
152                        if (response.getStatusCode() == 200) {
153                                return JsonMapper.builder().build().readValue(response.getBody(), BulkExportResponseJson.class);
154                        }
155                        if (response.getStatusCode() != 202) {
156                                throw new AssertionError("Bulk export at " + thePollUrl + " failed with HTTP "
157                                                + response.getStatusCode() + ": " + response.getBody());
158                        }
159                        if (Instant.now().isAfter(deadline)) {
160                                throw new AssertionError("Bulk export at " + thePollUrl + " still in progress after " + theTimeout
161                                                + " (X-Progress: " + response.getHeader("X-Progress") + ")");
162                        }
163                        try {
164                                Thread.sleep(thePollInterval.toMillis());
165                        } catch (InterruptedException e) {
166                                Thread.currentThread().interrupt();
167                                throw new IllegalStateException("Interrupted awaiting bulk export at " + thePollUrl, e);
168                        }
169                }
170        }
171
172        private static String bulkExportPath(BulkExportJobParameters theParameters) {
173                BulkExportJobParameters.ExportStyle style = theParameters.getExportStyle();
174                if (style == null || style == BulkExportJobParameters.ExportStyle.SYSTEM) {
175                        return "/" + ProviderConstants.OPERATION_EXPORT;
176                }
177                if (style == BulkExportJobParameters.ExportStyle.PATIENT) {
178                        return "/Patient/" + ProviderConstants.OPERATION_EXPORT;
179                }
180                String groupId = theParameters.getGroupId();
181                if (groupId == null || groupId.isBlank()) {
182                        throw new IllegalArgumentException("A GROUP export needs a group ID");
183                }
184                return "/Group/" + groupId.replaceFirst("^Group/", "") + "/" + ProviderConstants.OPERATION_EXPORT;
185        }
186
187        @SuppressWarnings("unchecked")
188        private static void addInstant(
189                        FhirContext theFhirContext, IBaseParameters theBody, String theName, @Nullable Date theValue) {
190                if (theValue == null) {
191                        return;
192                }
193                IPrimitiveType<Date> instant = (IPrimitiveType<Date>)
194                                Objects.requireNonNull(theFhirContext.getElementDefinition("instant")).newInstance();
195                instant.setValue(theValue);
196                ParametersUtil.addParameterToParameters(theFhirContext, theBody, theName, instant);
197        }
198
199        @Override
200        public IFetchConformanceUntyped capabilities() {
201                return myDelegate.capabilities();
202        }
203
204        @Override
205        public ICreate create() {
206                return myDelegate.create();
207        }
208
209        @Override
210        public IDelete delete() {
211                return myDelegate.delete();
212        }
213
214        @Override
215        @Deprecated(since = "3.0.0", forRemoval = true)
216        public IFetchConformanceUntyped fetchConformance() {
217                return myDelegate.fetchConformance();
218        }
219
220        @Override
221        public void forceConformanceCheck() throws FhirClientConnectionException {
222                myDelegate.forceConformanceCheck();
223        }
224
225        @Override
226        public IHistory history() {
227                return myDelegate.history();
228        }
229
230        @Override
231        public IGetPage loadPage() {
232                return myDelegate.loadPage();
233        }
234
235        @Override
236        public IMeta meta() {
237                return myDelegate.meta();
238        }
239
240        @Override
241        public IOperation operation() {
242                return myDelegate.operation();
243        }
244
245        @Override
246        public IPatch patch() {
247                return myDelegate.patch();
248        }
249
250        @Override
251        public IRead read() {
252                return myDelegate.read();
253        }
254
255        @Override
256        public IRawHttp rawHttpRequest() {
257                return myDelegate.rawHttpRequest();
258        }
259
260        @Override
261        @Deprecated
262        public <T extends IBaseResource> T read(Class<T> theType, String theUrl) {
263                return myDelegate.read(theType, theUrl);
264        }
265
266        @Override
267        @Deprecated
268        public <T extends IBaseResource> T read(Class<T> theType, UriDt theUrl) {
269                return myDelegate.read(theType, theUrl);
270        }
271
272        @Override
273        @Deprecated
274        public IBaseResource read(UriDt theUrl) {
275                return myDelegate.read(theUrl);
276        }
277
278        @Override
279        public void registerInterceptor(Object theInterceptor) {
280                myDelegate.registerInterceptor(theInterceptor);
281        }
282
283        @Override
284        public <T extends IBaseBundle> IUntypedQuery<T> search() {
285                return myDelegate.search();
286        }
287
288        @Override
289        @Deprecated
290        public void setLogRequestAndResponse(boolean theLogRequestAndResponse) {
291                myDelegate.setLogRequestAndResponse(theLogRequestAndResponse);
292        }
293
294        @Override
295        public ITransaction transaction() {
296                return myDelegate.transaction();
297        }
298
299        @Override
300        public void unregisterInterceptor(Object theInterceptor) {
301                myDelegate.unregisterInterceptor(theInterceptor);
302        }
303
304        @Override
305        public IUpdate update() {
306                return myDelegate.update();
307        }
308
309        @Override
310        @Deprecated
311        public MethodOutcome update(IdDt theId, IBaseResource theResource) {
312                return myDelegate.update(theId, theResource);
313        }
314
315        @Override
316        @Deprecated
317        public MethodOutcome update(String theId, IBaseResource theResource) {
318                return myDelegate.update(theId, theResource);
319        }
320
321        @Override
322        public IValidate validate() {
323                return myDelegate.validate();
324        }
325
326        @Override
327        @Deprecated
328        public MethodOutcome validate(IBaseResource theResource) {
329                return myDelegate.validate(theResource);
330        }
331
332        @Override
333        @Deprecated
334        public <T extends IBaseResource> T vread(Class<T> theType, IdDt theId) {
335                return myDelegate.vread(theType, theId);
336        }
337
338        @Override
339        @Deprecated
340        public <T extends IBaseResource> T vread(Class<T> theType, String theId, String theVersionId) {
341                return myDelegate.vread(theType, theId, theVersionId);
342        }
343
344        @Override
345        public IInterceptorService getInterceptorService() {
346                return myDelegate.getInterceptorService();
347        }
348
349        @Override
350        public void setInterceptorService(@Nonnull IInterceptorService theInterceptorService) {
351                myDelegate.setInterceptorService(theInterceptorService);
352        }
353
354        @Override
355        public <T extends IBaseResource> T fetchResourceFromUrl(Class<T> theResourceType, String theUrl) {
356                return myDelegate.fetchResourceFromUrl(theResourceType, theUrl);
357        }
358
359        @Override
360        public EncodingEnum getEncoding() {
361                return myDelegate.getEncoding();
362        }
363
364        @Override
365        public void setEncoding(EncodingEnum theEncoding) {
366                myDelegate.setEncoding(theEncoding);
367        }
368
369        @Override
370        public FhirContext getFhirContext() {
371                return myDelegate.getFhirContext();
372        }
373
374        @Override
375        public IHttpClient getHttpClient() {
376                return myDelegate.getHttpClient();
377        }
378
379        @Override
380        public String getServerBase() {
381                return myDelegate.getServerBase();
382        }
383
384        @Override
385        public void setPrettyPrint(Boolean thePrettyPrint) {
386                myDelegate.setPrettyPrint(thePrettyPrint);
387        }
388
389        @Override
390        public void setSummary(SummaryEnum theSummary) {
391                myDelegate.setSummary(theSummary);
392        }
393
394        @Override
395        public void setFormatParamStyle(RequestFormatParamStyleEnum theRequestFormatParamStyle) {
396                myDelegate.setFormatParamStyle(theRequestFormatParamStyle);
397        }
398}