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