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.TranslateConceptResults;
009import ca.uhn.fhir.context.support.ValidationSupportContext;
010import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
011import ca.uhn.fhir.i18n.Msg;
012import org.apache.commons.lang3.Validate;
013import org.hl7.fhir.instance.model.api.IBaseResource;
014import org.hl7.fhir.instance.model.api.IPrimitiveType;
015
016import javax.annotation.Nonnull;
017import java.util.ArrayList;
018import java.util.HashSet;
019import java.util.List;
020import java.util.Optional;
021import java.util.Set;
022import java.util.function.Function;
023
024import static org.apache.commons.lang3.StringUtils.isBlank;
025
026public class ValidationSupportChain implements IValidationSupport {
027
028        private List<IValidationSupport> myChain;
029
030        /**
031         * Constructor
032         */
033        public ValidationSupportChain() {
034                myChain = new ArrayList<>();
035        }
036
037        /**
038         * Constructor
039         */
040        public ValidationSupportChain(IValidationSupport... theValidationSupportModules) {
041                this();
042                for (IValidationSupport next : theValidationSupportModules) {
043                        if (next != null) {
044                                addValidationSupport(next);
045                        }
046                }
047        }
048
049        @Override
050        public TranslateConceptResults translateConcept(TranslateCodeRequest theRequest) {
051                TranslateConceptResults retVal = null;
052                for (IValidationSupport next : myChain) {
053                        TranslateConceptResults translations = next.translateConcept(theRequest);
054                        if (translations != null) {
055                                if (retVal == null) {
056                                        retVal = new TranslateConceptResults();
057                                }
058
059                                if (retVal.getMessage() == null) {
060                                        retVal.setMessage(translations.getMessage());
061                                }
062
063                                if (translations.getResult() && !retVal.getResult()) {
064                                        retVal.setResult(translations.getResult());
065                                        retVal.setMessage(translations.getMessage());
066                                }
067
068                                retVal.getResults().addAll(translations.getResults());
069                        }
070                }
071                return retVal;
072        }
073
074        @Override
075        public void invalidateCaches() {
076                for (IValidationSupport next : myChain) {
077                        next.invalidateCaches();
078                }
079        }
080
081        @Override
082        public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
083                for (IValidationSupport next : myChain) {
084                        boolean retVal = next.isValueSetSupported(theValidationSupportContext, theValueSetUrl);
085                        if (retVal) {
086                                return true;
087                        }
088                }
089                return false;
090        }
091
092        @Override
093        public IBaseResource generateSnapshot(ValidationSupportContext theValidationSupportContext, IBaseResource theInput, String theUrl, String theWebUrl, String theProfileName) {
094                for (IValidationSupport next : myChain) {
095                        IBaseResource retVal = next.generateSnapshot(theValidationSupportContext, theInput, theUrl, theWebUrl, theProfileName);
096                        if (retVal != null) {
097                                return retVal;
098                        }
099                }
100                return null;
101        }
102
103        @Override
104        public FhirContext getFhirContext() {
105                if (myChain.size() == 0) {
106                        return null;
107                }
108                return myChain.get(0).getFhirContext();
109        }
110
111        /**
112         * Add a validation support module to the chain.
113         * <p>
114         * Note that this method is not thread-safe. All validation support modules should be added prior to use.
115         * </p>
116         *
117         * @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.
118         */
119        public void addValidationSupport(IValidationSupport theValidationSupport) {
120                int index = myChain.size();
121                addValidationSupport(index, theValidationSupport);
122        }
123
124        /**
125         * Add a validation support module to the chain at the given index.
126         * <p>
127         * Note that this method is not thread-safe. All validation support modules should be added prior to use.
128         * </p>
129         *
130         * @param theIndex             The index to add to
131         * @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.
132         */
133        public void addValidationSupport(int theIndex, IValidationSupport theValidationSupport) {
134                Validate.notNull(theValidationSupport, "theValidationSupport must not be null");
135
136                if (theValidationSupport.getFhirContext() == null) {
137                        String message = "Can not add validation support: getFhirContext() returns null";
138                        throw new ConfigurationException(Msg.code(708) + message);
139                }
140
141                FhirContext existingFhirContext = getFhirContext();
142                if (existingFhirContext != null) {
143                        FhirVersionEnum newVersion = theValidationSupport.getFhirContext().getVersion().getVersion();
144                        FhirVersionEnum existingVersion = existingFhirContext.getVersion().getVersion();
145                        if (!existingVersion.equals(newVersion)) {
146                                String message = "Trying to add validation support of version " + newVersion + " to chain with " + myChain.size() + " entries of version " + existingVersion;
147                                throw new ConfigurationException(Msg.code(709) + message);
148                        }
149                }
150
151                myChain.add(theIndex, theValidationSupport);
152        }
153
154        /**
155         * Removes an item from the chain. Note that this method is mostly intended for testing. Removing items from the chain while validation is
156         * actually occurring is not an expected use case for this class.
157         */
158        public void removeValidationSupport(IValidationSupport theValidationSupport) {
159                myChain.remove(theValidationSupport);
160        }
161
162        @Override
163        public ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, ValueSetExpansionOptions theExpansionOptions, @Nonnull IBaseResource theValueSetToExpand) {
164                for (IValidationSupport next : myChain) {
165                        // TODO: test if code system is supported?
166                        ValueSetExpansionOutcome expanded = next.expandValueSet(theValidationSupportContext, theExpansionOptions, theValueSetToExpand);
167                        if (expanded != null) {
168                                return expanded;
169                        }
170                }
171                return null;
172        }
173
174        @Override
175        public boolean isRemoteTerminologyServiceConfigured() {
176                if (myChain != null) {
177                        Optional<IValidationSupport> remoteTerminologyService = myChain.stream().filter(RemoteTerminologyServiceValidationSupport.class::isInstance).findFirst();
178                        if (remoteTerminologyService.isPresent()) {
179                                return true;
180                        }
181                }
182                return false;
183        }
184
185        @Override
186        public List<IBaseResource> fetchAllConformanceResources() {
187                List<IBaseResource> retVal = new ArrayList<>();
188                for (IValidationSupport next : myChain) {
189                        List<IBaseResource> candidates = next.fetchAllConformanceResources();
190                        if (candidates != null) {
191                                retVal.addAll(candidates);
192                        }
193                }
194                return retVal;
195        }
196
197        @Override
198        public List<IBaseResource> fetchAllStructureDefinitions() {
199                return doFetchStructureDefinitions(t->t.fetchAllStructureDefinitions());
200        }
201
202        @Override
203        public List<IBaseResource> fetchAllNonBaseStructureDefinitions() {
204                return doFetchStructureDefinitions(t->t.fetchAllNonBaseStructureDefinitions());
205        }
206
207        private List<IBaseResource> doFetchStructureDefinitions(Function<IValidationSupport, List<IBaseResource>> theFunction) {
208                ArrayList<IBaseResource> retVal = new ArrayList<>();
209                Set<String> urls = new HashSet<>();
210                for (IValidationSupport nextSupport : myChain) {
211                        List<IBaseResource> allStructureDefinitions = theFunction.apply(nextSupport);
212                        if (allStructureDefinitions != null) {
213                                for (IBaseResource next : allStructureDefinitions) {
214
215                                        IPrimitiveType<?> urlType = getFhirContext().newTerser().getSingleValueOrNull(next, "url", IPrimitiveType.class);
216                                        if (urlType == null || isBlank(urlType.getValueAsString()) || urls.add(urlType.getValueAsString())) {
217                                                retVal.add(next);
218                                        }
219                                }
220                        }
221                }
222                return retVal;
223        }
224
225        @Override
226        public IBaseResource fetchCodeSystem(String theSystem) {
227                for (IValidationSupport next : myChain) {
228                        IBaseResource retVal = next.fetchCodeSystem(theSystem);
229                        if (retVal != null) {
230                                return retVal;
231                        }
232                }
233                return null;
234        }
235
236        @Override
237        public IBaseResource fetchValueSet(String theUrl) {
238                for (IValidationSupport next : myChain) {
239                        IBaseResource retVal = next.fetchValueSet(theUrl);
240                        if (retVal != null) {
241                                return retVal;
242                        }
243                }
244                return null;
245        }
246
247
248        @Override
249        public <T extends IBaseResource> T fetchResource(Class<T> theClass, String theUri) {
250                for (IValidationSupport next : myChain) {
251                        T retVal = next.fetchResource(theClass, theUri);
252                        if (retVal != null) {
253                                return retVal;
254                        }
255                }
256                return null;
257        }
258
259        @Override
260        public IBaseResource fetchStructureDefinition(String theUrl) {
261                for (IValidationSupport next : myChain) {
262                        IBaseResource retVal = next.fetchStructureDefinition(theUrl);
263                        if (retVal != null) {
264                                return retVal;
265                        }
266                }
267                return null;
268        }
269
270        @Override
271        public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
272                for (IValidationSupport next : myChain) {
273                        if (next.isCodeSystemSupported(theValidationSupportContext, theSystem)) {
274                                return true;
275                        }
276                }
277                return false;
278        }
279
280        @Override
281        public CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
282                for (IValidationSupport next : myChain) {
283                        if (isBlank(theValueSetUrl) || next.isValueSetSupported(theValidationSupportContext, theValueSetUrl)) {
284                                if (theOptions.isInferSystem() || (theCodeSystem != null && next.isCodeSystemSupported(theValidationSupportContext, theCodeSystem))) {
285                                        CodeValidationResult retVal = next.validateCode(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSetUrl);
286                                        if (retVal != null) {
287                                                return retVal;
288                                        }
289                                }
290                        }
291                }
292                return null;
293        }
294
295        @Override
296        public CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, @Nonnull IBaseResource theValueSet) {
297                for (IValidationSupport next : myChain) {
298                        String url = CommonCodeSystemsTerminologyService.getValueSetUrl(theValueSet);
299                        if (isBlank(url) || next.isValueSetSupported(theValidationSupportContext, url)) {
300                                CodeValidationResult retVal = next.validateCodeInValueSet(theValidationSupportContext, theOptions, theCodeSystem, theCode, theDisplay, theValueSet);
301                                if (retVal != null) {
302                                        return retVal;
303                                }
304                        }
305                }
306                return null;
307        }
308
309        @Override
310        public LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theDisplayLanguage) {
311                for (IValidationSupport next : myChain) {
312                        if (next.isCodeSystemSupported(theValidationSupportContext, theSystem)) {
313                                return next.lookupCode(theValidationSupportContext, theSystem, theCode, theDisplayLanguage);
314                        }
315                }
316                return null;
317        }
318}