001package org.hl7.fhir.common.hapi.validation.support;
002
003import ca.uhn.fhir.context.ConfigurationException;
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.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.context.support.ValueSetExpansionOptions;
012import ca.uhn.fhir.i18n.Msg;
013import ca.uhn.fhir.util.Logs;
014import jakarta.annotation.Nonnull;
015import org.apache.commons.lang3.Validate;
016import org.hl7.fhir.instance.model.api.IBaseResource;
017import org.hl7.fhir.instance.model.api.IPrimitiveType;
018import org.slf4j.Logger;
019
020import java.util.ArrayList;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Optional;
024import java.util.Set;
025import java.util.function.Function;
026
027import static org.apache.commons.lang3.StringUtils.isBlank;
028import static org.apache.commons.lang3.StringUtils.isNotBlank;
029
030public class ValidationSupportChain implements IValidationSupport {
031        static Logger ourLog = Logs.getTerminologyTroubleshootingLog();
032
033        private List<IValidationSupport> myChain;
034
035        /**
036         * Constructor
037         */
038        public ValidationSupportChain() {
039                myChain = new ArrayList<>();
040        }
041
042        /**
043         * Constructor
044         */
045        public ValidationSupportChain(IValidationSupport... theValidationSupportModules) {
046                this();
047                for (IValidationSupport next : theValidationSupportModules) {
048                        if (next != null) {
049                                addValidationSupport(next);
050                        }
051                }
052        }
053
054        @Override
055        public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
056                TranslateConceptResults retVal = null;
057                for (IValidationSupport next : myChain) {
058                        TranslateConceptResults translations = next.translateConcept(theRequest);
059                        if (translations != null) {
060                                if (retVal == null) {
061                                        retVal = new TranslateConceptResults();
062                                }
063
064                                if (retVal.getMessage() == null) {
065                                        retVal.setMessage(translations.getMessage());
066                                }
067
068                                if (translations.getResult() && !retVal.getResult()) {
069                                        retVal.setResult(translations.getResult());
070                                        retVal.setMessage(translations.getMessage());
071                                }
072
073                                if (!translations.isEmpty()) {
074                                        if (ourLog.isDebugEnabled()) {
075                                                ourLog.debug(
076                                                                "{} found {} concept translation{} for {}",
077                                                                next.getName(),
078                                                                translations.size(),
079                                                                translations.size() > 1 ? "s" : "",
080                                                                theRequest);
081                                        }
082                                        retVal.getResults().addAll(translations.getResults());
083                                }
084                        }
085                }
086                return retVal;
087        }
088
089        @Override
090        public void invalidateCaches() {
091                ourLog.debug("Invalidating caches in {} validation support modules", myChain.size());
092                for (IValidationSupport next : myChain) {
093                        next.invalidateCaches();
094                }
095        }
096
097        @Override
098        public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
099                for (IValidationSupport next : myChain) {
100                        boolean retVal = next.isValueSetSupported(theValidationSupportContext, theValueSetUrl);
101                        if (retVal) {
102                                if (ourLog.isDebugEnabled()) {
103                                        ourLog.debug("ValueSet {} found in {}", theValueSetUrl, next.getName());
104                                }
105                                return true;
106                        }
107                }
108                return false;
109        }
110
111        @Override
112        public IBaseResource generateSnapshot(
113                        ValidationSupportContext theValidationSupportContext,
114                        IBaseResource theInput,
115                        String theUrl,
116                        String theWebUrl,
117                        String theProfileName) {
118                for (IValidationSupport next : myChain) {
119                        IBaseResource retVal =
120                                        next.generateSnapshot(theValidationSupportContext, theInput, theUrl, theWebUrl, theProfileName);
121                        if (retVal != null) {
122                                if (ourLog.isDebugEnabled()) {
123                                        ourLog.debug("Profile snapshot for {} generated by {}", theInput.getIdElement(), next.getName());
124                                }
125                                return retVal;
126                        }
127                }
128                return null;
129        }
130
131        @Override
132        public FhirContext getFhirContext() {
133                if (myChain.size() == 0) {
134                        return null;
135                }
136                return myChain.get(0).getFhirContext();
137        }
138
139        /**
140         * Add a validation support module to the chain.
141         * <p>
142         * Note that this method is not thread-safe. All validation support modules should be added prior to use.
143         * </p>
144         *
145         * @param theValidationSupport The validation support. Must not be null, and must have a {@link #getFhirContext() FhirContext} that is configured for the same FHIR version as other entries in the chain.
146         */
147        public void addValidationSupport(IValidationSupport theValidationSupport) {
148                int index = myChain.size();
149                addValidationSupport(index, theValidationSupport);
150        }
151
152        /**
153         * Add a validation support module to the chain at the given index.
154         * <p>
155         * Note that this method is not thread-safe. All validation support modules should be added prior to use.
156         * </p>
157         *
158         * @param theIndex             The index to add to
159         * @param theValidationSupport The validation support. Must not be null, and must have a {@link #getFhirContext() FhirContext} that is configured for the same FHIR version as other entries in the chain.
160         */
161        public void addValidationSupport(int theIndex, IValidationSupport theValidationSupport) {
162                Validate.notNull(theValidationSupport, "theValidationSupport must not be null");
163
164                if (theValidationSupport.getFhirContext() == null) {
165                        String message = "Can not add validation support: getFhirContext() returns null";
166                        throw new ConfigurationException(Msg.code(708) + message);
167                }
168
169                FhirContext existingFhirContext = getFhirContext();
170                if (existingFhirContext != null) {
171                        FhirVersionEnum newVersion =
172                                        theValidationSupport.getFhirContext().getVersion().getVersion();
173                        FhirVersionEnum existingVersion = existingFhirContext.getVersion().getVersion();
174                        if (!existingVersion.equals(newVersion)) {
175                                String message = "Trying to add validation support of version " + newVersion + " to chain with "
176                                                + myChain.size() + " entries of version " + existingVersion;
177                                throw new ConfigurationException(Msg.code(709) + message);
178                        }
179                }
180
181                myChain.add(theIndex, theValidationSupport);
182        }
183
184        /**
185         * Removes an item from the chain. Note that this method is mostly intended for testing. Removing items from the chain while validation is
186         * actually occurring is not an expected use case for this class.
187         */
188        public void removeValidationSupport(IValidationSupport theValidationSupport) {
189                myChain.remove(theValidationSupport);
190        }
191
192        @Override
193        public ValueSetExpansionOutcome expandValueSet(
194                        ValidationSupportContext theValidationSupportContext,
195                        ValueSetExpansionOptions theExpansionOptions,
196                        @Nonnull IBaseResource theValueSetToExpand) {
197                for (IValidationSupport next : myChain) {
198                        // TODO: test if code system is supported?
199                        ValueSetExpansionOutcome expanded =
200                                        next.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand);
201                        if (expanded != null) {
202                                if (ourLog.isDebugEnabled()) {
203                                        ourLog.debug("ValueSet {} expanded by {}", theValueSetToExpand.getIdElement(), next.getName());
204                                }
205                                return expanded;
206                        }
207                }
208                return null;
209        }
210
211        @Override
212        public boolean isRemoteTerminologyServiceConfigured() {
213                if (myChain != null) {
214                        Optional<IValidationSupport> remoteTerminologyService = myChain.stream()
215                                        .filter(RemoteTerminologyServiceValidationSupport.class::isInstance)
216                                        .findFirst();
217                        if (remoteTerminologyService.isPresent()) {
218                                return true;
219                        }
220                }
221                return false;
222        }
223
224        @Override
225        public List<IBaseResource> fetchAllConformanceResources() {
226                List<IBaseResource> retVal = new ArrayList<>();
227                for (IValidationSupport next : myChain) {
228                        List<IBaseResource> candidates = next.fetchAllConformanceResources();
229                        if (candidates != null) {
230                                retVal.addAll(candidates);
231                        }
232                }
233                return retVal;
234        }
235
236        @Override
237        public List<IBaseResource> fetchAllStructureDefinitions() {
238                return doFetchStructureDefinitions(t -> t.fetchAllStructureDefinitions());
239        }
240
241        @Override
242        public List<IBaseResource> fetchAllNonBaseStructureDefinitions() {
243                return doFetchStructureDefinitions(t -> t.fetchAllNonBaseStructureDefinitions());
244        }
245
246        private List<IBaseResource> doFetchStructureDefinitions(
247                        Function<IValidationSupport, List<IBaseResource>> theFunction) {
248                ArrayList<IBaseResource> retVal = new ArrayList<>();
249                Set<String> urls = new HashSet<>();
250                for (IValidationSupport nextSupport : myChain) {
251                        List<IBaseResource> allStructureDefinitions = theFunction.apply(nextSupport);
252                        if (allStructureDefinitions != null) {
253                                for (IBaseResource next : allStructureDefinitions) {
254
255                                        IPrimitiveType<?> urlType =
256                                                        getFhirContext().newTerser().getSingleValueOrNull(next, "url", IPrimitiveType.class);
257                                        if (urlType == null
258                                                        || isBlank(urlType.getValueAsString())
259                                                        || urls.add(urlType.getValueAsString())) {
260                                                retVal.add(next);
261                                        }
262                                }
263                        }
264                }
265                return retVal;
266        }
267
268        @Override
269        public IBaseResource fetchCodeSystem(String theSystem) {
270                for (IValidationSupport next : myChain) {
271                        IBaseResource retVal = next.fetchCodeSystem(theSystem);
272                        if (retVal != null) {
273                                if (ourLog.isDebugEnabled()) {
274                                        ourLog.debug(
275                                                        "CodeSystem {} with System {} fetched by {}",
276                                                        retVal.getIdElement(),
277                                                        theSystem,
278                                                        next.getName());
279                                }
280                                return retVal;
281                        }
282                }
283                return null;
284        }
285
286        @Override
287        public IBaseResource fetchValueSet(String theUrl) {
288                for (IValidationSupport next : myChain) {
289                        IBaseResource retVal = next.fetchValueSet(theUrl);
290                        if (retVal != null) {
291                                if (ourLog.isDebugEnabled()) {
292                                        ourLog.debug(
293                                                        "ValueSet {} with URL {} fetched by {}", retVal.getIdElement(), theUrl, next.getName());
294                                }
295                                return retVal;
296                        }
297                }
298                return null;
299        }
300
301        @Override
302        public <T extends IBaseResource> T fetchResource(Class<T> theClass, String theUri) {
303                for (IValidationSupport next : myChain) {
304                        T retVal = next.fetchResource(theClass, theUri);
305                        if (retVal != null) {
306                                if (ourLog.isDebugEnabled()) {
307                                        ourLog.debug(
308                                                        "Resource {} with URI {} fetched by {}", retVal.getIdElement(), theUri, next.getName());
309                                }
310                                return retVal;
311                        }
312                }
313                return null;
314        }
315
316        @Override
317        public byte[] fetchBinary(String key) {
318                for (IValidationSupport next : myChain) {
319                        byte[] retVal = next.fetchBinary(key);
320                        if (retVal != null) {
321                                if (ourLog.isDebugEnabled()) {
322                                        ourLog.debug("Binary with key {} fetched by {}", key, next.getName());
323                                }
324                                return retVal;
325                        }
326                }
327                return null;
328        }
329
330        @Override
331        public IBaseResource fetchStructureDefinition(String theUrl) {
332                for (IValidationSupport next : myChain) {
333                        IBaseResource retVal = next.fetchStructureDefinition(theUrl);
334                        if (retVal != null) {
335                                if (ourLog.isDebugEnabled()) {
336                                        ourLog.debug("StructureDefinition with URL {} fetched by {}", theUrl, next.getName());
337                                }
338                                return retVal;
339                        }
340                }
341                return null;
342        }
343
344        @Override
345        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
346                for (IValidationSupport next : myChain) {
347                        if (next.isCodeSystemSupported(theValidationSupportContext, theSystem)) {
348                                if (ourLog.isDebugEnabled()) {
349                                        ourLog.debug("CodeSystem with System {} is supported by {}", theSystem, next.getName());
350                                }
351                                return true;
352                        }
353                }
354                return false;
355        }
356
357        @Override
358        public CodeValidationResult validateCode(
359                        @Nonnull ValidationSupportContext theValidationSupportContext,
360                        @Nonnull ConceptValidationOptions theOptions,
361                        String theCodeSystem,
362                        String theCode,
363                        String theDisplay,
364                        String theValueSetUrl) {
365                for (IValidationSupport next : myChain) {
366                        if ((isBlank(theValueSetUrl) && next.isCodeSystemSupported(theValidationSupportContext, theCodeSystem))
367                                        || (isNotBlank(theValueSetUrl)
368                                                        && next.isValueSetSupported(theValidationSupportContext, theValueSetUrl))) {
369                                CodeValidationResult retVal = next.validateCode(
370                                                theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl);
371                                if (retVal != null) {
372                                        if (ourLog.isDebugEnabled()) {
373                                                ourLog.debug(
374                                                                "Code {}|{} '{}' in ValueSet {} validated by {}",
375                                                                theCodeSystem,
376                                                                theCode,
377                                                                theDisplay,
378                                                                theValueSetUrl,
379                                                                next.getName());
380                                        }
381                                        return retVal;
382                                }
383                        }
384                }
385                return null;
386        }
387
388        @Override
389        public CodeValidationResult validateCodeInValueSet(
390                        ValidationSupportContext theValidationSupportContext,
391                        ConceptValidationOptions theOptions,
392                        String theCodeSystem,
393                        String theCode,
394                        String theDisplay,
395                        @Nonnull IBaseResource theValueSet) {
396                for (IValidationSupport next : myChain) {
397                        String url = CommonCodeSystemsTerminologyService.getValueSetUrl(getFhirContext(), theValueSet);
398                        if (isBlank(url) || next.isValueSetSupported(theValidationSupportContext, url)) {
399                                CodeValidationResult retVal = next.validateCodeInValueSet(
400                                                theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSet);
401                                if (retVal != null) {
402                                        if (ourLog.isDebugEnabled()) {
403                                                ourLog.debug(
404                                                                "Code {}|{} '{}' in ValueSet {} validated by {}",
405                                                                theCodeSystem,
406                                                                theCode,
407                                                                theDisplay,
408                                                                theValueSet.getIdElement(),
409                                                                next.getName());
410                                        }
411                                        return retVal;
412                                }
413                        }
414                }
415                return null;
416        }
417
418        @Override
419        public LookupCodeResult lookupCode(
420                        ValidationSupportContext theValidationSupportContext, @Nonnull LookupCodeRequest theLookupCodeRequest) {
421                for (IValidationSupport next : myChain) {
422                        final String system = theLookupCodeRequest.getSystem();
423                        final String code = theLookupCodeRequest.getCode();
424                        final String displayLanguage = theLookupCodeRequest.getDisplayLanguage();
425                        if (next.isCodeSystemSupported(theValidationSupportContext, system)) {
426                                LookupCodeResult lookupCodeResult = next.lookupCode(theValidationSupportContext, theLookupCodeRequest);
427                                if (lookupCodeResult == null) {
428                                        /*
429                                        This branch has been added as a fall-back mechanism for supporting lookupCode
430                                        methods marked as deprecated in interface IValidationSupport.
431                                        */
432                                        lookupCodeResult = next.lookupCode(theValidationSupportContext, system, code, displayLanguage);
433                                }
434                                if (ourLog.isDebugEnabled()) {
435                                        ourLog.debug(
436                                                        "Code {}|{}{} {} by {}",
437                                                        system,
438                                                        code,
439                                                        isBlank(displayLanguage) ? "" : " (" + theLookupCodeRequest.getDisplayLanguage() + ")",
440                                                        lookupCodeResult != null && lookupCodeResult.isFound() ? "found" : "not found",
441                                                        next.getName());
442                                }
443                                return lookupCodeResult;
444                        }
445                }
446                return null;
447        }
448}