001package org.hl7.fhir.common.hapi.validation.validator;
002
003import ca.uhn.fhir.context.FhirContext;
004import ca.uhn.fhir.context.FhirVersionEnum;
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.validation.IInstanceValidatorModule;
009import ca.uhn.fhir.validation.IValidationContext;
010import org.apache.commons.lang3.Validate;
011import org.hl7.fhir.convertors.VersionConvertor_10_50;
012import org.hl7.fhir.exceptions.FHIRException;
013import org.hl7.fhir.exceptions.PathEngineException;
014import org.hl7.fhir.instance.model.api.IBaseResource;
015import org.hl7.fhir.r5.model.Base;
016import org.hl7.fhir.r5.model.Resource;
017import org.hl7.fhir.r5.model.TypeDetails;
018import org.hl7.fhir.r5.model.ValueSet;
019import org.hl7.fhir.r5.utils.FHIRPathEngine;
020import org.hl7.fhir.r5.utils.IResourceValidator;
021import org.hl7.fhir.r5.utils.IResourceValidator.BestPracticeWarningLevel;
022import org.hl7.fhir.utilities.validation.ValidationMessage;
023
024import javax.annotation.Nonnull;
025import java.util.Arrays;
026import java.util.Collections;
027import java.util.List;
028
029@SuppressWarnings({"PackageAccessibility", "Duplicates"})
030public class FhirInstanceValidator extends BaseValidatorBridge implements IInstanceValidatorModule {
031
032        private boolean myAnyExtensionsAllowed = true;
033        private BestPracticeWarningLevel myBestPracticeWarningLevel;
034        private IValidationSupport myValidationSupport;
035        private boolean noTerminologyChecks = false;
036        private volatile VersionSpecificWorkerContextWrapper myWrappedWorkerContext;
037        private boolean errorForUnknownProfiles;
038        private boolean assumeValidRestReferences;
039        private List<String> myExtensionDomains = Collections.emptyList();
040        private IResourceValidator.IValidatorResourceFetcher validatorResourceFetcher;
041        private volatile FhirContext myDstu2Context;
042        private volatile FhirContext myHl7OrgDstu2Context;
043
044        /**
045         * Constructor
046         * <p>
047         * Uses {@link DefaultProfileValidationSupport} for {@link IValidationSupport validation support}
048         */
049        public FhirInstanceValidator(FhirContext theContext) {
050                this(theContext.getValidationSupport());
051        }
052
053        /**
054         * Constructor which uses the given validation support
055         *
056         * @param theValidationSupport The validation support
057         */
058        public FhirInstanceValidator(IValidationSupport theValidationSupport) {
059                if (theValidationSupport.getFhirContext().getVersion().getVersion() == FhirVersionEnum.DSTU2) {
060                        myValidationSupport = new HapiToHl7OrgDstu2ValidatingSupportWrapper(theValidationSupport);
061                } else {
062                        myValidationSupport = theValidationSupport;
063                }
064        }
065
066        /**
067         * Every element in a resource or data type includes an optional <it>extension</it> child element
068         * which is identified by it's {@code url attribute}. There exists a number of predefined
069         * extension urls or extension domains:<ul>
070         * <li>any url which contains {@code example.org}, {@code nema.org}, or {@code acme.com}.</li>
071         * <li>any url which starts with {@code http://hl7.org/fhir/StructureDefinition/}.</li>
072         * </ul>
073         * It is possible to extend this list of known extension by defining custom extensions:
074         * Any url which starts which one of the elements in the list of custom extension domains is
075         * considered as known.
076         * <p>
077         * Any unknown extension domain will result in an information message when validating a resource.
078         * </p>
079         */
080        public FhirInstanceValidator setCustomExtensionDomains(List<String> extensionDomains) {
081                this.myExtensionDomains = extensionDomains;
082                return this;
083        }
084
085        /**
086         * Every element in a resource or data type includes an optional <it>extension</it> child element
087         * which is identified by it's {@code url attribute}. There exists a number of predefined
088         * extension urls or extension domains:<ul>
089         * <li>any url which contains {@code example.org}, {@code nema.org}, or {@code acme.com}.</li>
090         * <li>any url which starts with {@code http://hl7.org/fhir/StructureDefinition/}.</li>
091         * </ul>
092         * It is possible to extend this list of known extension by defining custom extensions:
093         * Any url which starts which one of the elements in the list of custom extension domains is
094         * considered as known.
095         * <p>
096         * Any unknown extension domain will result in an information message when validating a resource.
097         * </p>
098         */
099        public FhirInstanceValidator setCustomExtensionDomains(String... extensionDomains) {
100                this.myExtensionDomains = Arrays.asList(extensionDomains);
101                return this;
102        }
103
104        /**
105         * Returns the "best practice" warning level (default is {@link BestPracticeWarningLevel#Hint}).
106         * <p>
107         * The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is
108         * set to {@link BestPracticeWarningLevel#Error}, any resource data which does not meet these best practices will be
109         * reported at the ERROR level. If this setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice
110         * guielines will be ignored.
111         * </p>
112         *
113         * @see #setBestPracticeWarningLevel(BestPracticeWarningLevel)
114         */
115        public BestPracticeWarningLevel getBestPracticeWarningLevel() {
116                return myBestPracticeWarningLevel;
117        }
118
119        /**
120         * Sets the "best practice warning level". When validating, any deviations from best practices will be reported at
121         * this level.
122         * <p>
123         * The FHIR Instance Validator has a number of checks for best practices in terms of FHIR usage. If this setting is
124         * set to {@link BestPracticeWarningLevel#Error}, any resource data which does not meet these best practices will be
125         * reported at the ERROR level. If this setting is set to {@link BestPracticeWarningLevel#Ignore}, best practice
126         * guielines will be ignored.
127         * </p>
128         *
129         * @param theBestPracticeWarningLevel The level, must not be <code>null</code>
130         */
131        public void setBestPracticeWarningLevel(BestPracticeWarningLevel theBestPracticeWarningLevel) {
132                Validate.notNull(theBestPracticeWarningLevel);
133                myBestPracticeWarningLevel = theBestPracticeWarningLevel;
134        }
135
136        /**
137         * Returns the {@link IValidationSupport validation support} in use by this validator. Default is an instance of
138         * DefaultProfileValidationSupport if the no-arguments constructor for this object was used.
139         *
140         * @return
141         */
142        public IValidationSupport getValidationSupport() {
143                return myValidationSupport;
144        }
145
146        /**
147         * Sets the {@link IValidationSupport validation support} in use by this validator. Default is an instance of
148         * DefaultProfileValidationSupport if the no-arguments constructor for this object was used.
149         */
150        public void setValidationSupport(IValidationSupport theValidationSupport) {
151                myValidationSupport = theValidationSupport;
152                myWrappedWorkerContext = null;
153        }
154
155        /**
156         * If set to {@literal true} (default is true) extensions which are not known to the
157         * validator (e.g. because they have not been explicitly declared in a profile) will
158         * be validated but will not cause an error.
159         */
160        public boolean isAnyExtensionsAllowed() {
161                return myAnyExtensionsAllowed;
162        }
163
164        /**
165         * If set to {@literal true} (default is true) extensions which are not known to the
166         * validator (e.g. because they have not been explicitly declared in a profile) will
167         * be validated but will not cause an error.
168         */
169        public void setAnyExtensionsAllowed(boolean theAnyExtensionsAllowed) {
170                myAnyExtensionsAllowed = theAnyExtensionsAllowed;
171        }
172
173        public boolean isErrorForUnknownProfiles() {
174                return errorForUnknownProfiles;
175        }
176
177        public void setErrorForUnknownProfiles(boolean errorForUnknownProfiles) {
178                this.errorForUnknownProfiles = errorForUnknownProfiles;
179        }
180
181        /**
182         * If set to {@literal true} (default is false) the valueSet will not be validate
183         */
184        public boolean isNoTerminologyChecks() {
185                return noTerminologyChecks;
186        }
187
188        /**
189         * If set to {@literal true} (default is false) the valueSet will not be validate
190         */
191        public void setNoTerminologyChecks(final boolean theNoTerminologyChecks) {
192                noTerminologyChecks = theNoTerminologyChecks;
193        }
194
195        public List<String> getExtensionDomains() {
196                return myExtensionDomains;
197        }
198
199        @Override
200        protected List<ValidationMessage> validate(IValidationContext<?> theValidationCtx) {
201                VersionSpecificWorkerContextWrapper wrappedWorkerContext = provideWorkerContext();
202
203                return new ValidatorWrapper()
204                        .setAnyExtensionsAllowed(isAnyExtensionsAllowed())
205                        .setBestPracticeWarningLevel(getBestPracticeWarningLevel())
206                        .setErrorForUnknownProfiles(isErrorForUnknownProfiles())
207                        .setExtensionDomains(getExtensionDomains())
208                        .setNoTerminologyChecks(isNoTerminologyChecks())
209                        .setValidatorResourceFetcher(getValidatorResourceFetcher())
210                        .setAssumeValidRestReferences(isAssumeValidRestReferences())
211                        .validate(wrappedWorkerContext, theValidationCtx);
212        }
213
214        @Nonnull
215        protected VersionSpecificWorkerContextWrapper provideWorkerContext() {
216                VersionSpecificWorkerContextWrapper wrappedWorkerContext = myWrappedWorkerContext;
217                if (wrappedWorkerContext == null) {
218                        VersionSpecificWorkerContextWrapper.IVersionTypeConverter converter;
219
220                        switch (myValidationSupport.getFhirContext().getVersion().getVersion()) {
221                                case DSTU2:
222                                case DSTU2_HL7ORG: {
223                                        converter = new VersionSpecificWorkerContextWrapper.IVersionTypeConverter() {
224                                                @Override
225                                                public Resource toCanonical(IBaseResource theNonCanonical) {
226                                                        IBaseResource nonCanonical = theNonCanonical;
227                                                        Resource retVal = VersionConvertor_10_50.convertResource((org.hl7.fhir.dstu2.model.Resource) nonCanonical);
228                                                        if (nonCanonical instanceof org.hl7.fhir.dstu2.model.ValueSet) {
229                                                                org.hl7.fhir.dstu2.model.ValueSet valueSet = (org.hl7.fhir.dstu2.model.ValueSet) nonCanonical;
230                                                                if (valueSet.hasCodeSystem() && valueSet.getCodeSystem().hasSystem()) {
231                                                                        if (!valueSet.hasCompose()) {
232                                                                                ValueSet valueSetR5 = (ValueSet) retVal;
233                                                                                valueSetR5.getCompose().addInclude().setSystem(valueSet.getCodeSystem().getSystem());
234                                                                        }
235                                                                }
236                                                        }
237                                                        return retVal;
238                                                }
239
240                                                @Override
241                                                public IBaseResource fromCanonical(Resource theCanonical) {
242                                                        IBaseResource canonical = VersionConvertor_10_50.convertResource(theCanonical);
243                                                        return canonical;
244                                                }
245                                        };
246                                        break;
247                                }
248
249                                case DSTU2_1: {
250                                        converter = new VersionTypeConverterDstu21();
251                                        break;
252                                }
253
254                                case DSTU3: {
255                                        converter = new VersionTypeConverterDstu3();
256                                        break;
257                                }
258
259                                case R4: {
260                                        converter = new VersionTypeConverterR4();
261                                        break;
262                                }
263
264                                case R5: {
265                                        converter = VersionSpecificWorkerContextWrapper.IDENTITY_VERSION_TYPE_CONVERTER;
266                                        break;
267                                }
268
269                                default:
270                                        throw new IllegalStateException();
271                        }
272
273                        wrappedWorkerContext = new VersionSpecificWorkerContextWrapper(new ValidationSupportContext(myValidationSupport), converter);
274                }
275                myWrappedWorkerContext = wrappedWorkerContext;
276                return wrappedWorkerContext;
277        }
278
279        private FhirContext getDstu2Context() {
280                FhirContext dstu2Context = myDstu2Context;
281                if (dstu2Context == null) {
282                        dstu2Context = FhirContext.forDstu2();
283                        myDstu2Context = dstu2Context;
284                }
285                return dstu2Context;
286        }
287
288        private FhirContext getHl7OrgDstu2Context() {
289                FhirContext hl7OrgDstu2Context = myHl7OrgDstu2Context;
290                if (hl7OrgDstu2Context == null) {
291                        hl7OrgDstu2Context = FhirContext.forDstu2Hl7Org();
292                        myHl7OrgDstu2Context = hl7OrgDstu2Context;
293                }
294                return hl7OrgDstu2Context;
295        }
296
297        public IResourceValidator.IValidatorResourceFetcher getValidatorResourceFetcher() {
298                return validatorResourceFetcher;
299        }
300
301        public void setValidatorResourceFetcher(IResourceValidator.IValidatorResourceFetcher validatorResourceFetcher) {
302                this.validatorResourceFetcher = validatorResourceFetcher;
303        }
304
305        public boolean isAssumeValidRestReferences() {
306                return assumeValidRestReferences;
307        }
308
309        public void setAssumeValidRestReferences(boolean assumeValidRestReferences) {
310                this.assumeValidRestReferences = assumeValidRestReferences;
311        }
312
313        /**
314         * Clear any cached data held by the validator or any of its internal stores. This is mostly intended
315         * for unit tests, but could be used for production uses too.
316         */
317        public void invalidateCaches() {
318                myValidationSupport.invalidateCaches();
319                if (myWrappedWorkerContext != null) {
320                        myWrappedWorkerContext.invalidateCaches();
321                }
322        }
323
324
325        public static class NullEvaluationContext implements FHIRPathEngine.IEvaluationContext {
326                @Override
327                public Base resolveConstant(Object appContext, String name, boolean beforeContext) throws PathEngineException {
328                        return null;
329                }
330
331                @Override
332                public TypeDetails resolveConstantType(Object appContext, String name) throws PathEngineException {
333                        return null;
334                }
335
336                @Override
337                public boolean log(String argument, List<Base> focus) {
338                        return false;
339                }
340
341                @Override
342                public FunctionDetails resolveFunction(String functionName) {
343                        return null;
344                }
345
346                @Override
347                public TypeDetails checkFunction(Object appContext, String functionName, List<TypeDetails> parameters) throws PathEngineException {
348                        return null;
349                }
350
351                @Override
352                public List<Base> executeFunction(Object appContext, String functionName, List<List<Base>> parameters) {
353                        return null;
354                }
355
356                @Override
357                public Base resolveReference(Object appContext, String url, Base refContext) throws FHIRException {
358                        return null;
359                }
360
361                @Override
362                public boolean conformsToProfile(Object appContext, Base item, String url) throws FHIRException {
363                        return false;
364                }
365
366                @Override
367                public ValueSet resolveValueSet(Object appContext, String url) {
368                        return null;
369                }
370        }
371
372
373}