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                IBaseResource valueSet = theValueSet;
052                String valueSetUrl = DefaultProfileValidationSupport.getConformanceResourceUrl(myCtx, valueSet);
053                if (isNotBlank(valueSetUrl)) {
054                        valueSet = null;
055                } else {
056                        valueSetUrl = null;
057                }
058                return invokeRemoteValidateCode(theCodeSystem, theCode, theDisplay, valueSetUrl, valueSet);
059        }
060
061        @Override
062        public IBaseResource fetchCodeSystem(String theSystem) {
063                IGenericClient client = provideClient();
064                Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
065                IBaseBundle results = client
066                        .search()
067                        .forResource("CodeSystem")
068                        .where(CodeSystem.URL.matches().value(theSystem))
069                        .returnBundle(bundleType)
070                        .execute();
071                List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
072                if (resultsList.size() > 0) {
073                        return resultsList.get(0);
074                }
075
076                return null;
077        }
078
079        @Override
080        public IBaseResource fetchValueSet(String theValueSetUrl) {
081                IGenericClient client = provideClient();
082                Class<? extends IBaseBundle> bundleType = myCtx.getResourceDefinition("Bundle").getImplementingClass(IBaseBundle.class);
083                IBaseBundle results = client
084                        .search()
085                        .forResource("ValueSet")
086                        .where(CodeSystem.URL.matches().value(theValueSetUrl))
087                        .returnBundle(bundleType)
088                        .execute();
089                List<IBaseResource> resultsList = BundleUtil.toListOfResources(myCtx, results);
090                if (resultsList.size() > 0) {
091                        return resultsList.get(0);
092                }
093
094                return null;
095        }
096
097        @Override
098        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
099                return fetchCodeSystem(theSystem) != null;
100        }
101
102        @Override
103        public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
104                return fetchValueSet(theValueSetUrl) != null;
105        }
106
107        private IGenericClient provideClient() {
108                IGenericClient retVal = myCtx.newRestfulGenericClient(myBaseUrl);
109                for (Object next : myClientInterceptors) {
110                        retVal.registerInterceptor(next);
111                }
112                return retVal;
113        }
114
115        protected CodeValidationResult invokeRemoteValidateCode(String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl, IBaseResource theValueSet) {
116                if (isBlank(theCode)) {
117                        return null;
118                }
119
120                IGenericClient client = provideClient();
121
122                IBaseParameters input = ParametersUtil.newInstance(getFhirContext());
123
124                String resourceType = "ValueSet";
125                if (theValueSet == null && theValueSetUrl == null) {
126                        resourceType = "CodeSystem";
127
128                        ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "url", theCodeSystem);
129                        ParametersUtil.addParameterToParametersString(getFhirContext(), input, "code", theCode);
130                        if (isNotBlank(theDisplay)) {
131                                ParametersUtil.addParameterToParametersString(getFhirContext(), input, "display", theDisplay);
132                        }
133
134                } else {
135
136                        if (isNotBlank(theValueSetUrl)) {
137                                ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "url", theValueSetUrl);
138                        }
139                        ParametersUtil.addParameterToParametersString(getFhirContext(), input, "code", theCode);
140                        if (isNotBlank(theCodeSystem)) {
141                                ParametersUtil.addParameterToParametersUri(getFhirContext(), input, "system", theCodeSystem);
142                        }
143                        if (isNotBlank(theDisplay)) {
144                                ParametersUtil.addParameterToParametersString(getFhirContext(), input, "display", theDisplay);
145                        }
146                        if (theValueSet != null) {
147                                ParametersUtil.addParameterToParameters(getFhirContext(), input, "valueSet", theValueSet);
148                        }
149
150                }
151
152
153                IBaseParameters output = client
154                        .operation()
155                        .onType(resourceType)
156                        .named("validate-code")
157                        .withParameters(input)
158                        .execute();
159
160                List<String> resultValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "result");
161                if (resultValues.size() < 1 || isBlank(resultValues.get(0))) {
162                        return null;
163                }
164                Validate.isTrue(resultValues.size() == 1, "Response contained %d 'result' values", resultValues.size());
165
166                boolean success = "true".equalsIgnoreCase(resultValues.get(0));
167
168                CodeValidationResult retVal = new CodeValidationResult();
169                if (success) {
170
171                        retVal.setCode(theCode);
172                        List<String> displayValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "display");
173                        if (displayValues.size() > 0) {
174                                retVal.setDisplay(displayValues.get(0));
175                        }
176
177                } else {
178
179                        retVal.setSeverity(IssueSeverity.ERROR);
180                        List<String> messageValues = ParametersUtil.getNamedParameterValuesAsString(getFhirContext(), output, "message");
181                        if (messageValues.size() > 0) {
182                                retVal.setMessage(messageValues.get(0));
183                        }
184
185                }
186                return retVal;
187        }
188
189        /**
190         * Sets the FHIR Terminology Server base URL
191         *
192         * @param theBaseUrl The base URL, e.g. "https://hapi.fhir.org/baseR4"
193         */
194        public void setBaseUrl(String theBaseUrl) {
195                Validate.notBlank(theBaseUrl, "theBaseUrl must be provided");
196                myBaseUrl = theBaseUrl;
197        }
198
199        /**
200         * Adds an interceptor that will be registered to all clients.
201         * <p>
202         * Note that this method is not thread-safe and should only be called prior to this module
203         * being used.
204         * </p>
205         *
206         * @param theClientInterceptor The interceptor (must not be null)
207         */
208        public void addClientInterceptor(@Nonnull Object theClientInterceptor) {
209                Validate.notNull(theClientInterceptor, "theClientInterceptor must not be null");
210                myClientInterceptors.add(theClientInterceptor);
211        }
212
213}