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.TranslateConceptResults;
009import ca.uhn.fhir.context.support.ValidationSupportContext;
010import ca.uhn.fhir.i18n.Msg;
011import ca.uhn.fhir.rest.client.api.IGenericClient;
012import ca.uhn.fhir.util.BundleUtil;
013import ca.uhn.fhir.util.ParametersUtil;
014import org.apache.commons.lang3.StringUtils;
015import org.apache.commons.lang3.Validate;
016import org.hl7.fhir.instance.model.api.IBaseBundle;
017import org.hl7.fhir.instance.model.api.IBaseParameters;
018import org.hl7.fhir.instance.model.api.IBaseResource;
019import org.hl7.fhir.r4.model.CodeSystem;
020import org.hl7.fhir.r4.model.ValueSet;
021import org.slf4j.Logger;
022import org.slf4j.LoggerFactory;
023
024import javax.annotation.Nonnull;
025import java.util.ArrayList;
026import java.util.List;
027
028import static org.apache.commons.lang3.StringUtils.isBlank;
029import static org.apache.commons.lang3.StringUtils.isNotBlank;
030
031/**
032 * This class is an implementation of {@link IValidationSupport} that fetches validation codes
033 * from a remote FHIR based terminology server. It will invoke the FHIR
034 * <a href="http://hl7.org/fhir/valueset-operation-validate-code.html">ValueSet/$validate-code</a>
035 * operation in order to validate codes.
036 */
037public class RemoteTerminologyServiceValidationSupport extends BaseValidationSupport implements IValidationSupport {
038        private static final Logger ourLog = LoggerFactory.getLogger(RemoteTerminologyServiceValidationSupport.class);
039
040        private String myBaseUrl;
041        private List<Object> myClientInterceptors = new ArrayList<>();
042
043        /**
044         * Constructor
045         *
046         * @param theFhirContext The FhirContext object to use
047         */
048        public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext) {
049                super(theFhirContext);
050        }
051
052        public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext, String theBaseUrl) {
053                super(theFhirContext);
054                myBaseUrl = theBaseUrl;
055        }
056
057        @Override
058        public CodeValidationResult validateCode(@Nonnull ValidationSupportContext theValidationSupportContext, @Nonnull ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
059                return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, theValueSetUrl, null);
060        }
061
062        @Override
063        public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
064
065                IBaseResource valueSet = theValueSet;
066
067                // some external validators require the system when the code is passed
068                // so let's try to get it from the VS if is is not present
069                String codeSystem = theCodeSystem;
070                if (isNotBlank(theCode) && isBlank(codeSystem)) {
071                        codeSystem = extractCodeSystemForCode((ValueSet) theValueSet, theCode);
072                }
073
074                // Remote terminology services shouldn't be used to validate codes with an implied system
075                if (isBlank(codeSystem)) { return null; }
076
077                String valueSetUrl = DefaultProfileValidationSupport.getConformanceResourceUrl(myCtx, valueSet);
078                if (isNotBlank(valueSetUrl)) {
079                        valueSet = null;
080                } else {
081                        valueSetUrl = null;
082                }
083                return invokeRemoteValidateCode(codeSystem, theCode, theDisplay, valueSetUrl, valueSet);
084        }
085
086        /**
087         * Try to obtain the codeSystem of the received code from the received ValueSet
088         */
089        private String extractCodeSystemForCode(ValueSet theValueSet, String theCode) {
090                if (theValueSet.getCompose() == null || theValueSet.getCompose().getInclude() == null
091                        || theValueSet.getCompose().getInclude().isEmpty()) {
092                        return null;
093                }
094
095                if (theValueSet.getCompose().getInclude().size() == 1) {
096                        ValueSet.ConceptSetComponent include = theValueSet.getCompose().getInclude().iterator().next();
097                        return getVersionedCodeSystem(include);
098                }
099
100                // when component has more than one include, their codeSystem(s) could be different, so we need to make sure
101                // that we are picking up the system for the include to which the code corresponds
102                for (ValueSet.ConceptSetComponent include: theValueSet.getCompose().getInclude()) {
103                        if (include.hasSystem()) {
104                                for (ValueSet.ConceptReferenceComponent concept : include.getConcept()) {
105                                        if (concept.hasCodeElement() && concept.getCode().equals(theCode)) {
106                                                return getVersionedCodeSystem(include);
107                                        }
108                                }
109                        }
110                }
111
112                // at this point codeSystem couldn't be extracted for a multi-include ValueSet. Just on case it was
113                // because the format was not well handled, let's allow to watch the VS by an easy logging change
114                ourLog.trace("CodeSystem couldn't be extracted for code: {} for ValueSet: {}", theCode, theValueSet.getId());
115                return null;
116        }
117
118        private String getVersionedCodeSystem(ValueSet.ConceptSetComponent theComponent) {
119                        String codeSystem = theComponent.getSystem();
120                        if ( ! codeSystem.contains("|") && theComponent.hasVersion()) {
121                                codeSystem += "|" + theComponent.getVersion();
122                        }
123                        return codeSystem;
124        }
125
126        @Override
127        public IBaseResource fetchCodeSystem(String theSystem) {
128                IGenericClient client = provideClient();
129                Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
130                IBaseBundle results = client
131                        .search()
132                        .forResource("CodeSystem")
133                        .where(CodeSystem.URL.matches().value(theSystem))
134                        .returnBundle(bundleType)
135                        .execute();
136                List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
137                if (resultsList.size() > 0) {
138                        return resultsList.get(0);
139                }
140
141                return null;
142        }
143
144        @Override
145        public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theDisplayLanguage) {
146                Validate.notBlank(theCode, "theCode must be provided");
147
148                IGenericClient client = provideClient();
149                FhirContext fhirContext = client.getFhirContext();
150                FhirVersionEnum fhirVersion = fhirContext.getVersion().getVersion();
151
152                switch (fhirVersion) {
153                        case DSTU3:
154                        case R4:
155                                IBaseParameters params = ParametersUtil.newInstance(fhirContext);
156                                ParametersUtil.addParameterToParametersString(fhirContext, params, "code", theCode);
157                                if (!StringUtils.isEmpty(theSystem)) {
158                                        ParametersUtil.addParameterToParametersString(fhirContext, params, "system", theSystem);
159                                }
160                                if (!StringUtils.isEmpty(theDisplayLanguage)) {
161                                        ParametersUtil.addParameterToParametersString(fhirContext, params, "language", theDisplayLanguage);
162                                }
163                                Class<?> codeSystemClass = myCtx.getResourceDefinition("CodeSystem").getImplementingClass();
164                                IBaseParameters outcome = client
165                                        .operation()
166                                        .onType((Class<? extends IBaseResource>) codeSystemClass)
167                                        .named("$lookup")
168                                        .withParameters(params)
169                                        .useHttpGet()
170                                        .execute();
171                                if (outcome != null && !outcome.isEmpty()) {
172                                        switch (fhirVersion) {
173                                                case DSTU3:
174                                                        return generateLookupCodeResultDSTU3(theCode, theSystem, (org.hl7.fhir.dstu3.model.Parameters)outcome);
175                                                case R4:
176                                                        return generateLookupCodeResultR4(theCode, theSystem, (org.hl7.fhir.r4.model.Parameters)outcome);
177                                        }
178                                }
179                                break;
180                        default:
181                                throw new UnsupportedOperationException(Msg.code(710) + "Unsupported FHIR version '" + fhirVersion.getFhirVersionString() +
182                                        "'. Only DSTU3 and R4 are supported.");
183                }
184                return null;
185        }
186
187        private LookupCodeResult generateLookupCodeResultDSTU3(String theCode, String theSystem, org.hl7.fhir.dstu3.model.Parameters outcomeDSTU3) {
188                // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding
189                //       several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in POM).
190                LookupCodeResult result = new LookupCodeResult();
191                result.setSearchedForCode(theCode);
192                result.setSearchedForSystem(theSystem);
193                result.setFound(true);
194                for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent parameterComponent : outcomeDSTU3.getParameter()) {
195                        switch (parameterComponent.getName()) {
196                                case "property":
197                                        org.hl7.fhir.dstu3.model.Property part = parameterComponent.getChildByName("part");
198                                        // The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be saved
199                                        if (part != null && part.hasValues() && part.getValues().size() >= 2) {
200                                                String key = ((org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) part.getValues().get(0)).getValue().toString();
201                                                String value = ((org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent) part.getValues().get(1)).getValue().toString();
202                                                if (!StringUtils.isEmpty(key) && !StringUtils.isEmpty(value)) {
203                                                        result.getProperties().add(new StringConceptProperty(key, value));
204                                                }
205                                        }
206                                        break;
207                                case "designation":
208                                        ConceptDesignation conceptDesignation = new ConceptDesignation();
209                                        for (org.hl7.fhir.dstu3.model.Parameters.ParametersParameterComponent designationComponent : parameterComponent.getPart()) {
210                                                switch(designationComponent.getName()) {
211                                                        case "language":
212                                                                conceptDesignation.setLanguage(designationComponent.getValue().toString());
213                                                                break;
214                                                        case "use":
215                                                                org.hl7.fhir.dstu3.model.Coding coding = (org.hl7.fhir.dstu3.model.Coding)designationComponent.getValue();
216                                                                if (coding != null) {
217                                                                        conceptDesignation.setUseSystem(coding.getSystem());
218                                                                        conceptDesignation.setUseCode(coding.getCode());
219                                                                        conceptDesignation.setUseDisplay(coding.getDisplay());
220                                                                }
221                                                                break;
222                                                        case "value":
223                                                                conceptDesignation.setValue(((designationComponent.getValue() == null)?null:designationComponent.getValue().toString()));
224                                                                break;
225                                                }
226                                        }
227                                        result.getDesignations().add(conceptDesignation);
228                                        break;
229                                case "name":
230                                        result.setCodeSystemDisplayName(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString()));
231                                        break;
232                                case "version":
233                                        result.setCodeSystemVersion(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString()));
234                                        break;
235                                case "display":
236                                        result.setCodeDisplay(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString()));
237                                        break;
238                                case "abstract":
239                                        result.setCodeIsAbstract(((parameterComponent.getValue() == null)?false:Boolean.parseBoolean(parameterComponent.getValue().toString())));
240                                        break;
241                        }
242                }
243                return result;
244        }
245
246        private LookupCodeResult generateLookupCodeResultR4(String theCode, String theSystem, org.hl7.fhir.r4.model.Parameters outcomeR4) {
247                // NOTE: I wanted to put all of this logic into the IValidationSupport Class, but it would've required adding
248                //       several new dependencies on version-specific libraries and that is explicitly forbidden (see comment in POM).
249                LookupCodeResult result = new LookupCodeResult();
250                result.setSearchedForCode(theCode);
251                result.setSearchedForSystem(theSystem);
252                result.setFound(true);
253                for (org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent parameterComponent : outcomeR4.getParameter()) {
254                        switch (parameterComponent.getName()) {
255                                case "property":
256                                        org.hl7.fhir.r4.model.Property part = parameterComponent.getChildByName("part");
257                                        // The assumption here is that we may only have 2 elements in this part, and if so, these 2 will be saved
258                                        if (part != null && part.hasValues() && part.getValues().size() >= 2) {
259                                                String key = ((org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent) part.getValues().get(0)).getValue().toString();
260                                                String value = ((org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent) part.getValues().get(1)).getValue().toString();
261                                                if (!StringUtils.isEmpty(key) && !StringUtils.isEmpty(value)) {
262                                                        result.getProperties().add(new StringConceptProperty(key, value));
263                                                }
264                                        }
265                                        break;
266                                case "designation":
267                                        ConceptDesignation conceptDesignation = new ConceptDesignation();
268                                        for (org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent designationComponent : parameterComponent.getPart()) {
269                                                switch(designationComponent.getName()) {
270                                                        case "language":
271                                                                conceptDesignation.setLanguage(designationComponent.getValue().toString());
272                                                                break;
273                                                        case "use":
274                                                                org.hl7.fhir.r4.model.Coding coding = (org.hl7.fhir.r4.model.Coding)designationComponent.getValue();
275                                                                if (coding != null) {
276                                                                        conceptDesignation.setUseSystem(coding.getSystem());
277                                                                        conceptDesignation.setUseCode(coding.getCode());
278                                                                        conceptDesignation.setUseDisplay(coding.getDisplay());
279                                                                }
280                                                                break;
281                                                        case "value":
282                                                                conceptDesignation.setValue(((designationComponent.getValue() == null)?null:designationComponent.getValue().toString()));
283                                                                break;
284                                                }
285                                        }
286                                        result.getDesignations().add(conceptDesignation);
287                                        break;
288                                case "name":
289                                        result.setCodeSystemDisplayName(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString()));
290                                        break;
291                                case "version":
292                                        result.setCodeSystemVersion(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString()));
293                                        break;
294                                case "display":
295                                        result.setCodeDisplay(((parameterComponent.getValue() == null)?null:parameterComponent.getValue().toString()));
296                                        break;
297                                case "abstract":
298                                        result.setCodeIsAbstract(((parameterComponent.getValue() == null)?false:Boolean.parseBoolean(parameterComponent.getValue().toString())));
299                                        break;
300                        }
301                }
302                return result;
303        }
304
305        @Override
306        public IBaseResource fetchValueSet(String theValueSetUrl) {
307                IGenericClient client = provideClient();
308                Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
309                IBaseBundle results = client
310                        .search()
311                        .forResource("ValueSet")
312                        .where(CodeSystem.URL.matches().value(theValueSetUrl))
313                        .returnBundle(bundleType)
314                        .execute();
315                List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
316                if (resultsList.size() > 0) {
317                        return resultsList.get(0);
318                }
319
320                return null;
321        }
322
323        @Override
324        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
325                return fetchCodeSystem(theSystem) != null;
326        }
327
328        @Override
329        public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
330                return fetchValueSet(theValueSetUrl) != null;
331        }
332
333        @Override
334        public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
335                IGenericClient client = provideClient();
336                FhirContext fhirContext = client.getFhirContext();
337
338                IBaseParameters params = RemoteTerminologyUtil.buildTranslateInputParameters(fhirContext, theRequest);
339
340                IBaseParameters outcome = client
341                        .operation()
342                        .onType("ConceptMap")
343                        .named("$translate")
344                        .withParameters(params)
345                        .execute();
346
347                return RemoteTerminologyUtil.translateOutcomeToResults(fhirContext, outcome);
348        }
349
350        private IGenericClient provideClient() {
351                IGenericClient retVal = myCtx.newRestfulGenericClient(myBaseUrl);
352                for (Object next : myClientInterceptors) {
353                        retVal.registerInterceptor(next);
354                }
355                return retVal;
356        }
357
358        protected CodeValidationResult invokeRemoteValidateCode(String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) {
359                if (isBlank(theCode)) {
360                        return null;
361                }
362
363                IGenericClient client = provideClient();
364
365                IBaseParameters input = buildValidateCodeInputParameters(theCodeSystem, theCode, theDisplay, theValueSetUrl, theValueSet);
366
367                String resourceType = "ValueSet";
368                if (theValueSet == null && theValueSetUrl == null) {
369                        resourceType = "CodeSystem";
370                }
371
372                IBaseParameters output = client
373                        .operation()
374                        .onType(resourceType)
375                        .named("validate-code")
376                        .withParameters(input)
377                        .execute();
378
379                List<String> resultValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "result");
380                if (resultValues.size() < 1 || isBlank(resultValues.get(0))) {
381                        return null;
382                }
383                Validate.isTrue(resultValues.size() == 1, "Response contained %d 'result' values", resultValues.size());
384
385                boolean success = "true".equalsIgnoreCase(resultValues.get(0));
386
387                CodeValidationResult retVal = new CodeValidationResult();
388                if (success) {
389
390                        retVal.setCode(theCode);
391                        List<String> displayValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "display");
392                        if (displayValues.size() > 0) {
393                                retVal.setDisplay(displayValues.get(0));
394                        }
395
396                } else {
397
398                        retVal.setSeverity(IssueSeverity.ERROR);
399                        List<String> messageValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "message");
400                        if (messageValues.size() > 0) {
401                                retVal.setMessage(messageValues.get(0));
402                        }
403
404                }
405                return retVal;
406        }
407
408        protected IBaseParameters buildValidateCodeInputParameters(String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) {
409                IBaseParameters params = ParametersUtil.newInstance(getFhirContext());
410
411                if (theValueSet == null && theValueSetUrl == null) {
412                        ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "url", theCodeSystem);
413                        ParametersUtil.addParameterToParametersString(getFhirContext(), params, "code", theCode);
414                        if (isNotBlank(theDisplay)) {
415                                ParametersUtil.addParameterToParametersString(getFhirContext(), params, "display", theDisplay);
416                        }
417                        return params;
418                }
419
420                if (isNotBlank(theValueSetUrl)) {
421                        ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "url", theValueSetUrl);
422                }
423                ParametersUtil.addParameterToParametersString(getFhirContext(), params, "code", theCode);
424                if (isNotBlank(theCodeSystem)) {
425                        ParametersUtil.addParameterToParametersUri(getFhirContext(), params, "system", theCodeSystem);
426                }
427                if (isNotBlank(theDisplay)) {
428                        ParametersUtil.addParameterToParametersString(getFhirContext(), params, "display", theDisplay);
429                }
430                if (theValueSet != null) {
431                        ParametersUtil.addParameterToParameters(getFhirContext(), params, "valueSet", theValueSet);
432                }
433                return params;
434        }
435
436        /**
437         * Sets the FHIR Terminology Server base URL
438         *
439         * @param theBaseUrl The base URL, e.g. "https://hapi.fhir.org/baseR4"
440         */
441        public void setBaseUrl(String theBaseUrl) {
442                Validate.notBlank(theBaseUrl, "theBaseUrl must be provided");
443                myBaseUrl = theBaseUrl;
444        }
445
446        /**
447         * Adds an interceptor that will be registered to all clients.
448         * <p>
449         * Note that this method is not thread-safe and should only be called prior to this module
450         * being used.
451         * </p>
452         *
453         * @param theClientInterceptor The interceptor (must not be null)
454         */
455        public void addClientInterceptor(@Nonnull Object theClientInterceptor) {
456                Validate.notNull(theClientInterceptor, "theClientInterceptor must not be null");
457                myClientInterceptors.add(theClientInterceptor);
458        }
459}