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.client.api.IRestfulClientFactory;
015import ca.uhn.fhir.rest.gclient.IQuery;
016import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
017import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
018import ca.uhn.fhir.util.BundleUtil;
019import ca.uhn.fhir.util.Logs;
020import ca.uhn.fhir.util.ParametersUtil;
021import jakarta.annotation.Nonnull;
022import jakarta.annotation.Nullable;
023import org.apache.commons.lang3.StringUtils;
024import org.apache.commons.lang3.Validate;
025import org.hl7.fhir.instance.model.api.IBaseBundle;
026import org.hl7.fhir.instance.model.api.IBaseDatatype;
027import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
028import org.hl7.fhir.instance.model.api.IBaseParameters;
029import org.hl7.fhir.instance.model.api.IBaseResource;
030import org.hl7.fhir.r4.model.Base;
031import org.hl7.fhir.r4.model.BooleanType;
032import org.hl7.fhir.r4.model.CodeSystem;
033import org.hl7.fhir.r4.model.CodeType;
034import org.hl7.fhir.r4.model.CodeableConcept;
035import org.hl7.fhir.r4.model.Coding;
036import org.hl7.fhir.r4.model.OperationOutcome;
037import org.hl7.fhir.r4.model.Parameters;
038import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
039import org.hl7.fhir.r4.model.Property;
040import org.hl7.fhir.r4.model.StringType;
041import org.hl7.fhir.r4.model.Type;
042import org.slf4j.Logger;
043
044import java.util.ArrayList;
045import java.util.Collection;
046import java.util.List;
047import java.util.Objects;
048import java.util.Optional;
049import java.util.stream.Collectors;
050
051import static ca.uhn.fhir.util.ParametersUtil.getNamedParameterResource;
052import static ca.uhn.fhir.util.ParametersUtil.getNamedParameterValueAsString;
053import static org.apache.commons.lang3.StringUtils.isBlank;
054import static org.apache.commons.lang3.StringUtils.isNotBlank;
055
056/**
057 * This class is an implementation of {@link IValidationSupport} that fetches validation codes
058 * from a remote FHIR based terminology server. It will invoke the FHIR
059 * <a href="http://hl7.org/fhir/valueset-operation-validate-code.html">ValueSet/$validate-code</a>
060 * operation in order to validate codes.
061 */
062public class RemoteTerminologyServiceValidationSupport extends BaseValidationSupport implements IValidationSupport {
063        private static final Logger ourLog = Logs.getTerminologyTroubleshootingLog();
064
065        public static final String ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM = "unknownCodeInSystem";
066        public static final String ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET = "unknownCodeInValueSet";
067
068        private String myBaseUrl;
069        private final List<Object> myClientInterceptors = new ArrayList<>();
070
071        @Nullable
072        private final IRestfulClientFactory myRestfulClientFactory;
073
074        /**
075         * Constructor
076         *
077         * @param theFhirContext The FhirContext. Will be used to create a FHIR client for remote terminology requests.
078         */
079        public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext) {
080                this(theFhirContext, null);
081        }
082
083        /**
084         * Constructor
085         *
086         * @param theFhirContext The FhirContext. Will be used to create a FHIR client for remote terminology requests.
087         * @param theBaseUrl The url used for the remote terminology FHIR client.
088         */
089        public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext, String theBaseUrl) {
090                this(theFhirContext, theBaseUrl, null);
091        }
092
093        /**
094         * Constructor
095         *
096         * @param theFhirContext The FhirContext.
097         * @param theBaseUrl The url used for the remote terminology FHIR client.
098         * @param theRestfulClientFactory Used to create the remote terminology FHIR client. If this is not supplied, a client will be created from the FhirContext
099         */
100        public RemoteTerminologyServiceValidationSupport(
101                        FhirContext theFhirContext, String theBaseUrl, @Nullable IRestfulClientFactory theRestfulClientFactory) {
102                super(theFhirContext);
103                myBaseUrl = theBaseUrl;
104                myRestfulClientFactory = theRestfulClientFactory;
105        }
106
107        @Override
108        public String getName() {
109                return getFhirContext().getVersion().getVersion() + " Remote Terminology Service Validation Support";
110        }
111
112        @Override
113        public CodeValidationResult validateCode(
114                        ValidationSupportContext theValidationSupportContext,
115                        ConceptValidationOptions theOptions,
116                        String theCodeSystem,
117                        String theCode,
118                        String theDisplay,
119                        String theValueSetUrl) {
120
121                return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, theValueSetUrl, null);
122        }
123
124        @Override
125        public CodeValidationResult validateCodeInValueSet(
126                        ValidationSupportContext theValidationSupportContext,
127                        ConceptValidationOptions theOptions,
128                        String theCodeSystem,
129                        String theCode,
130                        String theDisplay,
131                        @Nonnull IBaseResource theValueSet) {
132
133                IBaseResource valueSet = theValueSet;
134
135                // some external validators require the system when the code is passed
136                // so let's try to get it from the VS if is not present
137                String codeSystem = theCodeSystem;
138                if (isNotBlank(theCode) && isBlank(codeSystem)) {
139                        codeSystem = ValidationSupportUtils.extractCodeSystemForCode(theValueSet, theCode);
140                }
141
142                String valueSetUrl = DefaultProfileValidationSupport.getConformanceResourceUrl(myCtx, valueSet);
143                if (isNotBlank(valueSetUrl)) {
144                        valueSet = null;
145                } else {
146                        valueSetUrl = null;
147                }
148                return invokeRemoteValidateCode(codeSystem, theCode, theDisplay, valueSetUrl, valueSet);
149        }
150
151        @Override
152        public IBaseResource fetchCodeSystem(String theSystem) {
153                // callers of this want the whole resource.
154                return fetchCodeSystem(theSystem, SummaryEnum.FALSE);
155        }
156
157        /**
158         * Fetch the code system, possibly a summary.
159         * @param theSystem the canonical url
160         * @param theSummaryParam to force a summary mode - or null to allow server default.
161         * @return the CodeSystem
162         */
163        @Nullable
164        private IBaseResource fetchCodeSystem(String theSystem, @Nullable SummaryEnum theSummaryParam) {
165                IGenericClient client = provideClient();
166                Class<? extends IBaseBundle> bundleType =
167                                myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
168                IQuery<IBaseBundle> codeSystemQuery = client.search()
169                                .forResource("CodeSystem")
170                                .where(CodeSystem.URL.matches().value(theSystem));
171
172                if (theSummaryParam != null) {
173                        codeSystemQuery.summaryMode(theSummaryParam);
174                }
175
176                IBaseBundle results = codeSystemQuery.returnBundle(bundleType).execute();
177                List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
178                if (!resultsList.isEmpty()) {
179                        return resultsList.get(0);
180                }
181
182                return null;
183        }
184
185        @Override
186        public LookupCodeResult lookupCode(
187                        ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) {
188                final String code = theLookupCodeRequest.getCode();
189                final String system = theLookupCodeRequest.getSystem();
190                final String displayLanguage = theLookupCodeRequest.getDisplayLanguage();
191                Validate.notBlank(code, "theCode must be provided");
192
193                IGenericClient client = provideClient();
194                FhirContext fhirContext = client.getFhirContext();
195                FhirVersionEnum fhirVersion = fhirContext.getVersion().getVersion();
196
197                if (fhirVersion.isNewerThan(FhirVersionEnum.R4) || fhirVersion.isOlderThan(FhirVersionEnum.DSTU3)) {
198                        throw new UnsupportedOperationException(Msg.code(710) + "Unsupported FHIR version '"
199                                        + fhirVersion.getFhirVersionString() + "'. Only DSTU3 and R4 are supported.");
200                }
201
202                IBaseParameters params = ParametersUtil.newInstance(fhirContext);
203                ParametersUtil.addParameterToParametersString(fhirContext, params, "code", code);
204                if (!StringUtils.isEmpty(system)) {
205                        ParametersUtil.addParameterToParametersString(fhirContext, params, "system", system);
206                }
207                if (!StringUtils.isEmpty(displayLanguage)) {
208                        ParametersUtil.addParameterToParametersString(fhirContext, params, "language", displayLanguage);
209                }
210                for (String propertyName : theLookupCodeRequest.getPropertyNames()) {
211                        ParametersUtil.addParameterToParametersCode(fhirContext, params, "property", propertyName);
212                }
213                Class<? extends IBaseResource> codeSystemClass =
214                                myCtx.getResourceDefinition("CodeSystem").getImplementingClass();
215                IBaseParameters outcome;
216                try {
217                        outcome = client.operation()
218                                        .onType(codeSystemClass)
219                                        .named("$lookup")
220                                        .withParameters(params)
221                                        .useHttpGet()
222                                        .execute();
223                } catch (ResourceNotFoundException | InvalidRequestException e) {
224                        // this can potentially be moved to an interceptor and be reused in other areas
225                        // where we call a remote server or by the client as a custom interceptor
226                        // that interceptor would alter the status code of the response and the body into a different format
227                        // e.g. ClientResponseInterceptorModificationTemplate
228                        ourLog.error(e.getMessage(), e);
229                        LookupCodeResult result = LookupCodeResult.notFound(system, code);
230                        result.setErrorMessage(getErrorMessage(
231                                        ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, system, code, getBaseUrl(), e.getMessage()));
232                        return result;
233                }
234                if (outcome != null && !outcome.isEmpty()) {
235                        if (fhirVersion == FhirVersionEnum.DSTU3) {
236                                return generateLookupCodeResultDstu3(code, system, (org.hl7.fhir.dstu3.model.Parameters) outcome);
237                        }
238                        if (fhirVersion == FhirVersionEnum.R4) {
239                                return generateLookupCodeResultR4(code, system, (Parameters) outcome);
240                        }
241                }
242                return LookupCodeResult.notFound(system, code);
243        }
244
245        protected String getErrorMessage(String errorCode, Object... theParams) {
246                return getFhirContext().getLocalizer().getMessage(getClass(), errorCode, theParams);
247        }
248
249        private LookupCodeResult generateLookupCodeResultDstu3(
250                        String theCode, String theSystem, org.hl7.fhir.dstu3.model.Parameters outcomeDSTU3) {
251                // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding
252                // several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in
253                // POM).
254                LookupCodeResult result = new LookupCodeResult();
255                result.setSearchedForCode(theCode);
256                result.setSearchedForSystem(theSystem);
257                result.setFound(true);
258                for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent parameterComponent :
259                                outcomeDSTU3.getParameter()) {
260                        String parameterTypeAsString = Objects.toString(parameterComponent.getValue(), null);
261                        switch (parameterComponent.getName()) {
262                                case "property":
263                                        BaseConceptProperty conceptProperty = createConceptPropertyDstu3(parameterComponent);
264                                        if (conceptProperty != null) {
265                                                result.getProperties().add(conceptProperty);
266                                        }
267                                        break;
268                                case "designation":
269                                        ConceptDesignation conceptDesignation = createConceptDesignationDstu3(parameterComponent);
270                                        result.getDesignations().add(conceptDesignation);
271                                        break;
272                                case "name":
273                                        result.setCodeSystemDisplayName(parameterTypeAsString);
274                                        break;
275                                case "version":
276                                        result.setCodeSystemVersion(parameterTypeAsString);
277                                        break;
278                                case "display":
279                                        result.setCodeDisplay(parameterTypeAsString);
280                                        break;
281                                case "abstract":
282                                        result.setCodeIsAbstract(Boolean.parseBoolean(parameterTypeAsString));
283                                        break;
284                                default:
285                        }
286                }
287                return result;
288        }
289
290        private static BaseConceptProperty createConceptPropertyDstu3(
291                        org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent theParameterComponent) {
292                org.hl7.fhir.dstu3.model.Property property = theParameterComponent.getChildByName("part");
293
294                // The assumption here is that we may at east 2 elements in this part
295                if (property == null || property.getValues().size() < 2) {
296                        return null;
297                }
298
299                List<org.hl7.fhir.dstu3.model.Base> values = property.getValues();
300                org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent firstPart =
301                                (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(0);
302                String propertyName = ((org.hl7.fhir.dstu3.model.CodeType) firstPart.getValue()).getValue();
303
304                org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent secondPart =
305                                (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(1);
306                org.hl7.fhir.dstu3.model.Type value = secondPart.getValue();
307
308                if (value != null) {
309                        return createConceptPropertyDstu3(propertyName, value);
310                }
311
312                String groupName = secondPart.getName();
313                if (!"subproperty".equals(groupName)) {
314                        return null;
315                }
316
317                // handle property group (a property containing sub-properties)
318                GroupConceptProperty groupConceptProperty = new GroupConceptProperty(propertyName);
319
320                // we already retrieved the property name (group name) as first element, next will be the sub-properties.
321                // there is no dedicated value for a property group as it is an aggregate
322                for (int i = 1; i < values.size(); i++) {
323                        org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent nextPart =
324                                        (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) values.get(i);
325                        BaseConceptProperty subProperty = createConceptPropertyDstu3(nextPart);
326                        if (subProperty != null) {
327                                groupConceptProperty.addSubProperty(subProperty);
328                        }
329                }
330                return groupConceptProperty;
331        }
332
333        public static BaseConceptProperty createConceptProperty(final String theName, final IBaseDatatype theValue) {
334                if (theValue instanceof Type) {
335                        return createConceptPropertyR4(theName, (Type) theValue);
336                }
337                if (theValue instanceof org.hl7.fhir.dstu3.model.Type) {
338                        return createConceptPropertyDstu3(theName, (org.hl7.fhir.dstu3.model.Type) theValue);
339                }
340                return null;
341        }
342
343        private static BaseConceptProperty createConceptPropertyDstu3(
344                        final String theName, final org.hl7.fhir.dstu3.model.Type theValue) {
345                if (theValue == null) {
346                        return null;
347                }
348                BaseConceptProperty conceptProperty;
349                String fhirType = theValue.fhirType();
350                switch (fhirType) {
351                        case IValidationSupport.TYPE_STRING:
352                                org.hl7.fhir.dstu3.model.StringType stringType = (org.hl7.fhir.dstu3.model.StringType) theValue;
353                                conceptProperty = new StringConceptProperty(theName, stringType.getValue());
354                                break;
355                        case IValidationSupport.TYPE_CODING:
356                                org.hl7.fhir.dstu3.model.Coding coding = (org.hl7.fhir.dstu3.model.Coding) theValue;
357                                conceptProperty =
358                                                new CodingConceptProperty(theName, coding.getSystem(), coding.getCode(), coding.getDisplay());
359                                break;
360                                // TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699
361                        default:
362                                // other types will not fail for Remote Terminology
363                                conceptProperty = new StringConceptProperty(theName, theValue.toString());
364                }
365                return conceptProperty;
366        }
367
368        private ConceptDesignation createConceptDesignationDstu3(
369                        org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent theParameterComponent) {
370                ConceptDesignation conceptDesignation = new ConceptDesignation();
371                for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent designationComponent :
372                                theParameterComponent.getPart()) {
373                        org.hl7.fhir.dstu3.model.Type designationComponentValue = designationComponent.getValue();
374                        if (designationComponentValue == null) {
375                                continue;
376                        }
377                        switch (designationComponent.getName()) {
378                                case "language":
379                                        conceptDesignation.setLanguage(designationComponentValue.toString());
380                                        break;
381                                case "use":
382                                        org.hl7.fhir.dstu3.model.Coding coding =
383                                                        (org.hl7.fhir.dstu3.model.Coding) designationComponentValue;
384                                        conceptDesignation.setUseSystem(coding.getSystem());
385                                        conceptDesignation.setUseCode(coding.getCode());
386                                        conceptDesignation.setUseDisplay(coding.getDisplay());
387                                        break;
388                                case "value":
389                                        conceptDesignation.setValue(designationComponent.getValue().toString());
390                                        break;
391                                default:
392                        }
393                }
394                return conceptDesignation;
395        }
396
397        private LookupCodeResult generateLookupCodeResultR4(String theCode, String theSystem, Parameters outcomeR4) {
398                // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding
399                //       several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in
400                // POM).
401                LookupCodeResult result = new LookupCodeResult();
402                result.setSearchedForCode(theCode);
403                result.setSearchedForSystem(theSystem);
404                result.setFound(true);
405                for (ParametersParameterComponent parameterComponent : outcomeR4.getParameter()) {
406                        String parameterTypeAsString = Objects.toString(parameterComponent.getValue(), null);
407                        switch (parameterComponent.getName()) {
408                                case "property":
409                                        BaseConceptProperty conceptProperty = createConceptPropertyR4(parameterComponent);
410                                        if (conceptProperty != null) {
411                                                result.getProperties().add(conceptProperty);
412                                        }
413                                        break;
414                                case "designation":
415                                        ConceptDesignation conceptDesignation = createConceptDesignationR4(parameterComponent);
416                                        result.getDesignations().add(conceptDesignation);
417                                        break;
418                                case "name":
419                                        result.setCodeSystemDisplayName(parameterTypeAsString);
420                                        break;
421                                case "version":
422                                        result.setCodeSystemVersion(parameterTypeAsString);
423                                        break;
424                                case "display":
425                                        result.setCodeDisplay(parameterTypeAsString);
426                                        break;
427                                case "abstract":
428                                        result.setCodeIsAbstract(Boolean.parseBoolean(parameterTypeAsString));
429                                        break;
430                                default:
431                        }
432                }
433                return result;
434        }
435
436        private static BaseConceptProperty createConceptPropertyR4(ParametersParameterComponent thePropertyComponent) {
437                Property property = thePropertyComponent.getChildByName("part");
438
439                // The assumption here is that we may at east 2 elements in this part
440                if (property == null || property.getValues().size() < 2) {
441                        return null;
442                }
443
444                List<Base> values = property.getValues();
445                ParametersParameterComponent firstPart = (ParametersParameterComponent) values.get(0);
446                String propertyName = ((CodeType) firstPart.getValue()).getValue();
447
448                ParametersParameterComponent secondPart = (ParametersParameterComponent) values.get(1);
449                Type value = secondPart.getValue();
450
451                if (value != null) {
452                        return createConceptPropertyR4(propertyName, value);
453                }
454
455                String groupName = secondPart.getName();
456                if (!"subproperty".equals(groupName)) {
457                        return null;
458                }
459
460                // handle property group (a property containing sub-properties)
461                GroupConceptProperty groupConceptProperty = new GroupConceptProperty(propertyName);
462
463                // we already retrieved the property name (group name) as first element, next will be the sub-properties.
464                // there is no dedicated value for a property group as it is an aggregate
465                for (int i = 1; i < values.size(); i++) {
466                        ParametersParameterComponent nextPart = (ParametersParameterComponent) values.get(i);
467                        BaseConceptProperty subProperty = createConceptPropertyR4(nextPart);
468                        if (subProperty != null) {
469                                groupConceptProperty.addSubProperty(subProperty);
470                        }
471                }
472                return groupConceptProperty;
473        }
474
475        private static BaseConceptProperty createConceptPropertyR4(final String theName, final Type theValue) {
476                BaseConceptProperty conceptProperty;
477
478                String fhirType = theValue.fhirType();
479                switch (fhirType) {
480                        case IValidationSupport.TYPE_STRING:
481                                StringType stringType = (StringType) theValue;
482                                conceptProperty = new StringConceptProperty(theName, stringType.getValue());
483                                break;
484                        case IValidationSupport.TYPE_BOOLEAN:
485                                BooleanType booleanType = (BooleanType) theValue;
486                                conceptProperty = new BooleanConceptProperty(theName, booleanType.getValue());
487                                break;
488                        case IValidationSupport.TYPE_CODING:
489                                Coding coding = (Coding) theValue;
490                                conceptProperty =
491                                                new CodingConceptProperty(theName, coding.getSystem(), coding.getCode(), coding.getDisplay());
492                                break;
493                                // TODO: add other property types as per FHIR spec https://github.com/hapifhir/hapi-fhir/issues/5699
494                        default:
495                                // other types will not fail for Remote Terminology
496                                conceptProperty = new StringConceptProperty(theName, theValue.toString());
497                }
498                return conceptProperty;
499        }
500
501        private ConceptDesignation createConceptDesignationR4(ParametersParameterComponent theParameterComponent) {
502                ConceptDesignation conceptDesignation = new ConceptDesignation();
503                for (ParametersParameterComponent designationComponent : theParameterComponent.getPart()) {
504                        Type designationComponentValue = designationComponent.getValue();
505                        if (designationComponentValue == null) {
506                                continue;
507                        }
508                        switch (designationComponent.getName()) {
509                                case "language":
510                                        conceptDesignation.setLanguage(designationComponentValue.toString());
511                                        break;
512                                case "use":
513                                        Coding coding = (Coding) designationComponentValue;
514                                        conceptDesignation.setUseSystem(coding.getSystem());
515                                        conceptDesignation.setUseCode(coding.getCode());
516                                        conceptDesignation.setUseDisplay(coding.getDisplay());
517                                        break;
518                                case "value":
519                                        conceptDesignation.setValue(designationComponentValue.toString());
520                                        break;
521                                default:
522                        }
523                }
524                return conceptDesignation;
525        }
526
527        @Override
528        public IBaseResource fetchValueSet(String theValueSetUrl) {
529                // force the remote server to send the whole resource.
530                SummaryEnum summaryParam = SummaryEnum.FALSE;
531                return fetchValueSet(theValueSetUrl, summaryParam);
532        }
533
534        /**
535         * Search for a ValueSet by canonical url via IGenericClient.
536         *
537         * @param theValueSetUrl the canonical url of the ValueSet
538         * @param theSummaryParam force a summary mode - null allows server default
539         * @return the ValueSet or null if none match the url
540         */
541        @Nullable
542        private IBaseResource fetchValueSet(String theValueSetUrl, SummaryEnum theSummaryParam) {
543                IGenericClient client = provideClient();
544                Class<? extends IBaseBundle> bundleType =
545                                myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
546
547                IQuery<IBaseBundle> valueSetQuery = client.search()
548                                .forResource("ValueSet")
549                                .where(CodeSystem.URL.matches().value(theValueSetUrl));
550
551                if (theSummaryParam != null) {
552                        valueSetQuery.summaryMode(theSummaryParam);
553                }
554
555                IBaseBundle results = valueSetQuery.returnBundle(bundleType).execute();
556
557                List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
558                if (!resultsList.isEmpty()) {
559                        return resultsList.get(0);
560                }
561
562                return null;
563        }
564
565        @Override
566        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
567                // a summary is ok if we are just checking the presence.
568                SummaryEnum summaryParam = null;
569
570                return fetchCodeSystem(theSystem, summaryParam) != null;
571        }
572
573        @Override
574        public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
575                // a summary is ok if we are just checking the presence.
576                SummaryEnum summaryParam = null;
577
578                return fetchValueSet(theValueSetUrl, summaryParam) != null;
579        }
580
581        @Override
582        public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
583                IGenericClient client = provideClient();
584                FhirContext fhirContext = client.getFhirContext();
585
586                IBaseParameters params = RemoteTerminologyUtil.buildTranslateInputParameters(fhirContext, theRequest);
587
588                IBaseParameters outcome = client.operation()
589                                .onType("ConceptMap")
590                                .named("$translate")
591                                .withParameters(params)
592                                .execute();
593
594                return RemoteTerminologyUtil.translateOutcomeToResults(fhirContext, outcome);
595        }
596
597        private IGenericClient provideClient() {
598                IGenericClient retVal;
599                if (myRestfulClientFactory != null) {
600                        retVal = myRestfulClientFactory.newGenericClient(myBaseUrl);
601                } else {
602                        retVal = myCtx.newRestfulGenericClient(myBaseUrl);
603                }
604                for (Object next : myClientInterceptors) {
605                        retVal.registerInterceptor(next);
606                }
607                return retVal;
608        }
609
610        public String getBaseUrl() {
611                return myBaseUrl;
612        }
613
614        protected CodeValidationResult invokeRemoteValidateCode(
615                        String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) {
616                if (isBlank(theCode)) {
617                        return null;
618                }
619
620                IGenericClient client = provideClient();
621
622                // this message builder can be removed once we introduce a parameter object like CodeValidationRequest
623                ValidationErrorMessageBuilder errorMessageBuilder = theServerMessage -> {
624                        if (theValueSetUrl == null && theValueSet == null) {
625                                return getErrorMessage(
626                                                ERROR_CODE_UNKNOWN_CODE_IN_CODE_SYSTEM, theCodeSystem, theCode, getBaseUrl(), theServerMessage);
627                        }
628                        return getErrorMessage(
629                                        ERROR_CODE_UNKNOWN_CODE_IN_VALUE_SET,
630                                        theCodeSystem,
631                                        theCode,
632                                        theValueSetUrl,
633                                        getBaseUrl(),
634                                        theServerMessage);
635                };
636
637                IBaseParameters input =
638                                buildValidateCodeInputParameters(theCodeSystem, theCode, theDisplay, theValueSetUrl, theValueSet);
639
640                String resourceType = "ValueSet";
641                if (theValueSet == null && theValueSetUrl == null) {
642                        resourceType = "CodeSystem";
643                }
644
645                try {
646                        IBaseParameters output = client.operation()
647                                        .onType(resourceType)
648                                        .named("validate-code")
649                                        .withParameters(input)
650                                        .execute();
651                        return createCodeValidationResult(output, errorMessageBuilder, theCode);
652                } catch (ResourceNotFoundException | InvalidRequestException ex) {
653                        ourLog.error(ex.getMessage(), ex);
654                        String errorMessage = errorMessageBuilder.buildErrorMessage(ex.getMessage());
655                        CodeValidationIssueCode issueCode = ex instanceof ResourceNotFoundException
656                                        ? CodeValidationIssueCode.NOT_FOUND
657                                        : CodeValidationIssueCode.CODE_INVALID;
658                        return createErrorCodeValidationResult(issueCode, errorMessage);
659                }
660        }
661
662        private CodeValidationResult createErrorCodeValidationResult(
663                        CodeValidationIssueCode theIssueCode, String theMessage) {
664                IssueSeverity severity = IssueSeverity.ERROR;
665                return new CodeValidationResult()
666                                .setSeverity(severity)
667                                .setMessage(theMessage)
668                                .addIssue(new CodeValidationIssue(
669                                                theMessage, severity, theIssueCode, CodeValidationIssueCoding.INVALID_CODE));
670        }
671
672        private CodeValidationResult createCodeValidationResult(
673                        IBaseParameters theOutput, ValidationErrorMessageBuilder theMessageBuilder, String theCode) {
674                final FhirContext fhirContext = getFhirContext();
675                Optional<String> resultValue = getNamedParameterValueAsString(fhirContext, theOutput, "result");
676
677                if (!resultValue.isPresent()) {
678                        throw new IllegalArgumentException(
679                                        Msg.code(2560) + "Parameter `result` is missing from the $validate-code response.");
680                }
681
682                boolean success = resultValue.get().equalsIgnoreCase("true");
683
684                CodeValidationResult result = new CodeValidationResult();
685
686                // TODO MM: avoid passing the code and only retrieve it from the response
687                // that implies larger changes, like adding the result boolean to CodeValidationResult
688                // since CodeValidationResult#isOk() relies on code being populated to determine the result/success
689                if (success) {
690                        result.setCode(theCode);
691                }
692
693                Optional<String> systemValue = getNamedParameterValueAsString(fhirContext, theOutput, "system");
694                systemValue.ifPresent(result::setCodeSystemName);
695                Optional<String> versionValue = getNamedParameterValueAsString(fhirContext, theOutput, "version");
696                versionValue.ifPresent(result::setCodeSystemVersion);
697                Optional<String> displayValue = getNamedParameterValueAsString(fhirContext, theOutput, "display");
698                displayValue.ifPresent(result::setDisplay);
699
700                // in theory the message and the issues should not be populated when result=false
701                if (success) {
702                        return result;
703                }
704
705                // for now assume severity ERROR, we may need to process the following for success cases as well
706                result.setSeverity(IssueSeverity.ERROR);
707
708                Optional<String> messageValue = getNamedParameterValueAsString(fhirContext, theOutput, "message");
709                messageValue.ifPresent(value -> result.setMessage(theMessageBuilder.buildErrorMessage(value)));
710
711                Optional<IBaseResource> issuesValue = getNamedParameterResource(fhirContext, theOutput, "issues");
712                if (issuesValue.isPresent()) {
713                        // it seems to be safe to cast to IBaseOperationOutcome as any other type would not reach this point
714                        createCodeValidationIssues(
715                                                        (IBaseOperationOutcome) issuesValue.get(),
716                                                        fhirContext.getVersion().getVersion())
717                                        .ifPresent(i -> i.forEach(result::addIssue));
718                } else {
719                        // create a validation issue out of the message
720                        // this is a workaround to overcome an issue in the FHIR Validator library
721                        // where ValueSet bindings are only reading issues but not messages
722                        // @see https://github.com/hapifhir/org.hl7.fhir.core/issues/1766
723                        result.addIssue(createCodeValidationIssue(result.getMessage()));
724                }
725                return result;
726        }
727
728        /**
729         * Creates a list of {@link ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssue} from the issues
730         * returned by the $validate-code operation.
731         * Please note that this method should only be used for Remote Terminology for now as it only translates
732         * issues text/message and assumes all other fields.
733         * When issues will be supported across all validators in hapi-fhir, a proper generic conversion method should
734         * be available and this method will be deleted.
735         *
736         * @param theOperationOutcome the outcome of the $validate-code operation
737         * @param theFhirVersion the FHIR version
738         * @return the list of {@link ca.uhn.fhir.context.support.IValidationSupport.CodeValidationIssue}
739         */
740        public static Optional<Collection<CodeValidationIssue>> createCodeValidationIssues(
741                        IBaseOperationOutcome theOperationOutcome, FhirVersionEnum theFhirVersion) {
742                if (theFhirVersion == FhirVersionEnum.R4) {
743                        return Optional.of(createCodeValidationIssuesR4((OperationOutcome) theOperationOutcome));
744                }
745                if (theFhirVersion == FhirVersionEnum.DSTU3) {
746                        return Optional.of(
747                                        createCodeValidationIssuesDstu3((org.hl7.fhir.dstu3.model.OperationOutcome) theOperationOutcome));
748                }
749                return Optional.empty();
750        }
751
752        private static Collection<CodeValidationIssue> createCodeValidationIssuesR4(OperationOutcome theOperationOutcome) {
753                return theOperationOutcome.getIssue().stream()
754                                .map(issueComponent -> {
755                                        String diagnostics = issueComponent.getDiagnostics();
756                                        IssueSeverity issueSeverity =
757                                                        IssueSeverity.fromCode(issueComponent.getSeverity().toCode());
758                                        String issueTypeCode = issueComponent.getCode().toCode();
759                                        CodeableConcept details = issueComponent.getDetails();
760                                        CodeValidationIssue issue = new CodeValidationIssue(diagnostics, issueSeverity, issueTypeCode);
761                                        CodeValidationIssueDetails issueDetails = new CodeValidationIssueDetails(details.getText());
762                                        details.getCoding().forEach(coding -> issueDetails.addCoding(coding.getSystem(), coding.getCode()));
763                                        issue.setDetails(issueDetails);
764                                        return issue;
765                                })
766                                .collect(Collectors.toList());
767        }
768
769        private static Collection<CodeValidationIssue> createCodeValidationIssuesDstu3(
770                        org.hl7.fhir.dstu3.model.OperationOutcome theOperationOutcome) {
771                return theOperationOutcome.getIssue().stream()
772                                .map(issueComponent -> {
773                                        String diagnostics = issueComponent.getDiagnostics();
774                                        IssueSeverity issueSeverity =
775                                                        IssueSeverity.fromCode(issueComponent.getSeverity().toCode());
776                                        String issueTypeCode = issueComponent.getCode().toCode();
777                                        org.hl7.fhir.dstu3.model.CodeableConcept details = issueComponent.getDetails();
778                                        CodeValidationIssue issue = new CodeValidationIssue(diagnostics, issueSeverity, issueTypeCode);
779                                        CodeValidationIssueDetails issueDetails = new CodeValidationIssueDetails(details.getText());
780                                        details.getCoding().forEach(coding -> issueDetails.addCoding(coding.getSystem(), coding.getCode()));
781                                        issue.setDetails(issueDetails);
782                                        return issue;
783                                })
784                                .collect(Collectors.toList());
785        }
786
787        private static CodeValidationIssue createCodeValidationIssue(String theMessage) {
788                return new CodeValidationIssue(
789                                theMessage,
790                                IssueSeverity.ERROR,
791                                CodeValidationIssueCode.INVALID,
792                                CodeValidationIssueCoding.INVALID_CODE);
793        }
794
795        public interface ValidationErrorMessageBuilder {
796                String buildErrorMessage(String theServerMessage);
797        }
798
799        protected IBaseParameters buildValidateCodeInputParameters(
800                        String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) {
801                final FhirContext fhirContext = getFhirContext();
802                IBaseParameters params = ParametersUtil.newInstance(fhirContext);
803
804                if (theValueSet == null && theValueSetUrl == null) {
805                        ParametersUtil.addParameterToParametersUri(fhirContext, params, "url", theCodeSystem);
806                        ParametersUtil.addParameterToParametersString(fhirContext, params, "code", theCode);
807                        if (isNotBlank(theDisplay)) {
808                                ParametersUtil.addParameterToParametersString(fhirContext, params, "display", theDisplay);
809                        }
810                        return params;
811                }
812
813                if (isNotBlank(theValueSetUrl)) {
814                        ParametersUtil.addParameterToParametersUri(fhirContext, params, "url", theValueSetUrl);
815                }
816                ParametersUtil.addParameterToParametersString(fhirContext, params, "code", theCode);
817                if (isNotBlank(theCodeSystem)) {
818                        ParametersUtil.addParameterToParametersUri(fhirContext, params, "system", theCodeSystem);
819                }
820                if (isNotBlank(theDisplay)) {
821                        ParametersUtil.addParameterToParametersString(fhirContext, params, "display", theDisplay);
822                }
823                if (theValueSet != null) {
824                        ParametersUtil.addParameterToParameters(fhirContext, params, "valueSet", theValueSet);
825                }
826                return params;
827        }
828
829        /**
830         * Sets the FHIR Terminology Server base URL
831         *
832         * @param theBaseUrl The base URL, e.g. "<a href="https://hapi.fhir.org/baseR4">...</a>"
833         */
834        public void setBaseUrl(String theBaseUrl) {
835                Validate.notBlank(theBaseUrl, "theBaseUrl must be provided");
836                myBaseUrl = theBaseUrl;
837        }
838
839        /**
840         * Adds an interceptor that will be registered to all clients.
841         * <p>
842         * Note that this method is not thread-safe and should only be called prior to this module
843         * being used.
844         * </p>
845         *
846         * @param theClientInterceptor The interceptor (must not be null)
847         */
848        public void addClientInterceptor(@Nonnull Object theClientInterceptor) {
849                Validate.notNull(theClientInterceptor, "theClientInterceptor must not be null");
850                myClientInterceptors.add(theClientInterceptor);
851        }
852}