AbstractSimpleConstraintComponent.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.Optional;
import java.util.Set;
import java.util.function.Function;

import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.Literal;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent;
import org.eclipse.rdf4j.sail.shacl.ValidationSettings;
import org.eclipse.rdf4j.sail.shacl.ast.ShaclUnsupportedException;
import org.eclipse.rdf4j.sail.shacl.ast.SparqlFragment;
import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher;
import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable;
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.AbstractBulkJoinPlanNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.AllTargetsPlanNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.BufferedPlanNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.BulkedExternalInnerJoin;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.EmptyNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.FilterPlanNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.InnerJoin;
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.ShiftToPropertyShape;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.UnBufferedPlanNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.UnionNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.Unique;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.ValidationTuple;
import org.eclipse.rdf4j.sail.shacl.ast.targets.EffectiveTarget;
import org.eclipse.rdf4j.sail.shacl.ast.targets.TargetChain;
import org.eclipse.rdf4j.sail.shacl.wrapper.data.ConnectionsGroup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class AbstractSimpleConstraintComponent extends AbstractConstraintComponent {

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

	private Resource id;
	TargetChain targetChain;

	public AbstractSimpleConstraintComponent(Resource id) {
		this.id = id;
	}

	public AbstractSimpleConstraintComponent() {
	}

	public Resource getId() {
		return id;
	}

	@Override
	public TargetChain getTargetChain() {
		return targetChain;
	}

	@Override
	public void setTargetChain(TargetChain targetChain) {
		this.targetChain = targetChain;
	}

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

		boolean negatePlan = false;
		StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider = new StatementMatcher.StableRandomVariableProvider();

		EffectiveTarget effectiveTarget = targetChain.getEffectiveTarget(scope,
				connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider);
		Optional<Path> path = targetChain.getPath();

		if (overrideTargetNode != null) {
			return getPlanNodeForOverrideTargetNode(connectionsGroup, validationSettings, overrideTargetNode, scope,
					negatePlan, stableRandomVariableProvider, effectiveTarget, path);
		} else if (scope == Scope.nodeShape) {
			return effectiveTarget.getPlanNode(connectionsGroup, validationSettings.getDataGraph(), scope, false,
					p -> getFilterAttacherWithNegation(negatePlan, p, connectionsGroup));
		} else {

			PlanNode invalidValuesDirectOnPath = path.get()
					.getAnyAdded(connectionsGroup, validationSettings.getDataGraph(),
							planNode -> getFilterAttacherWithNegation(negatePlan, planNode, connectionsGroup));

			PlanNode addedTargets = effectiveTarget.getPlanNode(connectionsGroup, validationSettings.getDataGraph(),
					scope, false, null);

			InnerJoin innerJoin = new InnerJoin(addedTargets, invalidValuesDirectOnPath, connectionsGroup);

			if (connectionsGroup.getStats().wasEmptyBeforeTransaction()) {
				return innerJoin.getJoined(UnBufferedPlanNode.class);
			} else {
				PlanNode top = innerJoin.getJoined(BufferedPlanNode.class);

				// tuples from invalidValuesDirectOnPath that didn't match a target from addedTargets
				PlanNode discardedRight = innerJoin.getDiscardedRight(BufferedPlanNode.class);

				PlanNode typeFilterPlan = effectiveTarget.getTargetFilter(connectionsGroup,
						validationSettings.getDataGraph(), discardedRight);

				typeFilterPlan = effectiveTarget.extend(typeFilterPlan, connectionsGroup,
						validationSettings.getDataGraph(),
						scope,
						EffectiveTarget.Extend.left, true, null);

				top = UnionNode.getInstance(connectionsGroup, top, typeFilterPlan);

				PlanNode bulkedExternalInnerJoin = new BulkedExternalInnerJoin(
						effectiveTarget.getPlanNode(connectionsGroup, validationSettings.getDataGraph(), scope, false,
								null),
						connectionsGroup.getBaseConnection(),
						validationSettings.getDataGraph(),
						path.get()
								.getTargetQueryFragment(new Variable("a"),
										new Variable("c"),
										connectionsGroup.getRdfsSubClassOfReasoner(),
										stableRandomVariableProvider, Set.of()),
						connectionsGroup.hasPreviousStateConnection(),
						connectionsGroup.getPreviousStateConnection(),
						b -> new ValidationTuple(b.getValue("a"), b.getValue("c"), scope, true,
								validationSettings.getDataGraph()),
						connectionsGroup, AbstractBulkJoinPlanNode.DEFAULT_VARS);

				top = UnionNode.getInstance(connectionsGroup, top, bulkedExternalInnerJoin);

				return getFilterAttacherWithNegation(negatePlan, top, connectionsGroup);

			}
		}
	}

	private PlanNode getPlanNodeForOverrideTargetNode(ConnectionsGroup connectionsGroup,
			ValidationSettings validationSettings, PlanNodeProvider overrideTargetNode, Scope scope, boolean negatePlan,
			StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider, EffectiveTarget effectiveTarget,
			Optional<Path> path) {
		PlanNode planNode;

		if (scope == Scope.nodeShape) {
			PlanNode overrideTargetPlanNode = overrideTargetNode.getPlanNode();

			if (overrideTargetPlanNode instanceof AllTargetsPlanNode) {
				PlanNode allTargets = effectiveTarget.getAllTargets(connectionsGroup,
						validationSettings.getDataGraph(), scope);
				allTargets = getFilterAttacherWithNegation(negatePlan, allTargets, connectionsGroup);

				if (effectiveTarget.size() > 1) {
					allTargets = Unique.getInstance(allTargets, true, connectionsGroup);
				}
				return allTargets;
			} else {
				PlanNode extend = effectiveTarget.extend(overrideTargetPlanNode, connectionsGroup,
						validationSettings.getDataGraph(), scope,
						EffectiveTarget.Extend.right,
						false,
						p -> getFilterAttacherWithNegation(negatePlan, p, connectionsGroup)
				);

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

				return extend;

			}

		} else {
			PlanNode overrideTargetPlanNode = overrideTargetNode.getPlanNode();

			if (overrideTargetPlanNode instanceof AllTargetsPlanNode) {
				// We are cheating a bit here by retrieving all the targets and values at the same time by
				// pretending to be in node shape scope and then shifting the results back to property shape scope
				PlanNode allTargets = targetChain
						.getEffectiveTarget(Scope.nodeShape,
								connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider)
						.getAllTargets(connectionsGroup, validationSettings.getDataGraph(), Scope.nodeShape);
				allTargets = new ShiftToPropertyShape(allTargets, connectionsGroup);

				allTargets = getFilterAttacherWithNegation(negatePlan, allTargets, connectionsGroup);

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

				return allTargets;

			} else {

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

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

				planNode = new BulkedExternalInnerJoin(overrideTargetPlanNode,
						connectionsGroup.getBaseConnection(),
						validationSettings.getDataGraph(), path.get()
								.getTargetQueryFragment(new Variable("a"),
										new Variable("c"),
										connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider,
										Set.of()),
						false, null,
						BulkedExternalInnerJoin.getMapper("a", "c", scope, validationSettings.getDataGraph()),
						connectionsGroup, AbstractBulkJoinPlanNode.DEFAULT_VARS);
				planNode = connectionsGroup.getCachedNodeFor(planNode);
			}
		}

		return getFilterAttacherWithNegation(negatePlan, planNode, connectionsGroup);
	}

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

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

		EffectiveTarget effectiveTarget = targetChain.getEffectiveTarget(scope,
				connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider);
		String query = effectiveTarget.getQuery(false);

		Variable<Value> value;

		if (scope == Scope.nodeShape) {

			value = null;

			query += "\n" + getSparqlFilter(negatePlan, effectiveTarget.getTargetVar(), stableRandomVariableProvider);

		} else {
			value = Variable.VALUE;

			Optional<SparqlFragment> sparqlFragment = targetChain.getPath()
					.map(p -> p.getTargetQueryFragment(effectiveTarget.getTargetVar(), value,
							connectionsGroup.getRdfsSubClassOfReasoner(), stableRandomVariableProvider, Set.of()));

			String pathQuery = sparqlFragment.map(SparqlFragment::getFragment).orElseThrow(IllegalStateException::new);

			query += "\n" + pathQuery;
			query += "\n" + getSparqlFilter(negatePlan, value, stableRandomVariableProvider);
		}

		var allTargetVariables = effectiveTarget.getAllTargetVariables();

		return new ValidationQuery(getTargetChain().getNamespaces(), query, allTargetVariables, value, scope, this,
				null, null);

	}

	private String getSparqlFilter(boolean negatePlan, Variable<Value> variable,
			StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider) {
		// We use BIND and COALESCE because the filter expression could cause an error and the SHACL spec implicitly
		// says that values that cause errors are in violation of the constraint.

		assert !negatePlan : "This code has not been tested with negated plans! Should be still coalesce to true?";

		String tempVar = stableRandomVariableProvider.next().asSparqlVariable();

		return String.join("\n",
				"BIND((" + getSparqlFilterExpression(variable, negatePlan) + ") as " + tempVar + ")",
				"FILTER(COALESCE(" + tempVar + ", true))"
		);
	}

	/**
	 * Simple constraints need only implement this method to support SPARQL based validation. The returned filter body
	 * should evaluate to true for values that fail validation, unless negated==true. If the filter condition throws an
	 * error (a SPARQL runtime error, not Java error) then the error will be caught and coalesced to `true`.
	 *
	 * @param variable
	 * @param negated
	 * @return a string that is the body of a SPARQL filter
	 */
	abstract String getSparqlFilterExpression(Variable<Value> variable, boolean negated);

	private PlanNode getFilterAttacherWithNegation(boolean negatePlan, PlanNode allTargets,
			ConnectionsGroup connectionsGroup) {
		if (negatePlan) {
			allTargets = getFilterAttacher(connectionsGroup).apply(allTargets).getTrueNode(UnBufferedPlanNode.class);
		} else {
			allTargets = getFilterAttacher(connectionsGroup).apply(allTargets).getFalseNode(UnBufferedPlanNode.class);
		}
		return allTargets;
	}

	@Override
	public ValidationApproach getPreferredValidationApproach(ConnectionsGroup connectionsGroup) {
		return ValidationApproach.Transactional;
	}

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

	@Override
	public SourceConstraintComponent getConstraintComponent() {
		throw new ShaclUnsupportedException(this.getClass().getSimpleName());
	}

	abstract Function<PlanNode, FilterPlanNode> getFilterAttacher(ConnectionsGroup connectionsGroup);

	String literalToString(Literal literal) {
		IRI datatype = (literal).getDatatype();
		if (datatype == null) {
			return "\"" + literal.stringValue() + "\"";
		}
		if ((literal).getLanguage().isPresent()) {
			return "\"" + literal.stringValue() + "\"@" + (literal).getLanguage().get();
		}
		return "\"" + literal.stringValue() + "\"^^<" + datatype.stringValue() + ">";

	}

	@Override
	public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] dataGraph, Scope scope,
			StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider,
			ValidationSettings validationSettings) {

		if (scope == Scope.propertyShape) {

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

			PlanNode allTargetsPlan = effectiveTarget
					.getPlanNode(
							connectionsGroup,
							dataGraph, Scope.nodeShape,
							true,
							null
					);

			return Unique.getInstance(new ShiftToPropertyShape(allTargetsPlan, connectionsGroup),
					effectiveTarget.size() > 1, connectionsGroup);
		}
		return EmptyNode.getInstance();
	}

}