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