001package org.hl7.fhir.common.hapi.validation.support;
002
003import ca.uhn.fhir.context.FhirContext;
004import ca.uhn.fhir.context.support.ConceptValidationOptions;
005import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
006import ca.uhn.fhir.context.support.IValidationSupport;
007import ca.uhn.fhir.context.support.ValidationSupportContext;
008import ca.uhn.fhir.rest.client.api.IGenericClient;
009import ca.uhn.fhir.util.BundleUtil;
010import ca.uhn.fhir.util.ParametersUtil;
011import org.apache.commons.lang3.Validate;
012import org.hl7.fhir.instance.model.api.IBaseBundle;
013import org.hl7.fhir.instance.model.api.IBaseParameters;
014import org.hl7.fhir.instance.model.api.IBaseResource;
015import org.hl7.fhir.r4.model.CodeSystem;
016
017import javax.annotation.Nonnull;
018import java.util.ArrayList;
019import java.util.List;
020
021import static org.apache.commons.lang3.StringUtils.isBlank;
022import static org.apache.commons.lang3.StringUtils.isNotBlank;
023
024/**
025 * This class is an implementation of {@link IValidationSupport} that fetches validation codes
026 * from a remote FHIR based terminology server. It will invoke the FHIR
027 * <a href="http://hl7.org/fhir/valueset-operation-validate-code.html">ValueSet/$validate-code</a>
028 * operation in order to validate codes.
029 */
030public class RemoteTerminologyServiceValidationSupport extends BaseValidationSupport implements IValidationSupport {
031
032        private String myBaseUrl;
033        private List<Object> myClientInterceptors = new ArrayList<>();
034
035        /**
036         * Constructor
037         *
038         * @param theFhirContext The FhirContext object to use
039         */
040        public RemoteTerminologyServiceValidationSupport(FhirContext theFhirContext) {
041                super(theFhirContext);
042        }
043
044        @Override
045        public CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
046                return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, theValueSetUrl, null);
047        }
048
049        @Override
050        public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
051
052                if (theOptions != null) {
053                        if (theOptions.isInferSystem()) {
054                                return null;
055                        }
056                }
057
058                IBaseResource valueSet = theValueSet;
059                String valueSetUrl = DefaultProfileValidationSupport.getConformanceResourceUrl(myCtx, valueSet);
060                if (isNotBlank(valueSetUrl)) {
061                        valueSet = null;
062                } else {
063                        valueSetUrl = null;
064                }
065                return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, valueSetUrl, valueSet);
066        }
067
068        @Override
069        public IBaseResource fetchCodeSystem(String theSystem) {
070                IGenericClient client = provideClient();
071                Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
072                IBaseBundle results = client
073                        .search()
074                        .forResource("CodeSystem")
075                        .where(CodeSystem.URL.matches().value(theSystem))
076                        .returnBundle(bundleType)
077                        .execute();
078                List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
079                if (resultsList.size() > 0) {
080                        return resultsList.get(0);
081                }
082
083                return null;
084        }
085
086        @Override
087        public IBaseResource fetchValueSet(String theValueSetUrl) {
088                IGenericClient client = provideClient();
089                Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
090                IBaseBundle results = client
091                        .search()
092                        .forResource("ValueSet")
093                        .where(CodeSystem.URL.matches().value(theValueSetUrl))
094                        .returnBundle(bundleType)
095                        .execute();
096                List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
097                if (resultsList.size() > 0) {
098                        return resultsList.get(0);
099                }
100
101                return null;
102        }
103
104        @Override
105        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
106                return fetchCodeSystem(theSystem) != null;
107        }
108
109        @Override
110        public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
111                return fetchValueSet(theValueSetUrl) != null;
112        }
113
114        private IGenericClient provideClient() {
115                IGenericClient retVal = myCtx.newRestfulGenericClient(myBaseUrl);
116                for (Object next : myClientInterceptors) {
117                        retVal.registerInterceptor(next);
118                }
119                return retVal;
120        }
121
122        protected CodeValidationResult invokeRemoteValidateCode(String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) {
123                if (isBlank(theCode)) {
124                        return null;
125                }
126
127                IGenericClient client = provideClient();
128
129                IBaseParameters input = ParametersUtil.newInstance(getFhirContext());
130
131                String resourceType = "ValueSet";
132                if (theValueSet == null && theValueSetUrl == null) {
133                        resourceType = "CodeSystem";
134
135                        ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "url", theCodeSystem);
136                        ParametersUtil.addParameterToParametersString(getFhirContext(), input, "code", theCode);
137                        if (isNotBlank(theDisplay)) {
138                                ParametersUtil.addParameterToParametersString(getFhirContext(), input, "display", theDisplay);
139                        }
140
141                } else {
142
143                        if (isNotBlank(theValueSetUrl)) {
144                                ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "url", theValueSetUrl);
145                        }
146                        ParametersUtil.addParameterToParametersString(getFhirContext(), input, "code", theCode);
147                        if (isNotBlank(theCodeSystem)) {
148                                ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "system", theCodeSystem);
149                        }
150                        if (isNotBlank(theDisplay)) {
151                                ParametersUtil.addParameterToParametersString(getFhirContext(), input, "display", theDisplay);
152                        }
153                        if (theValueSet != null) {
154                                ParametersUtil.addParameterToParameters(getFhirContext(), input, "valueSet", theValueSet);
155                        }
156
157                }
158
159
160                IBaseParameters output = client
161                        .operation()
162                        .onType(resourceType)
163                        .named("validate-code")
164                        .withParameters(input)
165                        .execute();
166
167                List<String> resultValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "result");
168                if (resultValues.size() < 1 || isBlank(resultValues.get(0))) {
169                        return null;
170                }
171                Validate.isTrue(resultValues.size() == 1, "Response contained %d 'result' values", resultValues.size());
172
173                boolean success = "true".equalsIgnoreCase(resultValues.get(0));
174
175                CodeValidationResult retVal = new CodeValidationResult();
176                if (success) {
177
178                        retVal.setCode(theCode);
179                        List<String> displayValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "display");
180                        if (displayValues.size() > 0) {
181                                retVal.setDisplay(displayValues.get(0));
182                        }
183
184                } else {
185
186                        retVal.setSeverity(IssueSeverity.ERROR);
187                        List<String> messageValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "message");
188                        if (messageValues.size() > 0) {
189                                retVal.setMessage(messageValues.get(0));
190                        }
191
192                }
193                return retVal;
194        }
195
196        /**
197         * Sets the FHIR Terminology Server base URL
198         *
199         * @param theBaseUrl The base URL, e.g. "https://hapi.fhir.org/baseR4"
200         */
201        public void setBaseUrl(String theBaseUrl) {
202                Validate.notBlank(theBaseUrl, "theBaseUrl must be provided");
203                myBaseUrl = theBaseUrl;
204        }
205
206        /**
207         * Adds an interceptor that will be registered to all clients.
208         * <p>
209         * Note that this method is not thread-safe and should only be called prior to this module
210         * being used.
211         * </p>
212         *
213         * @param theClientInterceptor The interceptor (must not be null)
214         */
215        public void addClientInterceptor(@Nonnull Object theClientInterceptor) {
216                Validate.notNull(theClientInterceptor, "theClientInterceptor must not be null");
217                myClientInterceptors.add(theClientInterceptor);
218        }
219
220}