SparqlConstraintComponent.java

/*******************************************************************************
 * Copyright (c) 2020 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.ast.constraintcomponents;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;

import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Namespace;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.model.util.Values;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.SHACL;
import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent;
import org.eclipse.rdf4j.sail.shacl.ValidationSettings;
import org.eclipse.rdf4j.sail.shacl.ast.CanProduceValidationReport;
import org.eclipse.rdf4j.sail.shacl.ast.ShaclPrefixParser;
import org.eclipse.rdf4j.sail.shacl.ast.Shape;
import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher;
import org.eclipse.rdf4j.sail.shacl.ast.ValidationApproach;
import org.eclipse.rdf4j.sail.shacl.ast.ValidationQuery;
import org.eclipse.rdf4j.sail.shacl.ast.paths.Path;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.AllTargetsPlanNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.PlanNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.PlanNodeProvider;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.SparqlConstraintSelect;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.Unique;
import org.eclipse.rdf4j.sail.shacl.ast.targets.EffectiveTarget;
import org.eclipse.rdf4j.sail.shacl.wrapper.data.ConnectionsGroup;
import org.eclipse.rdf4j.sail.shacl.wrapper.shape.ShapeSource;

public class SparqlConstraintComponent extends AbstractConstraintComponent implements CanProduceValidationReport {

	private final Shape shape;
	public boolean produceValidationReports;
	private String select;
	private String originalSelect;
	private List<Literal> message = new ArrayList<>();
	private Boolean deactivated;
	private final Set<Namespace> namespaces;
	private final Model prefixes;

	public SparqlConstraintComponent(Resource id, ShapeSource shapeSource, Shape shape) {
		super(id);
		this.shape = shape;
		try (Stream<Value> objects = shapeSource.getObjects(id, ShapeSource.Predicates.SELECT)) {
			objects.forEach(literal -> {
				if (select != null) {
					throw new IllegalStateException("Multiple sh:select queries found for constraint component " + id);
				}
				if (!(literal.isLiteral())) {
					throw new IllegalStateException("sh:select must be a literal for constraint component " + id);
				}
				select = literal.stringValue();
				originalSelect = select;
			});
		}

		if (select == null) {
			throw new IllegalStateException("No sh:select query found for constraint component " + id);
		}

		try (Stream<Value> objects = shapeSource.getObjects(id, ShapeSource.Predicates.MESSAGE)) {
			objects.forEach(literal -> {
				if (!(literal.isLiteral())) {
					throw new IllegalStateException("sh:message must be a literal for constraint component " + id);
				}
				message.add((Literal) literal);
			});
		}

		try (Stream<Value> objects = shapeSource.getObjects(id, ShapeSource.Predicates.DEACTIVATED)) {
			objects.forEach(literal -> {
				if (deactivated != null) {
					throw new IllegalStateException("Multiple sh:deactivated found for constraint component " + id);
				}
				if (!(literal.isLiteral())) {
					throw new IllegalStateException("sh:deactivated must be a literal for constraint component " + id);
				}
				deactivated = ((Literal) literal).booleanValue();
			});
		}

		var shaclNamespaces = ShaclPrefixParser.extractNamespaces(id, shapeSource);
		prefixes = shaclNamespaces.getModel();
		namespaces = shaclNamespaces.getNamespaces();

		select = ShaclPrefixParser.toSparqlPrefixes(namespaces) + "\n" + select;
	}

	public SparqlConstraintComponent(Resource id, Shape shape, boolean produceValidationReports, String select,
			String originalSelect, List<Literal> message, Boolean deactivated, Set<Namespace> namespaces,
			Model prefixes) {
		super(id);
		this.shape = shape;
		this.produceValidationReports = produceValidationReports;
		this.select = select;
		this.originalSelect = originalSelect;
		this.message = message;
		this.deactivated = deactivated;
		this.prefixes = prefixes;
		this.namespaces = namespaces;
	}

	@Override
	public void toModel(Resource subject, IRI predicate, Model model, Set<Resource> cycleDetection) {
		model.add(subject, SHACL.SPARQL, getId());
		model.add(getId(), SHACL.SELECT, Values.literal(originalSelect));
		model.add(getId(), RDF.TYPE, SHACL.SPARQL_CONSTRAINT);

		for (Literal literal : message) {
			model.add(getId(), SHACL.MESSAGE, literal);
		}

		if (deactivated != null) {
			model.add(getId(), SHACL.DEACTIVATED, Values.literal(deactivated));
		}

		model.addAll(prefixes);

	}

	@Override
	public SourceConstraintComponent getConstraintComponent() {
		return SourceConstraintComponent.SPARQLConstraintComponent;
	}

	@Override
	public PlanNode generateTransactionalValidationPlan(ConnectionsGroup connectionsGroup,
			ValidationSettings validationSettings,
			PlanNodeProvider overrideTargetNode, Scope scope) {

		StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider = new StatementMatcher.StableRandomVariableProvider();

		EffectiveTarget effectiveTarget = getTargetChain().getEffectiveTarget(scope,
				connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider);

		String select = this.select;
		if (scope == Scope.propertyShape) {
			Path path = getTargetChain().getPath().get();
			String s = path.toSparqlPathString();
			select = select.replace(" $PATH ", " " + s + " ");
		}

		PlanNode allTargets;
		if (overrideTargetNode != null) {
			allTargets = getPlanNodeForOverrideTargetNode(
					connectionsGroup,
					validationSettings,
					overrideTargetNode,
					scope,
					effectiveTarget
			);

		} else {
			allTargets = effectiveTarget.getAllTargets(connectionsGroup, validationSettings.getDataGraph(), scope);
		}

		if (effectiveTarget.size() > 1) {
			allTargets = Unique.getInstance(allTargets, true, connectionsGroup);
		}

		return new SparqlConstraintSelect(connectionsGroup.getBaseConnection(), allTargets, select, scope,
				validationSettings.getDataGraph(), produceValidationReports, this, shape);

	}

	private PlanNode getPlanNodeForOverrideTargetNode(ConnectionsGroup connectionsGroup,
			ValidationSettings validationSettings, PlanNodeProvider overrideTargetNode, Scope scope,
			EffectiveTarget effectiveTarget) {
		PlanNode planNode;
		assert scope != null;

		PlanNode overrideTargetPlanNode = overrideTargetNode.getPlanNode();

		if (scope == Scope.nodeShape) {

			if (overrideTargetPlanNode instanceof AllTargetsPlanNode) {
				return effectiveTarget.getAllTargets(connectionsGroup, validationSettings.getDataGraph(), scope);
			} else {
				return effectiveTarget.extend(
						overrideTargetPlanNode,
						connectionsGroup,
						validationSettings.getDataGraph(),
						scope,
						EffectiveTarget.Extend.right,
						false,
						null
				);

			}

		} else {

			if (overrideTargetPlanNode instanceof AllTargetsPlanNode) {
				return effectiveTarget.getAllTargets(connectionsGroup, validationSettings.getDataGraph(), scope);
			} else {

				overrideTargetPlanNode = effectiveTarget.extend(overrideTargetPlanNode, connectionsGroup,
						validationSettings.getDataGraph(), scope,
						EffectiveTarget.Extend.right, false, null);

				planNode = connectionsGroup.getCachedNodeFor(overrideTargetPlanNode);
			}
		}

		return planNode;
	}

	@Override
	public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] dataGraph, Scope scope,
			StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider,
			ValidationSettings validationSettings) {
		return getTargetChain()
				.getEffectiveTarget(scope, connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider)
				.getAllTargets(connectionsGroup, dataGraph, scope);
	}

	@Override
	public ConstraintComponent deepClone() {
		return new SparqlConstraintComponent(getId(), shape, produceValidationReports, select, originalSelect, message,
				deactivated, namespaces, prefixes);
	}

	@Override
	public ValidationQuery generateSparqlValidationQuery(ConnectionsGroup connectionsGroup,
			ValidationSettings validationSettings, boolean negatePlan, boolean negateChildren, Scope scope) {
		return null;
	}

	@Override
	public ValidationApproach getOptimalBulkValidationApproach() {
		return ValidationApproach.Transactional;
	}

	@Override
	public List<Literal> getDefaultMessage() {
		return message;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}

		SparqlConstraintComponent that = (SparqlConstraintComponent) o;

		if (produceValidationReports != that.produceValidationReports) {
			return false;
		}
		if (!select.equals(that.select)) {
			return false;
		}
		if (!originalSelect.equals(that.originalSelect)) {
			return false;
		}
		if (!Objects.equals(message, that.message)) {
			return false;
		}
		return Objects.equals(deactivated, that.deactivated);
	}

	@Override
	public int hashCode() {
		int result = (produceValidationReports ? 1 : 0);
		result = 31 * result + select.hashCode();
		result = 31 * result + originalSelect.hashCode();
		result = 31 * result + (message != null ? message.hashCode() : 0);
		result = 31 * result + (deactivated != null ? deactivated.hashCode() : 0);
		return result + "SparqlConstraintComponent".hashCode();
	}

	@Override
	public void setProducesValidationReport(boolean producesValidationReport) {
		this.produceValidationReports = producesValidationReport;
	}

	@Override
	public boolean producesValidationReport() {
		return produceValidationReports;
	}

}