001package org.hl7.fhir.common.hapi.validation.support;
002
003import ca.uhn.fhir.context.FhirContext;
004import ca.uhn.fhir.context.FhirVersionEnum;
005import ca.uhn.fhir.context.support.ConceptValidationOptions;
006import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
007import ca.uhn.fhir.context.support.IValidationSupport;
008import ca.uhn.fhir.context.support.LookupCodeRequest;
009import ca.uhn.fhir.context.support.TranslateConceptResults;
010import ca.uhn.fhir.context.support.ValidationSupportContext;
011import ca.uhn.fhir.i18n.Msg;
012import ca.uhn.fhir.rest.api.SummaryEnum;
013import ca.uhn.fhir.rest.client.api.IGenericClient;
014import ca.uhn.fhir.rest.gclient.IQuery;
015import ca.uhn.fhir.util.BundleUtil;
016import ca.uhn.fhir.util.ParametersUtil;
017import jakarta.annotation.Nonnull;
018import jakarta.annotation.Nullable;
019import org.apache.commons.lang3.StringUtils;
020import org.apache.commons.lang3.Validate;
021import org.hl7.fhir.instance.model.api.IBaseBundle;
022import org.hl7.fhir.instance.model.api.IBaseDatatype;
023import org.hl7.fhir.instance.model.api.IBaseParameters;
024import org.hl7.fhir.instance.model.api.IBaseResource;
025import org.hl7.fhir.r4.model.Base;
026import org.hl7.fhir.r4.model.CodeSystem;
027import org.hl7.fhir.r4.model.CodeType;
028import org.hl7.fhir.r4.model.Coding;
029import org.hl7.fhir.r4.model.Parameters;
030import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
031import org.hl7.fhir.r4.model.Property;
032import org.hl7.fhir.r4.model.StringType;
033import org.hl7.fhir.r4.model.Type;
034import org.hl7.fhir.r4.model.ValueSet;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import java.util.ArrayList;
039import java.util.List;
040import java.util.Objects;
041
042import static org.apache.commons.lang3.StringUtils.isBlank;
043import static org.apache.commons.lang3.StringUtils.isNotBlank;
044
045/**
046 * This class is an implementation of {@link IValidationSupport} that fetches validation codes
047 * from a remote FHIR based terminology server. It will invoke the FHIR
048 * <a href="http://hl7.org/fhir/valueset-operation-validate-code.html">ValueSet/$validate-code</a>
049 * operation in order to validate codes.
050 */
051public class RemoteTerminologyServiceValidationSupport extends BaseValidationSupport implements IValidationSupport {
052        private static final Logger ourLog = LoggerFactory.getLogger(RemoteTerminologyServiceValidationSupport.class);
053
054        private String myBaseUrl;
055        private final List<Object> myClientInterceptors = new ArrayList<>();
056
057        /**
058         * Constructor
059         *
060         * @param theFhirContext The FhirContext object to use
061         */
062        public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext) {
063                super(theFhirContext);
064        }
065
066        public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext, String theBaseUrl) {
067                super(theFhirContext);
068                myBaseUrl = theBaseUrl;
069        }
070
071        @Override
072        public String getName() {
073                return getFhirContext().getVersion().getVersion() + " Remote Terminology Service Validation Support";
074        }
075
076        @Override
077        public CodeValidationResult validateCode(
078                        ValidationSupportContext theValidationSupportContext,
079                        ConceptValidationOptions theOptions,
080                        String theCodeSystem,
081                        String theCode,
082                        String theDisplay,
083                        String theValueSetUrl) {
084                return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, theValueSetUrl, null);
085        }
086
087        @Override
088        public CodeValidationResult validateCodeInValueSet(
089                        ValidationSupportContext theValidationSupportContext,
090                        ConceptValidationOptions theOptions,
091                        String theCodeSystem,
092                        String theCode,
093                        String theDisplay,
094                        @Nonnull IBaseResource theValueSet) {
095
096                IBaseResource valueSet = theValueSet;
097
098                // some external validators require the system when the code is passed
099                // so let's try to get it from the VS if is not present
100                String codeSystem = theCodeSystem;
101                if (isNotBlank(theCode) && isBlank(codeSystem)) {
102                        codeSystem = extractCodeSystemForCode((ValueSet) theValueSet, theCode);
103                }
104
105                // Remote terminology services shouldn't be used to validate codes with an implied system
106                if (isBlank(codeSystem)) {
107                        return null;
108                }
109
110                String valueSetUrl = DefaultProfileValidationSupport.getConformanceResourceUrl(myCtx, valueSet);
111                if (isNotBlank(valueSetUrl)) {
112                        valueSet = null;
113                } else {
114                        valueSetUrl = null;
115                }
116                return invokeRemoteValidateCode(codeSystem, theCode, theDisplay, valueSetUrl, valueSet);
117        }
118
119        /**
120         * Try to obtain the codeSystem of the received code from the received ValueSet
121         */
122        private String extractCodeSystemForCode(ValueSet theValueSet, String theCode) {
123                if (theValueSet.getCompose() == null
124                                || theValueSet.getCompose().getInclude() == null
125                                || theValueSet.getCompose().getInclude().isEmpty()) {
126                        return null;
127                }
128
129                if (theValueSet.getCompose().getInclude().size() == 1) {
130                        ValueSet.ConceptSetComponent include =
131                                        theValueSet.getCompose().getInclude().iterator().next();
132                        return getVersionedCodeSystem(include);
133                }
134
135                // when component has more than one include, their codeSystem(s) could be different, so we need to make sure
136                // that we are picking up the system for the include filter to which the code corresponds
137                for (ValueSet.ConceptSetComponent include : theValueSet.getCompose().getInclude()) {
138                        if (include.hasSystem()) {
139                                for (ValueSet.ConceptReferenceComponent concept : include.getConcept()) {
140                                        if (concept.hasCodeElement() && concept.getCode().equals(theCode)) {
141                                                return getVersionedCodeSystem(include);
142                                        }
143                                }
144                        }
145                }
146
147                // at this point codeSystem couldn't be extracted for a multi-include ValueSet. Just on case it was
148                // because the format was not well handled, let's allow to watch the VS by an easy logging change
149                ourLog.trace("CodeSystem couldn't be extracted for code: {} for ValueSet: {}", theCode, theValueSet.getId());
150                return null;
151        }
152
153        private String getVersionedCodeSystem(ValueSet.ConceptSetComponent theComponent) {
154                String codeSystem = theComponent.getSystem();
155                if (!codeSystem.contains("|") && theComponent.hasVersion()) {
156                        codeSystem += "|" + theComponent.getVersion();
157                }
158                return codeSystem;
159        }
160
161        @Override
162        public IBaseResource fetchCodeSystem(String theSystem) {
163                // callers of this want the whole resource.
164                return fetchCodeSystem(theSystem, SummaryEnum.FALSE);
165        }
166
167        /**
168         * Fetch the code system, possibly a summary.
169         * @param theSystem the canonical url
170         * @param theSummaryParam to force a summary mode - or null to allow server default.
171         * @return the CodeSystem
172         */
173        @Nullable
174        private IBaseResource fetchCodeSystem(String theSystem, @Nullable SummaryEnum theSummaryParam) {
175                IGenericClient client = provideClient();
176                Class<? extends IBaseBundle> bundleType =
177                                myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
178                IQuery<IBaseBundle> codeSystemQuery = client.search()
179                                .forResource("CodeSystem")
180                                .where(CodeSystem.URL.matches().value(theSystem));
181
182                if (theSummaryParam != null) {
183                        codeSystemQuery.summaryMode(theSummaryParam);
184                }
185
186                IBaseBundle results = codeSystemQuery.returnBundle(bundleType).execute();
187                List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
188                if (!resultsList.isEmpty()) {
189                        return resultsList.get(0);
190                }
191
192                return null;
193        }
194
195        @Override
196        public LookupCodeResult lookupCode(
197                        ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) {
198                final String code = theLookupCodeRequest.getCode();
199                final String system = theLookupCodeRequest.getSystem();
200                final String displayLanguage = theLookupCodeRequest.getDisplayLanguage();
201                Validate.notBlank(code, "theCode must be provided");
202
203                IGenericClient client = provideClient();
204                FhirContext fhirContext = client.getFhirContext();
205                FhirVersionEnum fhirVersion = fhirContext.getVersion().getVersion();
206
207                switch (fhirVersion) {
208                        case DSTU3:
209                        case R4:
210                                IBaseParameters params = ParametersUtil.newInstance(fhirContext);
211                                ParametersUtil.addParameterToParametersString(fhirContext, params, "code", code);
212                                if (!StringUtils.isEmpty(system)) {
213                                        ParametersUtil.addParameterToParametersString(fhirContext, params, "system", system);
214                                }
215                                if (!StringUtils.isEmpty(displayLanguage)) {
216                                        ParametersUtil.addParameterToParametersString(fhirContext, params, "language", displayLanguage);
217                                }
218                                for (String propertyName : theLookupCodeRequest.getPropertyNames()) {
219                                        ParametersUtil.addParameterToParametersString(fhirContext, params, "property", propertyName);
220                                }
221                                Class<? extends IBaseResource> codeSystemClass =
222                                                myCtx.getResourceDefinition("CodeSystem").getImplementingClass();
223                                IBaseParameters outcome = client.operation()
224                                                .onType(codeSystemClass)
225                                                .named("$lookup")
226                                                .withParameters(params)
227                                                .useHttpGet()
228                                                .execute();
229                                if (outcome != null && !outcome.isEmpty()) {
230                                        switch (fhirVersion) {
231                                                case DSTU3:
232                                                        return generateLookupCodeResultDSTU3(
233                                                                        code, system, (org.hl7.fhir.dstu3.model.Parameters) outcome);
234                                                case R4:
235                                                        return generateLookupCodeResultR4(code, system, (Parameters) outcome);
236                                        }
237                                }
238                                break;
239                        default:
240                                throw new UnsupportedOperationException(Msg.code(710) + "Unsupported FHIR version '"
241                                                + fhirVersion.getFhirVersionString() + "'. Only DSTU3 and R4 are supported.");
242                }
243                return null;
244        }
245
246        private LookupCodeResult generateLookupCodeResultDSTU3(
247                        String theCode, String theSystem, org.hl7.fhir.dstu3.model.Parameters outcomeDSTU3) {
248                // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding
249                //       several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in
250                // POM).
251                LookupCodeResult result = new LookupCodeResult();
252                result.setSearchedForCode(theCode);
253                result.setSearchedForSystem(theSystem);
254                result.setFound(true);
255                for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent parameterComponent :
256                                outcomeDSTU3.getParameter()) {
257                        String parameterTypeAsString = Objects.toString(parameterComponent.getValue(), null);
258                        switch (parameterComponent.getName()) {
259                                case "property":
260                                        org.hl7.fhir.dstu3.model.Property part = parameterComponent.getChildByName("part");
261                                        // The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be
262                                        // saved
263                                        if (part == null || part.getValues().size() < 2) {
264                                                continue;
265                                        }
266                                        BaseConceptProperty conceptProperty = createBaseConceptPropertyDstu3(part.getValues());
267                                        if (conceptProperty != null) {
268                                                result.getProperties().add(conceptProperty);
269                                        }
270                                        break;
271                                case "designation":
272                                        ConceptDesignation conceptDesignation = createConceptDesignationDstu3(parameterComponent);
273                                        result.getDesignations().add(conceptDesignation);
274                                        break;
275                                case "name":
276                                        result.setCodeSystemDisplayName(parameterTypeAsString);
277                                        break;
278                                case "version":
279                                        result.setCodeSystemVersion(parameterTypeAsString);
280                                        break;
281                                case "display":
282                                        result.setCodeDisplay(parameterTypeAsString);
283                                        break;
284                                case "abstract":
285                                        result.setCodeIsAbstract(Boolean.parseBoolean(parameterTypeAsString));
286                                        break;
287                        }
288                }
289                return result;
290        }
291
292        private static BaseConceptProperty createBaseConceptPropertyDstu3(List<org.hl7.fhir.dstu3.model.Base> theValues) {
293                org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent part1 =
294                                (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) theValues.get(0);
295                String propertyName = ((org.hl7.fhir.dstu3.model.CodeType) part1.getValue()).getValue();
296
297                BaseConceptProperty conceptProperty = null;
298                org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent part2 =
299                                (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) theValues.get(1);
300
301                org.hl7.fhir.dstu3.model.Type value = part2.getValue();
302                if (value == null) {
303                        return conceptProperty;
304                }
305                String fhirType = value.fhirType();
306                switch (fhirType) {
307                        case TYPE_STRING:
308                                org.hl7.fhir.dstu3.model.StringType stringType = (org.hl7.fhir.dstu3.model.StringType) part2.getValue();
309                                conceptProperty = new StringConceptProperty(propertyName, stringType.getValue());
310                                break;
311                        case TYPE_CODING:
312                                org.hl7.fhir.dstu3.model.Coding coding = (org.hl7.fhir.dstu3.model.Coding) part2.getValue();
313                                conceptProperty = new CodingConceptProperty(
314                                                propertyName, coding.getSystem(), coding.getCode(), coding.getDisplay());
315                                break;
316                                // TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699
317                        default:
318                                // other types will not fail for Remote Terminology
319                                conceptProperty = new StringConceptProperty(propertyName, value.toString());
320                }
321                return conceptProperty;
322        }
323
324        public static BaseConceptProperty createConceptProperty(final String theName, final IBaseDatatype theValue) {
325                if (theValue instanceof Type) {
326                        return createConceptPropertyR4(theName, (Type) theValue);
327                }
328                if (theValue instanceof org.hl7.fhir.dstu3.model.Type) {
329                        return createConceptPropertyDstu3(theName, (org.hl7.fhir.dstu3.model.Type) theValue);
330                }
331                return null;
332        }
333
334        private static BaseConceptProperty createConceptPropertyDstu3(
335                        final String theName, final org.hl7.fhir.dstu3.model.Type theValue) {
336                if (theValue == null) {
337                        return null;
338                }
339                BaseConceptProperty conceptProperty;
340                String fhirType = theValue.fhirType();
341                switch (fhirType) {
342                        case IValidationSupport.TYPE_STRING:
343                                org.hl7.fhir.dstu3.model.StringType stringType = (org.hl7.fhir.dstu3.model.StringType) theValue;
344                                conceptProperty = new StringConceptProperty(theName, stringType.getValue());
345                                break;
346                        case IValidationSupport.TYPE_CODING:
347                                org.hl7.fhir.dstu3.model.Coding coding = (org.hl7.fhir.dstu3.model.Coding) theValue;
348                                conceptProperty =
349                                                new CodingConceptProperty(theName, coding.getSystem(), coding.getCode(), coding.getDisplay());
350                                break;
351                                // TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699
352                        default:
353                                // other types will not fail for Remote Terminology
354                                conceptProperty = new StringConceptProperty(theName, theValue.toString());
355                }
356                return conceptProperty;
357        }
358
359        private ConceptDesignation createConceptDesignationDstu3(
360                        org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent theParameterComponent) {
361                ConceptDesignation conceptDesignation = new ConceptDesignation();
362                for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent designationComponent :
363                                theParameterComponent.getPart()) {
364                        org.hl7.fhir.dstu3.model.Type designationComponentValue = designationComponent.getValue();
365                        if (designationComponentValue == null) {
366                                continue;
367                        }
368                        switch (designationComponent.getName()) {
369                                case "language":
370                                        conceptDesignation.setLanguage(designationComponentValue.toString());
371                                        break;
372                                case "use":
373                                        org.hl7.fhir.dstu3.model.Coding coding =
374                                                        (org.hl7.fhir.dstu3.model.Coding) designationComponentValue;
375                                        conceptDesignation.setUseSystem(coding.getSystem());
376                                        conceptDesignation.setUseCode(coding.getCode());
377                                        conceptDesignation.setUseDisplay(coding.getDisplay());
378                                        break;
379                                case "value":
380                                        conceptDesignation.setValue(designationComponent.getValue().toString());
381                                        break;
382                        }
383                }
384                return conceptDesignation;
385        }
386
387        private LookupCodeResult generateLookupCodeResultR4(String theCode, String theSystem, Parameters outcomeR4) {
388                // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding
389                //       several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in
390                // POM).
391                LookupCodeResult result = new LookupCodeResult();
392                result.setSearchedForCode(theCode);
393                result.setSearchedForSystem(theSystem);
394                result.setFound(true);
395                for (ParametersParameterComponent parameterComponent : outcomeR4.getParameter()) {
396                        String parameterTypeAsString = Objects.toString(parameterComponent.getValue(), null);
397                        switch (parameterComponent.getName()) {
398                                case "property":
399                                        Property part = parameterComponent.getChildByName("part");
400                                        // The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be
401                                        // saved
402                                        if (part == null || part.getValues().size() < 2) {
403                                                continue;
404                                        }
405                                        BaseConceptProperty conceptProperty = createBaseConceptPropertyR4(part.getValues());
406                                        if (conceptProperty != null) {
407                                                result.getProperties().add(conceptProperty);
408                                        }
409                                        break;
410                                case "designation":
411                                        ConceptDesignation conceptDesignation = createConceptDesignationR4(parameterComponent);
412                                        result.getDesignations().add(conceptDesignation);
413                                        break;
414                                case "name":
415                                        result.setCodeSystemDisplayName(parameterTypeAsString);
416                                        break;
417                                case "version":
418                                        result.setCodeSystemVersion(parameterTypeAsString);
419                                        break;
420                                case "display":
421                                        result.setCodeDisplay(parameterTypeAsString);
422                                        break;
423                                case "abstract":
424                                        result.setCodeIsAbstract(Boolean.parseBoolean(parameterTypeAsString));
425                                        break;
426                        }
427                }
428                return result;
429        }
430
431        private static BaseConceptProperty createBaseConceptPropertyR4(List<Base> values) {
432                ParametersParameterComponent part1 = (ParametersParameterComponent) values.get(0);
433                String propertyName = ((CodeType) part1.getValue()).getValue();
434
435                ParametersParameterComponent part2 = (ParametersParameterComponent) values.get(1);
436
437                Type value = part2.getValue();
438                if (value == null) {
439                        return null;
440                }
441                BaseConceptProperty conceptProperty;
442                String fhirType = value.fhirType();
443                switch (fhirType) {
444                        case IValidationSupport.TYPE_STRING:
445                                StringType stringType = (StringType) part2.getValue();
446                                conceptProperty = new StringConceptProperty(propertyName, stringType.getValue());
447                                break;
448                        case IValidationSupport.TYPE_CODING:
449                                Coding coding = (Coding) part2.getValue();
450                                conceptProperty = new CodingConceptProperty(
451                                                propertyName, coding.getSystem(), coding.getCode(), coding.getDisplay());
452                                break;
453                                // TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699
454                        default:
455                                // other types will not fail for Remote Terminology
456                                conceptProperty = new StringConceptProperty(propertyName, value.toString());
457                }
458                return conceptProperty;
459        }
460
461        private static BaseConceptProperty createConceptPropertyR4(final String theName, final Type theValue) {
462                BaseConceptProperty conceptProperty;
463
464                String fhirType = theValue.fhirType();
465                switch (fhirType) {
466                        case IValidationSupport.TYPE_STRING:
467                                StringType stringType = (StringType) theValue;
468                                conceptProperty = new StringConceptProperty(theName, stringType.getValue());
469                                break;
470                        case IValidationSupport.TYPE_CODING:
471                                Coding coding = (Coding) theValue;
472                                conceptProperty =
473                                                new CodingConceptProperty(theName, coding.getSystem(), coding.getCode(), coding.getDisplay());
474                                break;
475                                // TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699
476                        default:
477                                // other types will not fail for Remote Terminology
478                                conceptProperty = new StringConceptProperty(theName, theValue.toString());
479                }
480                return conceptProperty;
481        }
482
483        private ConceptDesignation createConceptDesignationR4(ParametersParameterComponent theParameterComponent) {
484                ConceptDesignation conceptDesignation = new ConceptDesignation();
485                for (ParametersParameterComponent designationComponent : theParameterComponent.getPart()) {
486                        Type designationComponentValue = designationComponent.getValue();
487                        if (designationComponentValue == null) {
488                                continue;
489                        }
490                        switch (designationComponent.getName()) {
491                                case "language":
492                                        conceptDesignation.setLanguage(designationComponentValue.toString());
493                                        break;
494                                case "use":
495                                        Coding coding = (Coding) designationComponentValue;
496                                        conceptDesignation.setUseSystem(coding.getSystem());
497                                        conceptDesignation.setUseCode(coding.getCode());
498                                        conceptDesignation.setUseDisplay(coding.getDisplay());
499                                        break;
500                                case "value":
501                                        conceptDesignation.setValue(designationComponentValue.toString());
502                                        break;
503                        }
504                }
505                return conceptDesignation;
506        }
507
508        @Override
509        public IBaseResource fetchValueSet(String theValueSetUrl) {
510                // force the remote server to send the whole resource.
511                SummaryEnum summaryParam = SummaryEnum.FALSE;
512                return fetchValueSet(theValueSetUrl, summaryParam);
513        }
514
515        /**
516         * Search for a ValueSet by canonical url via IGenericClient.
517         *
518         * @param theValueSetUrl the canonical url of the ValueSet
519         * @param theSummaryParam force a summary mode - null allows server default
520         * @return the ValueSet or null if none match the url
521         */
522        @Nullable
523        private IBaseResource fetchValueSet(String theValueSetUrl, SummaryEnum theSummaryParam) {
524                IGenericClient client = provideClient();
525                Class<? extends IBaseBundle> bundleType =
526                                myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
527
528                IQuery<IBaseBundle> valueSetQuery = client.search()
529                                .forResource("ValueSet")
530                                .where(CodeSystem.URL.matches().value(theValueSetUrl));
531
532                if (theSummaryParam != null) {
533                        valueSetQuery.summaryMode(theSummaryParam);
534                }
535
536                IBaseBundle results = valueSetQuery.returnBundle(bundleType).execute();
537
538                List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
539                if (!resultsList.isEmpty()) {
540                        return resultsList.get(0);
541                }
542
543                return null;
544        }
545
546        @Override
547        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
548                // a summary is ok if we are just checking the presence.
549                SummaryEnum summaryParam = null;
550
551                return fetchCodeSystem(theSystem, summaryParam) != null;
552        }
553
554        @Override
555        public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
556                // a summary is ok if we are just checking the presence.
557                SummaryEnum summaryParam = null;
558
559                return fetchValueSet(theValueSetUrl, summaryParam) != null;
560        }
561
562        @Override
563        public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
564                IGenericClient client = provideClient();
565                FhirContext fhirContext = client.getFhirContext();
566
567                IBaseParameters params = RemoteTerminologyUtil.buildTranslateInputParameters(fhirContext, theRequest);
568
569                IBaseParameters outcome = client.operation()
570                                .onType("ConceptMap")
571                                .named("$translate")
572                                .withParameters(params)
573                                .execute();
574
575                return RemoteTerminologyUtil.translateOutcomeToResults(fhirContext, outcome);
576        }
577
578        private IGenericClient provideClient() {
579                IGenericClient retVal = myCtx.newRestfulGenericClient(myBaseUrl);
580                for (Object next : myClientInterceptors) {
581                        retVal.registerInterceptor(next);
582                }
583                return retVal;
584        }
585
586        protected CodeValidationResult invokeRemoteValidateCode(
587                        String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) {
588                if (isBlank(theCode)) {
589                        return null;
590                }
591
592                IGenericClient client = provideClient();
593
594                IBaseParameters input =
595                                buildValidateCodeInputParameters(theCodeSystem, theCode, theDisplay, theValueSetUrl, theValueSet);
596
597                String resourceType = "ValueSet";
598                if (theValueSet == null && theValueSetUrl == null) {
599                        resourceType = "CodeSystem";
600                }
601
602                IBaseParameters output = client.operation()
603                                .onType(resourceType)
604                                .named("validate-code")
605                                .withParameters(input)
606                                .execute();
607
608                List<String> resultValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "result");
609                if (resultValues.isEmpty() || isBlank(resultValues.get(0))) {
610                        return null;
611                }
612                Validate.isTrue(resultValues.size() == 1, "Response contained %d 'result' values", resultValues.size());
613
614                boolean success = "true".equalsIgnoreCase(resultValues.get(0));
615
616                CodeValidationResult retVal = new CodeValidationResult();
617                if (success) {
618
619                        retVal.setCode(theCode);
620                        List<String> displayValues =
621                                        ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "display");
622                        if (!displayValues.isEmpty()) {
623                                retVal.setDisplay(displayValues.get(0));
624                        }
625
626                } else {
627
628                        retVal.setSeverity(IssueSeverity.ERROR);
629                        List<String> messageValues =
630                                        ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "message");
631                        if (!messageValues.isEmpty()) {
632                                retVal.setMessage(messageValues.get(0));
633                        }
634                }
635                return retVal;
636        }
637
638        protected IBaseParameters buildValidateCodeInputParameters(
639                        String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) {
640                IBaseParameters params = ParametersUtil.newInstance(getFhirContext());
641
642                if (theValueSet == null && theValueSetUrl == null) {
643                        ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "url", theCodeSystem);
644                        ParametersUtil.addParameterToParametersString(getFhirContext(), params, "code", theCode);
645                        if (isNotBlank(theDisplay)) {
646                                ParametersUtil.addParameterToParametersString(getFhirContext(), params, "display", theDisplay);
647                        }
648                        return params;
649                }
650
651                if (isNotBlank(theValueSetUrl)) {
652                        ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "url", theValueSetUrl);
653                }
654                ParametersUtil.addParameterToParametersString(getFhirContext(), params, "code", theCode);
655                if (isNotBlank(theCodeSystem)) {
656                        ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "system", theCodeSystem);
657                }
658                if (isNotBlank(theDisplay)) {
659                        ParametersUtil.addParameterToParametersString(getFhirContext(), params, "display", theDisplay);
660                }
661                if (theValueSet != null) {
662                        ParametersUtil.addParameterToParameters(getFhirContext(), params, "valueSet", theValueSet);
663                }
664                return params;
665        }
666
667        /**
668         * Sets the FHIR Terminology Server base URL
669         *
670         * @param theBaseUrl The base URL, e.g. "<a href="https://hapi.fhir.org/baseR4">...</a>"
671         */
672        public void setBaseUrl(String theBaseUrl) {
673                Validate.notBlank(theBaseUrl, "theBaseUrl must be provided");
674                myBaseUrl = theBaseUrl;
675        }
676
677        /**
678         * Adds an interceptor that will be registered to all clients.
679         * <p>
680         * Note that this method is not thread-safe and should only be called prior to this module
681         * being used.
682         * </p>
683         *
684         * @param theClientInterceptor The interceptor (must not be null)
685         */
686        public void addClientInterceptor(@Nonnull Object theClientInterceptor) {
687                Validate.notNull(theClientInterceptor, "theClientInterceptor must not be null");
688                myClientInterceptors.add(theClientInterceptor);
689        }
690}