001package ca.uhn.fhir.validation;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2016 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import java.util.ArrayList;
024import java.util.Iterator;
025import java.util.List;
026
027import org.apache.commons.lang3.Validate;
028import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
029import org.hl7.fhir.instance.model.api.IBaseResource;
030
031import ca.uhn.fhir.context.FhirContext;
032import ca.uhn.fhir.model.api.Bundle;
033import ca.uhn.fhir.model.api.IResource;
034import ca.uhn.fhir.util.OperationOutcomeUtil;
035import ca.uhn.fhir.validation.schematron.SchematronProvider;
036
037/**
038 * Resource validator, which checks resources for compliance against various validation schemes (schemas, schematrons, profiles, etc.)
039 * 
040 * <p>
041 * To obtain a resource validator, call {@link FhirContext#newValidator()}
042 * </p>
043 * 
044 * <p>
045 * <b>Thread safety note:</b> This class is thread safe, so you may register or unregister validator modules at any time. Individual modules are not guaranteed to be thread safe however. Reconfigure
046 * them with caution.
047 * </p>
048 */
049public class FhirValidator {
050
051        private static final String I18N_KEY_NO_PHLOC_ERROR = FhirValidator.class.getName() + ".noPhlocError";
052
053        private static volatile Boolean ourPhlocPresentOnClasspath;
054        private final FhirContext myContext;
055        private List<IValidatorModule> myValidators = new ArrayList<IValidatorModule>();
056
057        /**
058         * Constructor (this should not be called directly, but rather {@link FhirContext#newValidator()} should be called to obtain an instance of {@link FhirValidator})
059         */
060        public FhirValidator(FhirContext theFhirContext) {
061                myContext = theFhirContext;
062
063                if (ourPhlocPresentOnClasspath == null) {
064                        ourPhlocPresentOnClasspath = SchematronProvider.isScematronAvailable(theFhirContext);
065                }
066        }
067
068        private void addOrRemoveValidator(boolean theValidateAgainstStandardSchema, Class<? extends IValidatorModule> type, IValidatorModule theInstance) {
069                if (theValidateAgainstStandardSchema) {
070                        boolean found = haveValidatorOfType(type);
071                        if (!found) {
072                                registerValidatorModule(theInstance);
073                        }
074                } else {
075                        for (Iterator<IValidatorModule> iter = myValidators.iterator(); iter.hasNext();) {
076                                IValidatorModule next = iter.next();
077                                if (next.getClass().equals(type)) {
078                                        unregisterValidatorModule(next);
079                                }
080                        }
081                }
082        }
083
084        private boolean haveValidatorOfType(Class<? extends IValidatorModule> type) {
085                boolean found = false;
086                for (IValidatorModule next : myValidators) {
087                        if (next.getClass().equals(type)) {
088                                found = true;
089                        }
090                }
091                return found;
092        }
093
094        /**
095         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
096         */
097        public synchronized boolean isValidateAgainstStandardSchema() {
098                return haveValidatorOfType(SchemaBaseValidator.class);
099        }
100
101        /**
102         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
103         */
104        public synchronized boolean isValidateAgainstStandardSchematron() {
105                if (!ourPhlocPresentOnClasspath) {
106                        return false;   // No need to ask since we dont have Phloc. Also Class.forname will complain
107                                                        // about missing phloc import.
108                }
109                Class<? extends IValidatorModule> cls = SchematronProvider.getSchematronValidatorClass();
110                return haveValidatorOfType(cls);
111        }
112
113        /**
114         * Add a new validator module to this validator. You may register as many modules as you like at any time.
115         * 
116         * @param theValidator
117         *           The validator module. Must not be null.
118         */
119        public synchronized void registerValidatorModule(IValidatorModule theValidator) {
120                Validate.notNull(theValidator, "theValidator must not be null");
121                ArrayList<IValidatorModule> newValidators = new ArrayList<IValidatorModule>(myValidators.size() + 1);
122                newValidators.addAll(myValidators);
123                newValidators.add(theValidator);
124
125                myValidators = newValidators;
126        }
127
128        /**
129         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
130         * 
131         * @return Returns a referens to <code>this<code> for method chaining
132         */
133        public synchronized  FhirValidator setValidateAgainstStandardSchema(boolean theValidateAgainstStandardSchema) {
134                addOrRemoveValidator(theValidateAgainstStandardSchema, SchemaBaseValidator.class, new SchemaBaseValidator(myContext));
135                return this;
136        }
137
138        /**
139         * Should the validator validate the resource against the base schematron (the schematron provided with the FHIR distribution itself)
140         * 
141         * @return Returns a referens to <code>this<code> for method chaining
142         */
143        public synchronized FhirValidator setValidateAgainstStandardSchematron(boolean theValidateAgainstStandardSchematron) {
144                if (theValidateAgainstStandardSchematron && !ourPhlocPresentOnClasspath) {
145                        throw new IllegalArgumentException(myContext.getLocalizer().getMessage(I18N_KEY_NO_PHLOC_ERROR));
146                }
147                Class<? extends IValidatorModule> cls = SchematronProvider.getSchematronValidatorClass();
148                IValidatorModule instance = SchematronProvider.getSchematronValidatorInstance(myContext);
149                addOrRemoveValidator(theValidateAgainstStandardSchematron, cls, instance);
150                return this;
151        }
152
153        /**
154         * Removes a validator module from this validator. You may register as many modules as you like, and remove them at any time.
155         * 
156         * @param theValidator
157         *           The validator module. Must not be null.
158         */
159        public synchronized void unregisterValidatorModule(IValidatorModule theValidator) {
160                Validate.notNull(theValidator, "theValidator must not be null");
161                ArrayList<IValidatorModule> newValidators = new ArrayList<IValidatorModule>(myValidators.size() + 1);
162                newValidators.addAll(myValidators);
163                newValidators.remove(theValidator);
164
165                myValidators = newValidators;
166        }
167
168        /**
169         * Validates a bundle instance, throwing a {@link ValidationFailureException} if the validation fails. This validation includes validation of all resources in the bundle.
170         * 
171         * @param theBundle
172         *           The resource to validate
173         * @throws ValidationFailureException
174         *            If the validation fails
175         * @deprecated use {@link #validateWithResult(ca.uhn.fhir.model.api.Bundle)} instead
176         */
177        @Deprecated
178        public void validate(Bundle theBundle) {
179                Validate.notNull(theBundle, "theBundle must not be null");
180                
181                applyDefaultValidators();
182                
183                IValidationContext<Bundle> ctx = ValidationContext.forBundle(myContext, theBundle);
184
185                for (IValidatorModule next : myValidators) {
186                        next.validateBundle(ctx);
187                }
188
189                if (ctx.toResult().isSuccessful() == false ) {
190                        IBaseOperationOutcome oo = ctx.toResult().toOperationOutcome();
191                        if (oo != null && OperationOutcomeUtil.hasIssues(myContext, oo)) {
192                                throw new ValidationFailureException(myContext, oo);
193                        }
194                }
195                
196        }
197
198        private void applyDefaultValidators() {
199                if (myValidators.isEmpty()) {
200                        setValidateAgainstStandardSchema(true);
201                        if (ourPhlocPresentOnClasspath) {
202                                setValidateAgainstStandardSchematron(true);
203                        }
204                }
205        }
206
207        /**
208         * Validates a resource instance, throwing a {@link ValidationFailureException} if the validation fails
209         * 
210         * @param theResource
211         *           The resource to validate
212         * @throws ValidationFailureException
213         *            If the validation fails
214         * @deprecated use {@link #validateWithResult(IBaseResource)} instead
215         */
216        @Deprecated
217        public void validate(IResource theResource) throws ValidationFailureException {
218                
219                applyDefaultValidators();
220                
221                ValidationResult validationResult = validateWithResult(theResource);
222                if (!validationResult.isSuccessful()) {
223                        throw new ValidationFailureException(myContext, validationResult.toOperationOutcome());
224                }
225        }
226
227        /**
228         * Validates a bundle instance returning a {@link ca.uhn.fhir.validation.ValidationResult} which contains the results. This validation includes validation of all resources in the bundle.
229         *
230         * @param theBundle
231         *           the bundle to validate
232         * @return the results of validation
233         * @since 0.7
234         */
235        public ValidationResult validateWithResult(Bundle theBundle) {
236                Validate.notNull(theBundle, "theBundle must not be null");
237                
238                applyDefaultValidators();
239                
240                IValidationContext<Bundle> ctx = ValidationContext.forBundle(myContext, theBundle);
241
242                for (IValidatorModule next : myValidators) {
243                        next.validateBundle(ctx);
244                }
245
246                return ctx.toResult();
247        }
248
249        /**
250         * Validates a resource instance returning a {@link ca.uhn.fhir.validation.ValidationResult} which contains the results.
251         *
252         * @param theResource
253         *           the resource to validate
254         * @return the results of validation
255         * @since 0.7
256         */
257        public ValidationResult validateWithResult(IBaseResource theResource) {
258                Validate.notNull(theResource, "theResource must not be null");
259                
260                applyDefaultValidators();
261                
262                IValidationContext<IBaseResource> ctx = ValidationContext.forResource(myContext, theResource);
263
264                for (IValidatorModule next : myValidators) {
265                        next.validateResource(ctx);
266                }
267
268                return ctx.toResult();
269        }
270
271        /**
272         * Validates a resource instance returning a {@link ca.uhn.fhir.validation.ValidationResult} which contains the results.
273         *
274         * @param theResource
275         *           the resource to validate
276         * @return the results of validation
277         * @since 1.1
278         */
279        public ValidationResult validateWithResult(String theResource) {
280                Validate.notNull(theResource, "theResource must not be null");
281                
282                applyDefaultValidators();
283                
284                IValidationContext<IBaseResource> ctx = ValidationContext.forText(myContext, theResource);
285
286                for (IValidatorModule next : myValidators) {
287                        next.validateResource(ctx);
288                }
289
290                return ctx.toResult();
291        }
292
293}