All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.fhir.validation.FHIRValidator Maven / Gradle / Ivy

/*
 * (C) Copyright IBM Corp. 2019, 2021
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package com.ibm.fhir.validation;

import static com.ibm.fhir.model.type.String.string;
import static com.ibm.fhir.path.util.FHIRPathUtil.evaluatesToBoolean;
import static com.ibm.fhir.path.util.FHIRPathUtil.getResourceNode;
import static com.ibm.fhir.path.util.FHIRPathUtil.getRootResourceNode;
import static com.ibm.fhir.path.util.FHIRPathUtil.isFalse;
import static com.ibm.fhir.path.util.FHIRPathUtil.singleton;
import static com.ibm.fhir.profile.ProfileSupport.createConstraint;
import static com.ibm.fhir.validation.util.FHIRValidationUtil.ISSUE_COMPARATOR;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.ibm.fhir.model.annotation.Constraint;
import com.ibm.fhir.model.resource.OperationOutcome.Issue;
import com.ibm.fhir.model.resource.Resource;
import com.ibm.fhir.model.resource.StructureDefinition;
import com.ibm.fhir.model.type.CodeableConcept;
import com.ibm.fhir.model.type.Extension;
import com.ibm.fhir.model.type.code.IssueSeverity;
import com.ibm.fhir.model.type.code.IssueType;
import com.ibm.fhir.model.util.ModelSupport;
import com.ibm.fhir.path.FHIRPathBooleanValue;
import com.ibm.fhir.path.FHIRPathElementNode;
import com.ibm.fhir.path.FHIRPathNode;
import com.ibm.fhir.path.FHIRPathResourceNode;
import com.ibm.fhir.path.FHIRPathTree;
import com.ibm.fhir.path.evaluator.FHIRPathEvaluator;
import com.ibm.fhir.path.evaluator.FHIRPathEvaluator.EvaluationContext;
import com.ibm.fhir.path.function.ResolveFunction;
import com.ibm.fhir.path.visitor.FHIRPathDefaultNodeVisitor;
import com.ibm.fhir.profile.ProfileSupport;
import com.ibm.fhir.registry.FHIRRegistry;
import com.ibm.fhir.validation.exception.FHIRValidationException;

public class FHIRValidator {
    private static final Logger log = Logger.getLogger(FHIRValidator.class.getName());

    private final ValidatingNodeVisitor visitor = new ValidatingNodeVisitor();

    private FHIRValidator() { }

    /**
     * Validate a {@link Resource} against constraints in the base specification and
     * resource-asserted profile references or specific profile references but not both.
     *
     * 

Resource-asserted profile references that are not available in the FHIRRegistry result in issues with severity WARNING. * *

Unknown profile references passed as arguments to this method result in issues with severity ERROR. * *

Profiles that are incompatible with the resource type being validated result in issues with severity ERROR. * *

Profile references that are passed into this method are only applicable to the outermost * resource (not contained resources). * * @param resource * a {@link Resource} instance (the target of validation) * @param profiles * specific profile references to validate the resource against * @return * a non-null, possibly empty list of issues generated during validation (sorted by severity) * @throws FHIRValidationException * for errors that occur during validation */ public List validate(Resource resource, String... profiles) throws FHIRValidationException { return validate(resource, (profiles.length == 0), profiles); } /** * Validate a {@link Resource} against constraints in the base specification and * resource-asserted profile references and/or specific profile references. * *

Resource-asserted profile references that are not available in the FHIRRegistry result in issues with severity WARNING. * *

Unknown profile references passed as arguments to this method result in issues with severity ERROR. * *

Profiles that are incompatible with the resource type being validated result in issues with severity ERROR. * *

Profile references that are passed into this method are only applicable to the outermost * resource (not contained resources). * * @param resource * a {@link Resource} instance (the target of validation) * @param includeResourceAssertedProfiles * whether or not to consider resource-asserted profiles during validation * @param profiles * specific profile references to validate the resource against * @return * a non-null, possibly empty list of issues generated during validation (sorted by severity) * @throws FHIRValidationException * for errors that occur during validation */ public List validate(Resource resource, boolean includeResourceAssertedProfiles, String... profiles) throws FHIRValidationException { return validate(new EvaluationContext(resource), includeResourceAssertedProfiles, profiles); } /** * Validate a resource, using an {@link EvaluationContext}, against constraints in the base specification and * resource-asserted profile references or specific profile references but not both. * *

Resource-asserted profile references that are not available in the FHIRRegistry result in issues with severity WARNING. * *

Unknown profile references passed as arguments to this method result in issues with severity ERROR. * *

Profiles that are incompatible with the resource type being validated result in issues with severity ERROR. * *

Profile references that are passed into this method are only applicable to the outermost * resource (not contained resources). * * @param evaluationContext * the {@link EvaluationContext} for this validation which includes a {@link FHIRPathTree} * built from a {@link Resource} instance (the target of validation) * @param profiles * specific profile references to validate the evaluation context against * @return * a non-null, possibly empty list of issues generated during validation (sorted by severity) * @throws FHIRValidationException * for errors that occur during validation * @see {@link FHIRPathEvaluator.EvaluationContext} */ public List validate(EvaluationContext evaluationContext, String... profiles) throws FHIRValidationException { return validate(evaluationContext, (profiles.length == 0), profiles); } /** * Validate a resource, using an {@link EvaluationContext}, against constraints in the base specification and * resource-asserted profile references and/or specific profile references. * *

Resource-asserted profile references that are not available in the FHIRRegistry result in issues with severity WARNING. * *

Unknown profile references passed as arguments to this method result in issues with severity ERROR. * *

Profiles that are incompatible with the resource type being validated result in issues with severity ERROR. * *

Profile references that are passed into this method are only applicable to the outermost * resource (not contained resources). * * @param evaluationContext * the {@link EvaluationContext} for this validation which includes a {@link FHIRPathTree} * built from a {@link Resource} instance (the target of validation) * @param includeResourceAssertedProfiles * whether or not to consider resource-asserted profiles during validation * @param profiles * specific profile references to validate the evaluation context against * @return * a non-null, possibly empty list of issues generated during validation (sorted by severity) * @throws FHIRValidationException * for errors that occur during validation * @see {@link FHIRPathEvaluator.EvaluationContext} */ public List validate(EvaluationContext evaluationContext, boolean includeResourceAssertedProfiles, String... profiles) throws FHIRValidationException { Objects.requireNonNull(evaluationContext); Objects.requireNonNull(evaluationContext.getTree()); if (!evaluationContext.getTree().getRoot().isResourceNode()) { throw new IllegalArgumentException("Root must be resource node"); } try { evaluationContext.setExternalConstant(ResolveFunction.RESOLVE_RELATIVE_REFERENCES, FHIRPathBooleanValue.TRUE); List issues = new ArrayList<>(); validateProfileReferences(evaluationContext.getTree().getRoot().asResourceNode(), Arrays.asList(profiles), false, issues); issues.addAll(visitor.validate(evaluationContext, includeResourceAssertedProfiles, profiles)); Collections.sort(issues, ISSUE_COMPARATOR); return Collections.unmodifiableList(issues); } catch (Exception e) { throw new FHIRValidationException("An error occurred during validation", e); } } public static FHIRValidator validator() { return new FHIRValidator(); } /** * Validate a list of profile references to ensure they are supported (known by the FHIR registry) and applicable * (the type constrained by the profile is compatible with the resource being validated). * *

Resource-asserted profile references that are not available in the FHIRRegistry result in issues with severity WARNING. * *

Unknown profile references passed as arguments to this method result in issues with severity ERROR. * *

Profiles that are incompatible with the resource type being validated result in issues with severity ERROR. * * @param resourceNode * the resource node being validated by a FHIRValidator instance * @param profiles * the list of profile references to validate * @param resourceAsserted * indicates whether the profile references came from the resource or were explicitly passed in as arguments * @param issues * the list of issues to add to */ private static void validateProfileReferences( FHIRPathResourceNode resourceNode, List profiles, boolean resourceAsserted, List issues) { Class resourceType = resourceNode.resource().getClass(); for (String url : profiles) { StructureDefinition profile = ProfileSupport.getProfile(url); if (profile == null) { issues.add(issue(resourceAsserted ? IssueSeverity.WARNING : IssueSeverity.ERROR, IssueType.NOT_SUPPORTED, "Profile '" + url + "' is not supported", resourceNode)); } else if (!ProfileSupport.isApplicable(profile, resourceType)) { issues.add(issue(IssueSeverity.ERROR, IssueType.INVALID, "Profile '" + url + "' is not applicable to resource type: " + resourceType.getSimpleName(), resourceNode)); } } } private static Issue issue(IssueSeverity severity, IssueType code, String description, FHIRPathNode node) { return Issue.builder() .severity(severity) .code(code) .details(CodeableConcept.builder() .text(string(description)) .build()) .expression(string(node.path())) .build(); } private static class ValidatingNodeVisitor extends FHIRPathDefaultNodeVisitor { private FHIRPathEvaluator evaluator = FHIRPathEvaluator.evaluator(); private EvaluationContext evaluationContext; private boolean includeResourceAssertedProfiles; private List profiles; private List issues = new ArrayList<>(); private ValidatingNodeVisitor() { } private List validate(EvaluationContext evaluationContext, boolean includeResourceAssertedProfiles, String... profiles) { reset(); this.evaluationContext = evaluationContext; this.includeResourceAssertedProfiles = includeResourceAssertedProfiles; this.profiles = Arrays.asList(profiles); this.evaluationContext.getTree().getRoot().accept(this); return issues; } private void reset() { issues.clear(); } @Override public void doVisit(FHIRPathElementNode node) { validate(node); } @Override public void doVisit(FHIRPathResourceNode node) { validate(node); } /** * @throws RuntimeException if the registered constraints cannot be evaluated for the passed element node */ private void validate(FHIRPathElementNode elementNode) { Class elementType = elementNode.element().getClass(); Collection constraints = ModelSupport.getConstraints(elementType); if (Extension.class.equals(elementType)) { String url = elementNode.element().as(Extension.class).getUrl(); if (isAbsolute(url)) { if (FHIRRegistry.getInstance().hasResource(url, StructureDefinition.class)) { constraints = new ArrayList<>(constraints); constraints.add(createConstraint("generated-ext-1", Constraint.LEVEL_RULE, Constraint.LOCATION_BASE, "Extension must conform to definition '" + url + "'", "conformsTo('" + url + "')", false, true)); } else { issues.add(issue(IssueSeverity.WARNING, IssueType.NOT_SUPPORTED, "Extension definition '" + url + "' is not supported", elementNode)); } } } validate(elementType, elementNode, constraints); } private boolean isAbsolute(String url) { try { return new URI(url).isAbsolute(); } catch (URISyntaxException e) { log.warning("Invalid URI: " + url); } return false; } /** * @throws RuntimeException if the registered constraints cannot be evaluated for the passed resource node */ private void validate(FHIRPathResourceNode resourceNode) { Class resourceType = resourceNode.resource().getClass(); validate(resourceType, resourceNode, ModelSupport.getConstraints(resourceType)); if (includeResourceAssertedProfiles) { List resourceAssertedProfiles = ProfileSupport.getResourceAssertedProfiles(resourceNode.resource()); validateProfileReferences(resourceNode, resourceAssertedProfiles, true, issues); validate(resourceType, resourceNode, ProfileSupport.getConstraints(resourceAssertedProfiles, resourceType)); } if (!profiles.isEmpty() && !resourceNode.path().contains(".")) { validate(resourceType, resourceNode, ProfileSupport.getConstraints(profiles, resourceType)); } } /** * @throws RuntimeException if one of the passed constraints cannot be evaluated for the passed node */ private void validate(Class type, FHIRPathNode node, Collection constraints) { for (Constraint constraint : constraints) { if (constraint.modelChecked()) { if (log.isLoggable(Level.FINER)) { log.finer(" Constraint: " + constraint.id() + " is model-checked"); } continue; } evaluationContext.setConstraint(constraint); validate(type, node, constraint); evaluationContext.unsetConstraint(); } } /** * @throws RuntimeException if the passed constraint cannot be evaluated for the passed node */ private void validate(Class type, FHIRPathNode node, Constraint constraint) { String path = node.path(); try { if (log.isLoggable(Level.FINER)) { log.finer(" Constraint: " + constraint); } Collection initialContext = singleton(node); if (!Constraint.LOCATION_BASE.equals(constraint.location())) { initialContext = evaluator.evaluate(evaluationContext, constraint.location(), initialContext); issues.addAll(evaluationContext.getIssues()); evaluationContext.clearIssues(); } IssueSeverity severity = Constraint.LEVEL_WARNING.equals(constraint.level()) ? IssueSeverity.WARNING : IssueSeverity.ERROR; for (FHIRPathNode contextNode : initialContext) { evaluationContext.setExternalConstant("rootResource", getRootResourceNode(evaluationContext.getTree(), contextNode)); evaluationContext.setExternalConstant("resource", getResourceNode(evaluationContext.getTree(), contextNode)); Collection result = evaluator.evaluate(evaluationContext, constraint.expression(), singleton(contextNode)); issues.addAll(evaluationContext.getIssues()); evaluationContext.clearIssues(); if (evaluatesToBoolean(result) && isFalse(result)) { issues.add(issue(severity, IssueType.INVARIANT, constraint.id() + ": " + constraint.description(), contextNode)); } if (log.isLoggable(Level.FINER)) { log.finer(" Evaluation result: " + result + ", Path: " + contextNode.path()); } } } catch (Exception e) { throw new RuntimeException("An error occurred while validating constraint: " + constraint.id() + " with location: " + constraint.location() + " and expression: " + constraint.expression() + " at path: " + path, e); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy