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