001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2023 Smile CDR, Inc.
006 * %%
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 * You may obtain a copy of the License at
010 *
011 *      http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 * #L%
019 */
020package ca.uhn.fhir.validation;
021
022import ca.uhn.fhir.context.FhirContext;
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.interceptor.api.HookParams;
025import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
026import ca.uhn.fhir.interceptor.api.Pointcut;
027import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
028import ca.uhn.fhir.util.BundleUtil;
029import ca.uhn.fhir.util.TerserUtil;
030import ca.uhn.fhir.validation.schematron.SchematronProvider;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.commons.lang3.Validate;
033import org.hl7.fhir.instance.model.api.IBaseBundle;
034import org.hl7.fhir.instance.model.api.IBaseResource;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import java.util.ArrayList;
039import java.util.List;
040import java.util.concurrent.ExecutionException;
041import java.util.concurrent.ExecutorService;
042import java.util.concurrent.Future;
043import java.util.function.Function;
044import java.util.stream.Collectors;
045import java.util.stream.IntStream;
046
047import static org.apache.commons.lang3.StringUtils.isBlank;
048
049
050/**
051 * Resource validator, which checks resources for compliance against various validation schemes (schemas, schematrons, profiles, etc.)
052 *
053 * <p>
054 * To obtain a resource validator, call {@link FhirContext#newValidator()}
055 * </p>
056 *
057 * <p>
058 * <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
059 * them with caution.
060 * </p>
061 */
062public class FhirValidator {
063        private static final Logger ourLog = LoggerFactory.getLogger(FhirValidator.class);
064
065        private static final String I18N_KEY_NO_PH_ERROR = FhirValidator.class.getName() + ".noPhError";
066
067        private static volatile Boolean ourPhPresentOnClasspath;
068        private final FhirContext myContext;
069        private List<IValidatorModule> myValidators = new ArrayList<>();
070        private IInterceptorBroadcaster myInterceptorBroadcaster;
071        private boolean myConcurrentBundleValidation;
072        private boolean mySkipContainedReferenceValidation;
073
074        private ExecutorService myExecutorService;
075
076        /**
077         * Constructor (this should not be called directly, but rather {@link FhirContext#newValidator()} should be called to obtain an instance of {@link FhirValidator})
078         */
079        public FhirValidator(FhirContext theFhirContext) {
080                myContext = theFhirContext;
081
082                if (ourPhPresentOnClasspath == null) {
083                        ourPhPresentOnClasspath = SchematronProvider.isSchematronAvailable(theFhirContext);
084                }
085        }
086
087        private void addOrRemoveValidator(boolean theValidateAgainstStandardSchema, Class<? extends IValidatorModule> type, IValidatorModule theInstance) {
088                if (theValidateAgainstStandardSchema) {
089                        boolean found = haveValidatorOfType(type);
090                        if (!found) {
091                                registerValidatorModule(theInstance);
092                        }
093                } else {
094                        for (IValidatorModule next : myValidators) {
095                                if (next.getClass().equals(type)) {
096                                        unregisterValidatorModule(next);
097                                }
098                        }
099                }
100        }
101
102        private boolean haveValidatorOfType(Class<? extends IValidatorModule> type) {
103                boolean found = false;
104                for (IValidatorModule next : myValidators) {
105                        if (next.getClass().equals(type)) {
106                                found = true;
107                                break;
108                        }
109                }
110                return found;
111        }
112
113        /**
114         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
115         */
116        public synchronized boolean isValidateAgainstStandardSchema() {
117                return haveValidatorOfType(SchemaBaseValidator.class);
118        }
119
120        /**
121         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
122         *
123         * @return Returns a referens to <code>this<code> for method chaining
124         */
125        public synchronized FhirValidator setValidateAgainstStandardSchema(boolean theValidateAgainstStandardSchema) {
126                addOrRemoveValidator(theValidateAgainstStandardSchema, SchemaBaseValidator.class, new SchemaBaseValidator(myContext));
127                return this;
128        }
129
130        /**
131         * Should the validator validate the resource against the base schema (the schema provided with the FHIR distribution itself)
132         */
133        public synchronized boolean isValidateAgainstStandardSchematron() {
134                if (!ourPhPresentOnClasspath) {
135                        // No need to ask since we dont have Ph-Schematron. Also Class.forname will complain
136                        // about missing ph-schematron import.
137                        return false;
138                }
139                Class<? extends IValidatorModule> cls = SchematronProvider.getSchematronValidatorClass();
140                return haveValidatorOfType(cls);
141        }
142
143        /**
144         * Should the validator validate the resource against the base schematron (the schematron provided with the FHIR distribution itself)
145         *
146         * @return Returns a referens to <code>this<code> for method chaining
147         */
148        public synchronized FhirValidator setValidateAgainstStandardSchematron(boolean theValidateAgainstStandardSchematron) {
149                if (theValidateAgainstStandardSchematron && !ourPhPresentOnClasspath) {
150                        throw new IllegalArgumentException(Msg.code(1970) + myContext.getLocalizer().getMessage(I18N_KEY_NO_PH_ERROR));
151                }
152                if (!theValidateAgainstStandardSchematron && !ourPhPresentOnClasspath) {
153                        return this;
154                }
155                Class<? extends IValidatorModule> cls = SchematronProvider.getSchematronValidatorClass();
156                IValidatorModule instance = SchematronProvider.getSchematronValidatorInstance(myContext);
157                addOrRemoveValidator(theValidateAgainstStandardSchematron, cls, instance);
158                return this;
159        }
160
161        /**
162         * Add a new validator module to this validator. You may register as many modules as you like at any time.
163         *
164         * @param theValidator The validator module. Must not be null.
165         * @return Returns a reference to <code>this</code> for easy method chaining.
166         */
167        public synchronized FhirValidator registerValidatorModule(IValidatorModule theValidator) {
168                Validate.notNull(theValidator, "theValidator must not be null");
169                ArrayList<IValidatorModule> newValidators = new ArrayList<>(myValidators.size() + 1);
170                newValidators.addAll(myValidators);
171                newValidators.add(theValidator);
172
173                myValidators = newValidators;
174                return this;
175        }
176
177        /**
178         * Removes a validator module from this validator. You may register as many modules as you like, and remove them at any time.
179         *
180         * @param theValidator The validator module. Must not be null.
181         */
182        public synchronized void unregisterValidatorModule(IValidatorModule theValidator) {
183                Validate.notNull(theValidator, "theValidator must not be null");
184                ArrayList<IValidatorModule> newValidators = new ArrayList<IValidatorModule>(myValidators.size() + 1);
185                newValidators.addAll(myValidators);
186                newValidators.remove(theValidator);
187
188                myValidators = newValidators;
189        }
190
191
192        private void applyDefaultValidators() {
193                if (myValidators.isEmpty()) {
194                        setValidateAgainstStandardSchema(true);
195                        if (ourPhPresentOnClasspath) {
196                                setValidateAgainstStandardSchematron(true);
197                        }
198                }
199        }
200
201
202        /**
203         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
204         *
205         * @param theResource the resource to validate
206         * @return the results of validation
207         * @since 0.7
208         */
209        public ValidationResult validateWithResult(IBaseResource theResource) {
210                return validateWithResult(theResource, null);
211        }
212
213        /**
214         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
215         *
216         * @param theResource the resource to validate
217         * @return the results of validation
218         * @since 1.1
219         */
220        public ValidationResult validateWithResult(String theResource) {
221                return validateWithResult(theResource, null);
222        }
223
224        /**
225         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
226         *
227         * @param theResource the resource to validate
228         * @param theOptions  Optionally provides options to the validator
229         * @return the results of validation
230         * @since 4.0.0
231         */
232        public ValidationResult validateWithResult(String theResource, ValidationOptions theOptions) {
233                Validate.notNull(theResource, "theResource must not be null");
234                IValidationContext<IBaseResource> validationContext = ValidationContext.forText(myContext, theResource, theOptions);
235                Function<ValidationResult, ValidationResult> callback = result -> invokeValidationCompletedHooks(null, theResource, result);
236                return doValidate(validationContext, theOptions, callback);
237        }
238
239        /**
240         * Validates a resource instance returning a {@link ValidationResult} which contains the results.
241         *
242         * @param theResource the resource to validate
243         * @param theOptions  Optionally provides options to the validator
244         * @return the results of validation
245         * @since 4.0.0
246         */
247        public ValidationResult validateWithResult(IBaseResource theResource, ValidationOptions theOptions) {
248                Validate.notNull(theResource, "theResource must not be null");
249                IValidationContext<IBaseResource> validationContext = ValidationContext.forResource(myContext, theResource, theOptions);
250                Function<ValidationResult, ValidationResult> callback = result -> invokeValidationCompletedHooks(theResource, null, result);
251                return doValidate(validationContext, theOptions, callback);
252        }
253
254        private ValidationResult doValidate(IValidationContext<IBaseResource> theValidationContext, ValidationOptions theOptions,
255                                                                                                        Function<ValidationResult, ValidationResult> theValidationCompletionCallback) {
256                applyDefaultValidators();
257
258                ValidationResult result;
259                if (myConcurrentBundleValidation && theValidationContext.getResource() instanceof IBaseBundle
260                        && myExecutorService != null) {
261                        result = validateBundleEntriesConcurrently(theValidationContext, theOptions);
262                } else {
263                        result = validateResource(theValidationContext);
264                }
265
266                return theValidationCompletionCallback.apply(result);
267        }
268
269        private ValidationResult validateBundleEntriesConcurrently(IValidationContext<IBaseResource> theValidationContext, ValidationOptions theOptions) {
270                List<IBaseResource> entries = BundleUtil.toListOfResources(myContext, (IBaseBundle) theValidationContext.getResource());
271                // Async validation tasks
272                List<ConcurrentValidationTask> validationTasks = IntStream.range(0, entries.size())
273                        .mapToObj(index -> {
274                                IBaseResource resourceToValidate;
275                                IBaseResource entry = entries.get(index);
276
277                                if (mySkipContainedReferenceValidation) {
278                                        resourceToValidate = withoutContainedResources(entry);
279                                } else {
280                                        resourceToValidate = entry;
281                                }
282
283                                String entryPathPrefix = String.format("Bundle.entry[%d].resource.ofType(%s)", index, resourceToValidate.fhirType());
284                                Future<ValidationResult> future = myExecutorService.submit(() -> {
285                                        IValidationContext<IBaseResource> entryValidationContext = ValidationContext.forResource(theValidationContext.getFhirContext(), resourceToValidate, theOptions);
286                                        return validateResource(entryValidationContext);
287                                });
288                                return new ConcurrentValidationTask(entryPathPrefix, future);
289                        }).collect(Collectors.toList());
290
291                List<SingleValidationMessage> validationMessages = buildValidationMessages(validationTasks);
292                return new ValidationResult(myContext, validationMessages);
293        }
294
295        IBaseResource withoutContainedResources(IBaseResource theEntry) {
296                if (TerserUtil.hasValues(myContext, theEntry, "contained")) {
297                        IBaseResource deepCopy = TerserUtil.clone(myContext, theEntry);
298                        TerserUtil.clearField(myContext, deepCopy, "contained");
299                        return deepCopy;
300                } else {
301                        return theEntry;
302                }
303        }
304
305        static List<SingleValidationMessage> buildValidationMessages(List<ConcurrentValidationTask> validationTasks) {
306                List<SingleValidationMessage> retval = new ArrayList<>();
307                try {
308                        for (ConcurrentValidationTask validationTask : validationTasks) {
309                                ValidationResult result = validationTask.getFuture().get();
310                                final String bundleEntryPathPrefix = validationTask.getResourcePathPrefix();
311                                List<SingleValidationMessage> messages = result.getMessages().stream()
312                                        .map(message -> {
313                                                String currentPath;
314
315                                                String locationString = StringUtils.defaultIfEmpty(message.getLocationString(), "");
316
317                                                int dotIndex = locationString.indexOf('.');
318                                                if (dotIndex >= 0) {
319                                                        currentPath = locationString.substring(dotIndex);
320                                                } else {
321                                                        if (isBlank(bundleEntryPathPrefix) || isBlank(locationString)) {
322                                                                currentPath = locationString;
323                                                        } else {
324                                                                currentPath = "." + locationString;
325                                                        }
326                                                }
327
328                                                message.setLocationString(bundleEntryPathPrefix + currentPath);
329                                                return message;
330                                        })
331                                        .collect(Collectors.toList());
332                                retval.addAll(messages);
333                        }
334                } catch (InterruptedException | ExecutionException exp) {
335                        throw new InternalErrorException(Msg.code(2246) + exp);
336                }
337                return retval;
338        }
339
340        private ValidationResult validateResource(IValidationContext<IBaseResource> theValidationContext) {
341                for (IValidatorModule next : myValidators) {
342                        next.validateResource(theValidationContext);
343                }
344                return theValidationContext.toResult();
345        }
346
347        private ValidationResult invokeValidationCompletedHooks(IBaseResource theResourceParsed, String theResourceRaw, ValidationResult theValidationResult) {
348                if (myInterceptorBroadcaster != null) {
349                        if (myInterceptorBroadcaster.hasHooks(Pointcut.VALIDATION_COMPLETED)) {
350                                HookParams params = new HookParams()
351                                        .add(IBaseResource.class, theResourceParsed)
352                                        .add(String.class, theResourceRaw)
353                                        .add(ValidationResult.class, theValidationResult);
354                                Object newResult = myInterceptorBroadcaster.callHooksAndReturnObject(Pointcut.VALIDATION_COMPLETED, params);
355                                if (newResult != null) {
356                                        theValidationResult = (ValidationResult) newResult;
357                                }
358                        }
359                }
360                return theValidationResult;
361        }
362
363        /**
364         * Optionally supplies an interceptor broadcaster that will be used to invoke validation related Pointcut events
365         *
366         * @since 5.5.0
367         */
368        public void setInterceptorBroadcaster(IInterceptorBroadcaster theInterceptorBraodcaster) {
369                myInterceptorBroadcaster = theInterceptorBraodcaster;
370        }
371
372        public FhirValidator setExecutorService(ExecutorService theExecutorService) {
373                myExecutorService = theExecutorService;
374                return this;
375        }
376
377        /**
378         * If this is true, bundles will be validated in parallel threads.  The bundle structure itself will not be validated,
379         * only the resources in its entries.
380         */
381
382        public boolean isConcurrentBundleValidation() {
383                return myConcurrentBundleValidation;
384        }
385
386        /**
387         * If this is true, bundles will be validated in parallel threads.  The bundle structure itself will not be validated,
388         * only the resources in its entries.
389         */
390        public FhirValidator setConcurrentBundleValidation(boolean theConcurrentBundleValidation) {
391                myConcurrentBundleValidation = theConcurrentBundleValidation;
392                return this;
393        }
394
395        /**
396         * If this is true, any resource that has contained resources will first be deep-copied and then the contained
397         * resources remove from the copy and this copy without contained resources will be validated.
398         */
399        public boolean isSkipContainedReferenceValidation() {
400                return mySkipContainedReferenceValidation;
401        }
402
403        /**
404         * If this is true, any resource that has contained resources will first be deep-copied and then the contained
405         * resources remove from the copy and this copy without contained resources will be validated.
406         */
407        public FhirValidator setSkipContainedReferenceValidation(boolean theSkipContainedReferenceValidation) {
408                mySkipContainedReferenceValidation = theSkipContainedReferenceValidation;
409                return this;
410        }
411
412        // Simple Tuple to keep track of bundle path and associate aync future task
413        static class ConcurrentValidationTask {
414                private final String myResourcePathPrefix;
415                private final Future<ValidationResult> myFuture;
416
417                ConcurrentValidationTask(String theResourcePathPrefix, Future<ValidationResult> theFuture) {
418                        myResourcePathPrefix = theResourcePathPrefix;
419                        myFuture = theFuture;
420                }
421
422                public String getResourcePathPrefix() {
423                        return myResourcePathPrefix;
424                }
425
426                public Future<ValidationResult> getFuture() {
427                        return myFuture;
428                }
429        }
430
431}