PropertyShape.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;

import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.rdf4j.model.IRI;
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.SHACL;
import org.eclipse.rdf4j.sail.shacl.SourceConstraintComponent;
import org.eclipse.rdf4j.sail.shacl.ValidationSettings;
import org.eclipse.rdf4j.sail.shacl.ast.StatementMatcher.Variable;
import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.ConstraintComponent;
import org.eclipse.rdf4j.sail.shacl.ast.paths.Path;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.EmptyNode;
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.ShiftToNodeShape;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.TargetChainPopper;
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.ValidationReportNode;
import org.eclipse.rdf4j.sail.shacl.ast.targets.TargetChain;
import org.eclipse.rdf4j.sail.shacl.results.ValidationResult;
import org.eclipse.rdf4j.sail.shacl.wrapper.data.ConnectionsGroup;
import org.eclipse.rdf4j.sail.shacl.wrapper.data.RdfsSubClassOfReasoner;
import org.eclipse.rdf4j.sail.shacl.wrapper.shape.ShapeSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PropertyShape extends Shape {
	private static final Logger logger = LoggerFactory.getLogger(PropertyShape.class);

	List<Literal> name;
	List<Literal> description;
	Value defaultValue;
	Value group;
	Value order;

	Path path;

	public PropertyShape() {
	}

	public PropertyShape(PropertyShape propertyShape) {
		super(propertyShape);
		this.name = propertyShape.name;
		this.description = propertyShape.description;
		this.defaultValue = propertyShape.defaultValue;
		this.group = propertyShape.group;
		this.path = propertyShape.path;
		this.order = propertyShape.order;
	}

	public static PropertyShape getInstance(ShaclProperties properties, ShapeSource shapeSource,
			ParseSettings parseSettings, Cache cache) {

		Shape shape = cache.get(properties.getId());

		if (shape == null) {
			shape = new PropertyShape();
			cache.put(properties.getId(), shape);
			shape.populate(properties, shapeSource, parseSettings, cache);
		}

		if (shape.constraintComponents.isEmpty()) {
			shape.deactivated = true;
		}

		return (PropertyShape) shape;
	}

	@Override
	public void populate(ShaclProperties properties, ShapeSource connection, ParseSettings parseSettings, Cache cache) {

		super.populate(properties, connection, parseSettings, cache);

		this.path = Path.buildPath(connection, properties.getPath());

		if (this.path == null) {
			throw new IllegalStateException(properties.getId() + " is a sh:PropertyShape without a sh:path!");
		}

		this.name = properties.getName();
		this.description = properties.getDescription();
		this.defaultValue = properties.getDefaultValue();
		this.order = properties.getOrder();
		this.group = properties.getGroup();

		constraintComponents = getConstraintComponents(properties, connection, parseSettings, cache);
	}

	@Override
	protected Shape shallowClone() {
		return new PropertyShape(this);
	}

	@Override
	public void toModel(Resource subject, IRI predicate, Model model, Set<Resource> cycleDetection) {

		super.toModel(subject, predicate, model, cycleDetection);
		model.add(getId(), RDF.TYPE, SHACL.PROPERTY_SHAPE);

		for (Literal literal : name) {
			model.add(getId(), SHACL.NAME, literal);
		}

		for (Literal literal : description) {
			model.add(getId(), SHACL.DESCRIPTION, literal);
		}

		if (defaultValue != null) {
			model.add(getId(), SHACL.DEFAULT_VALUE, defaultValue);
		}

		if (order != null) {
			model.add(getId(), SHACL.ORDER, order);
		}

		if (group != null) {
			model.add(getId(), SHACL.GROUP, group);
		}

		if (subject != null) {
			if (predicate == null) {
				model.add(subject, SHACL.PROPERTY, getId());
			} else {
				model.add(subject, predicate, getId());
			}
		}

//		if (cycleDetection.contains(getId())) {
//			return;
//		}
//		cycleDetection.add(getId());

		if (!cycleDetection.contains(getId())) {
			model.add(getId(), SHACL.PATH, path.getId());
			path.toModel(path.getId(), null, model, cycleDetection);
		}
		cycleDetection.add(getId());

		constraintComponents.forEach(c -> c.toModel(getId(), null, model, cycleDetection));

	}

	@Override
	public void setTargetChain(TargetChain targetChain) {
		super.setTargetChain(targetChain.add(path));
	}

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

		if (deactivated) {
			return ValidationQuery.Deactivated.getInstance();
		}

		if (!getPath().isSupported()) {
			logger.error("Unsupported SHACL feature detected: {}. Shape ignored!\n{}", path, this);
			return ValidationQuery.Deactivated.getInstance();
		}

		ValidationQuery validationQuery = constraintComponents.stream()
				.map(c -> {
					ValidationQuery validationQuery1 = c.generateSparqlValidationQuery(connectionsGroup,
							validationSettings, negatePlan,
							negateChildren, Scope.propertyShape);
					if (!(c instanceof PropertyShape)) {
						return validationQuery1.withConstraintComponent(c);
					}
					return validationQuery1;
				})
				.reduce((a, b) -> ValidationQuery.union(a, b, !produceValidationReports))
				.orElseThrow(IllegalStateException::new);

		if (produceValidationReports) {
			assert constraintComponents.size() == 1;
			assert !(constraintComponents.get(0) instanceof PropertyShape);

			validationQuery.withShape(this);
			validationQuery.withSeverity(getSeverity());
			validationQuery.makeCurrentStateValidationReport();
		}

		if (scope == Scope.propertyShape) {
			validationQuery.popTargetChain();
		} else {
			validationQuery.shiftToNodeShape();
		}

		return validationQuery;

	}

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

		if (isDeactivated()) {
			return EmptyNode.getInstance();
		}

		if (!getPath().isSupported()) {
			logger.error("Unsupported SHACL feature detected: {}. Shape ignored!\n{}", path, this);
			return EmptyNode.getInstance();
		}

		PlanNode union = EmptyNode.getInstance();

		for (ConstraintComponent constraintComponent : constraintComponents) {

			PlanNode validationPlanNode = constraintComponent
					.generateTransactionalValidationPlan(connectionsGroup, validationSettings, overrideTargetNode,
							Scope.propertyShape);

			if (produceValidationReports) {
				validationPlanNode = new ValidationReportNode(validationPlanNode, t -> {
					return new ValidationResult(t.getActiveTarget(), t.getValue(), this,
							constraintComponent, getSeverity(), t.getScope(), t.getContexts(),
							getContexts());
				}, connectionsGroup);
			}

			if (scope == Scope.propertyShape) {
				validationPlanNode = Unique.getInstance(new TargetChainPopper(validationPlanNode, connectionsGroup),
						true, connectionsGroup);
			} else {
				validationPlanNode = Unique.getInstance(new ShiftToNodeShape(validationPlanNode, connectionsGroup),
						true, connectionsGroup);
			}

			union = UnionNode.getInstance(connectionsGroup, union, validationPlanNode);
		}

		return union;
	}

	@Override
	public PlanNode getAllTargetsPlan(ConnectionsGroup connectionsGroup, Resource[] dataGraph, Scope scope,
			StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider,
			ValidationSettings validationSettings) {
		PlanNode planNode = constraintComponents.stream()
				.map(c -> c.getAllTargetsPlan(connectionsGroup, dataGraph, Scope.propertyShape,
						new StatementMatcher.StableRandomVariableProvider(), validationSettings))
				.distinct()
				.reduce((nodes, nodes2) -> UnionNode.getInstanceDedupe(connectionsGroup, nodes, nodes2))
				.orElse(EmptyNode.getInstance());

		if (connectionsGroup.getStats().hasRemoved()) {
			PlanNode planNodeEffectiveTarget = getTargetChain()
					.getEffectiveTarget(Scope.propertyShape, connectionsGroup.getRdfsSubClassOfReasoner(),
							stableRandomVariableProvider)
					.getPlanNode(connectionsGroup, dataGraph, Scope.propertyShape, true, null);

			planNode = UnionNode.getInstanceDedupe(connectionsGroup, planNode, planNodeEffectiveTarget);
		}

		if (scope == Scope.propertyShape) {
			planNode = Unique.getInstance(new TargetChainPopper(planNode, connectionsGroup), true, connectionsGroup);
		} else {
			planNode = new ShiftToNodeShape(planNode, connectionsGroup);
		}

		planNode = Unique.getInstance(planNode, false, connectionsGroup);

		return planNode;
	}

	@Override
	public ValidationApproach getPreferredValidationApproach(ConnectionsGroup connectionsGroup) {
		return constraintComponents.stream()
				.map(constraintComponent -> constraintComponent.getPreferredValidationApproach(connectionsGroup))
				.reduce(ValidationApproach::reducePreferred)
				.orElse(ValidationApproach.MOST_COMPATIBLE);
	}

	public Path getPath() {
		return path;
	}

	@Override
	public boolean requiresEvaluation(ConnectionsGroup connectionsGroup, Scope scope, Resource[] dataGraph,
			StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider) {
		if (!getPath().isSupported()) {
			logger.error("Unsupported SHACL feature detected: {}. Shape ignored!\n{}", path, this);
			return false;
		}

		return super.requiresEvaluation(connectionsGroup, scope, dataGraph, stableRandomVariableProvider);
	}

	@Override
	public ConstraintComponent deepClone() {
		PropertyShape nodeShape = new PropertyShape(this);

		nodeShape.constraintComponents = constraintComponents.stream()
				.map(ConstraintComponent::deepClone)
				.collect(Collectors.toList());

		return nodeShape;
	}

	@Override
	public SparqlFragment buildSparqlValidNodes_rsx_targetShape(Variable<Value> subject,
			Variable<Value> object,
			RdfsSubClassOfReasoner rdfsSubClassOfReasoner, Scope scope,
			StatementMatcher.StableRandomVariableProvider stableRandomVariableProvider) {

		List<SparqlFragment> sparqlFragments = constraintComponents.stream()
				.map(shape -> shape.buildSparqlValidNodes_rsx_targetShape(object,
						stableRandomVariableProvider.next(), rdfsSubClassOfReasoner, Scope.propertyShape,
						stableRandomVariableProvider))
				.collect(Collectors.toList());

		if (SparqlFragment.isFilterCondition(sparqlFragments)) {
			return SparqlFragment.and(sparqlFragments);
		} else {
			return SparqlFragment.join(sparqlFragments);
		}

	}

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

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

		PropertyShape that = (PropertyShape) o;

		if (!Objects.equals(name, that.name)) {
			return false;
		}
		if (!Objects.equals(description, that.description)) {
			return false;
		}
		if (!Objects.equals(defaultValue, that.defaultValue)) {
			return false;
		}
		if (!Objects.equals(group, that.group)) {
			return false;
		}
		return Objects.equals(path, that.path);
	}

	@Override
	public int hashCode() {
		int result = super.hashCode();
		result = 31 * result + (name != null ? name.hashCode() : 0);
		result = 31 * result + (description != null ? description.hashCode() : 0);
		result = 31 * result + (defaultValue != null ? defaultValue.hashCode() : 0);
		result = 31 * result + (group != null ? group.hashCode() : 0);
		result = 31 * result + (path != null ? path.hashCode() : 0);
		return result;
	}
}