ValidationResult.java

/*******************************************************************************
 * Copyright (c) 2019 Eclipse RDF4J contributors.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/

package org.eclipse.rdf4j.sail.shacl.results;

import static org.eclipse.rdf4j.model.util.Values.bnode;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.RDF4J;
import org.eclipse.rdf4j.model.vocabulary.RSX;
import org.eclipse.rdf4j.model.vocabulary.SHACL;
import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent;
import org.eclipse.rdf4j.sail.shacl.ast.PropertyShape;
import org.eclipse.rdf4j.sail.shacl.ast.Severity;
import org.eclipse.rdf4j.sail.shacl.ast.Shape;
import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.ConstraintComponent;
import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.SparqlConstraintComponent;
import org.eclipse.rdf4j.sail.shacl.ast.paths.Path;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The ValidationResult represents the results from a SHACL validation in an easy-to-use Java API.
 *
 * @deprecated The ValidationResult is deprecated because it is planned moved to a new package to allow it to be used
 *             with remote validation results.
 */
@Deprecated
public class ValidationResult {

	private static final Logger logger = LoggerFactory.getLogger(ValidationResult.class);

	private Resource id;
	private final Optional<Value> value;
	private final Shape shape;

	private final SourceConstraintComponent sourceConstraintComponent;
	private final ConstraintComponent sourceConstraint;
	private final Severity severity;
	private final Value focusNode;
	private final Resource[] dataGraphs;
	private final Resource[] shapesGraphs;
	private Path path;
	private Path rsxPairwisePath;
	private ValidationResult detail;
	private Value pathIri;

	public ValidationResult(Value focusNode, Value value, Shape shape,
			ConstraintComponent sourceConstraint, Severity severity, ConstraintComponent.Scope scope,
			Resource[] dataGraphs, Resource[] shapesGraphs) {
		this.focusNode = focusNode;
		assert this.focusNode != null;
		this.sourceConstraintComponent = sourceConstraint.getConstraintComponent();
		this.sourceConstraint = sourceConstraint;
		this.shape = shape;

		if (sourceConstraintComponent.producesValidationResultValue()) {
			assert !sourceConstraintComponent.alwaysProducesValidationResultValue() || value != null;

			// value could be null if assertions are disabled
			// noinspection ConstantValue
			if (value == null && sourceConstraintComponent.alwaysProducesValidationResultValue()) {
				logger.error(
						"Source constraint component {} was expected to produce a value, but value is null! Shape: {}",
						sourceConstraintComponent, shape);
			}

			this.value = Optional.ofNullable(value);
		} else {
			assert scope != ConstraintComponent.Scope.propertyShape || value == null;
			this.value = Optional.empty();
		}

		if (shape instanceof PropertyShape) {
			this.path = ((PropertyShape) shape).getPath();
		}
		this.severity = severity;
		this.dataGraphs = dataGraphs;
		this.shapesGraphs = shapesGraphs;
	}

	public ValidationResult(Value focusNode, Value value, Shape shape,
			ConstraintComponent sourceConstraint, Severity severity, ConstraintComponent.Scope scope,
			Resource[] dataGraphs, Resource[] shapesGraphs, Path rsxPairwisePath) {
		this(focusNode, value, shape, sourceConstraint, severity, scope, dataGraphs, shapesGraphs);
		this.rsxPairwisePath = rsxPairwisePath;
	}

	/**
	 * @return ValidationResult with more information as to what failed. Usually for nested Shapes in eg. sh:or.
	 */
	public ValidationResult getDetail() {
		return detail;
	}

	public void setDetail(ValidationResult detail) {
		this.detail = detail;
	}

	/**
	 * @return all ValidationResult(s) with more information as to what failed. Usually for nested Shapes in eg. sh:or.
	 */
	public List<ValidationResult> getDetails() {

		ArrayList<ValidationResult> validationResults = new ArrayList<>();

		ValidationResult temp = detail;
		while (temp != null) {
			validationResults.add(temp);
			temp = temp.detail;
		}

		return validationResults;

	}

	public Model asModel(Model model) {
		return asModel(model, new HashSet<>());
	}

	public Model asModel(Model model, Set<Resource> rdfListDedupe) {

		model.add(getId(), RDF.TYPE, SHACL.VALIDATION_RESULT);

		model.add(getId(), SHACL.FOCUS_NODE, focusNode);

		for (Resource graph : contextsToSet(dataGraphs)) {
			model.add(getId(), RSX.dataGraph, graph);
		}

		for (Resource graph : contextsToSet(shapesGraphs)) {
			model.add(getId(), RSX.shapesGraph, graph);
		}

		value.ifPresent(v -> model.add(getId(), SHACL.VALUE, v));

		if (pathIri != null) {
			model.add(getId(), SHACL.RESULT_PATH, pathIri);
		} else if (this.path != null) {
			path.toModel(path.getId(), null, model, new HashSet<>());
			model.add(getId(), SHACL.RESULT_PATH, path.getId());
		}

		if (rsxPairwisePath != null) {
			rsxPairwisePath.toModel(rsxPairwisePath.getId(), null, model, new HashSet<>());
			model.add(getId(), RSX.actualPairwisePath, rsxPairwisePath.getId());
		}

		if (sourceConstraint instanceof SparqlConstraintComponent) {
			model.add(getId(), SHACL.SOURCE_CONSTRAINT, ((SparqlConstraintComponent) sourceConstraint).getId());
		}

		model.add(getId(), SHACL.SOURCE_CONSTRAINT_COMPONENT, getSourceConstraintComponent().getIri());
		model.add(getId(), SHACL.RESULT_SEVERITY, severity.getIri());

		for (Literal message : shape.getMessage()) {
			model.add(getId(), SHACL.RESULT_MESSAGE, message);
		}

		shape.toModel(getId(), SHACL.SOURCE_SHAPE, model, new HashSet<>());

		return model;
	}

	private static Set<Resource> contextsToSet(Resource[] context) {
		if (context == null || context.length == 0) {
			return Collections.emptySet();
		}

		return Arrays.stream(context)
				.map(c -> c == null ? RDF4J.NIL : c)
				.collect(Collectors.toSet());
	}

	/**
	 * @return the path, as specified in the Shape, that caused the violation
	 */
	private Path getPath() {
		return path;
	}

	/**
	 * @return the focus node, aka. the subject, that caused the violation
	 */
	private Value getFocusNode() {
		return focusNode;
	}

	public final Resource getId() {
		if (id == null) {
			id = bnode();
		}
		return id;
	}

	/**
	 * @return the type of the source constraint that caused the violation
	 */
	public SourceConstraintComponent getSourceConstraintComponent() {
		return sourceConstraintComponent;
	}

	@Override
	public String toString() {
		return "ValidationResult{" +
				"focusNode=" + focusNode +
				", value=" + value.orElse(null) +
				", shape=" + shape.getId() +
				", path=" + path +
				", sourceConstraintComponent=" + sourceConstraintComponent +
				", severity=" + severity +
				", detail=" + detail +
				'}';
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}
		ValidationResult that = (ValidationResult) o;
		return value.equals(that.value) && shape.equals(that.shape)
				&& sourceConstraintComponent == that.sourceConstraintComponent && severity == that.severity
				&& focusNode.equals(that.focusNode) && Objects.equals(path, that.path)
				&& Objects.equals(detail, that.detail);
	}

	@Override
	public int hashCode() {
		return Objects.hash(value, shape, sourceConstraintComponent, severity, focusNode, path, detail);
	}

	public void setPathIri(Value path) {
		this.pathIri = path;
	}

	protected Optional<Value> getValue() {
		return value;
	}

	protected Shape getShape() {
		return shape;
	}

	protected ConstraintComponent getSourceConstraint() {
		return sourceConstraint;
	}

	protected Severity getSeverity() {
		return severity;
	}

	protected Resource[] getDataGraphs() {
		return dataGraphs;
	}

	protected Resource[] getShapesGraphs() {
		return shapesGraphs;
	}
}