ValidationTuple.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.planNodes;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.Value;
import org.eclipse.rdf4j.query.BindingSet;
import org.eclipse.rdf4j.query.algebra.evaluation.util.ValueComparator;
import org.eclipse.rdf4j.sail.shacl.ast.constraintcomponents.ConstraintComponent;
import org.eclipse.rdf4j.sail.shacl.results.ValidationResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ValidationTuple {

	private static final Resource[] NULL_CONTEXT = { null };

	private static final Logger logger = LoggerFactory.getLogger(ValidationTuple.class);
	private static final ValueComparator valueComparator = new ValueComparator();

	// all fields should be immutable
	private final Value[] chain;
	private final ConstraintComponent.Scope scope;
	private final boolean propertyShapeScopeWithValue;
	private final List<ValidationResult> validationResults;
	private final Set<ValidationTuple> compressedTuples;

	private final Resource[] contexts;

	public ValidationTuple(BindingSet bindingSet, String[] variables, ConstraintComponent.Scope scope,
			boolean hasValue, Resource[] contexts) {
		this(bindingSet, Arrays.asList(variables), scope, hasValue, contexts);
	}

	public ValidationTuple(BindingSet bindingSet, List<String> variables, ConstraintComponent.Scope scope,
			boolean hasValue, Resource[] contexts) {

		chain = new Value[variables.size()];

		for (int i = 0; i < variables.size(); i++) {
			chain[i] = bindingSet.getValue(variables.get(i));
		}

		this.scope = scope;
		this.propertyShapeScopeWithValue = hasValue;
		this.validationResults = List.of();
		this.compressedTuples = Set.of();
		this.contexts = contexts;
	}

	public ValidationTuple(List<Value> chain, ConstraintComponent.Scope scope, boolean hasValue, Resource[] contexts) {
		this.chain = chain.toArray(new Value[0]);
		this.scope = scope;
		this.propertyShapeScopeWithValue = hasValue;
		this.validationResults = List.of();
		this.compressedTuples = Set.of();
		this.contexts = contexts;
	}

	// We will assume that the provided chain will not be mutated elsewhere.
	public ValidationTuple(Value[] chain, ConstraintComponent.Scope scope, boolean hasValue, Resource[] contexts) {
		this.chain = chain;
		this.scope = scope;
		this.propertyShapeScopeWithValue = hasValue;
		this.validationResults = List.of();
		this.compressedTuples = Set.of();
		this.contexts = contexts;
	}

	public ValidationTuple(Value a, Value c, ConstraintComponent.Scope scope, boolean hasValue, Resource[] contexts) {
		chain = new Value[] { a, c };

		this.scope = scope;
		this.propertyShapeScopeWithValue = hasValue;
		this.validationResults = List.of();
		this.compressedTuples = Set.of();
		this.contexts = contexts;
	}

	public ValidationTuple(Value subject, ConstraintComponent.Scope scope, boolean hasValue, Resource[] contexts) {
		chain = new Value[] { subject };
		this.scope = scope;
		this.propertyShapeScopeWithValue = hasValue;
		this.validationResults = List.of();
		this.compressedTuples = Set.of();
		this.contexts = contexts;
	}

	private ValidationTuple(List<ValidationResult> validationResults, Value[] chain,
			ConstraintComponent.Scope scope, boolean propertyShapeScopeWithValue,
			Set<ValidationTuple> compressedTuples, Resource[] contexts) {
		this.validationResults = Collections.unmodifiableList(validationResults);
		this.chain = chain;
		this.scope = scope;
		this.propertyShapeScopeWithValue = propertyShapeScopeWithValue;
		this.compressedTuples = compressedTuples.isEmpty() ? Set.of() : Collections.unmodifiableSet(compressedTuples);
		this.contexts = contexts;

	}

	public ValidationTuple(ValidationTuple tuple, Set<ValidationTuple> compressedTuples) {
		this.validationResults = tuple.validationResults;
		this.chain = tuple.chain;
		this.scope = tuple.scope;
		this.propertyShapeScopeWithValue = tuple.propertyShapeScopeWithValue;
		this.compressedTuples = compressedTuples.isEmpty() ? Set.of() : Collections.unmodifiableSet(compressedTuples);
		this.contexts = tuple.contexts;
	}

	public boolean sameTargetAs(ValidationTuple other) {

		Value current = getActiveTarget();
		Value currentRight = other.getActiveTarget();

		return current.equals(currentRight);
	}

	public boolean hasValue() {
		assert scope != null;
		return propertyShapeScopeWithValue || scope == ConstraintComponent.Scope.nodeShape;
	}

	public Value getValue() {
		assert scope != null;
		if (hasValue()) {
			return chain[(chain.length - 1)];
		}

		return null;
	}

	public ConstraintComponent.Scope getScope() {
		return scope;
	}

	public int compareActiveTarget(ValidationTuple other) {

		Value left = getActiveTarget();
		Value right = other.getActiveTarget();

		return valueComparator.compare(left, right);
	}

	public int compareFullTarget(ValidationTuple other) {

		int min = Math.min(getFullChainSize(false), other.getFullChainSize(false));

		List<Value> targetChain = getTargetChain(false);
		List<Value> otherTargetChain = other.getTargetChain(false);

		Iterator<Value> iterator = targetChain.iterator();

		for (int i = 0; i < min; i++) {
			Value value = iterator.next();
			int compare = valueComparator.compare(value, otherTargetChain.get(i));
			if (compare != 0) {
				return compare;
			}
		}

		return Integer.compare(getFullChainSize(true), other.getFullChainSize(true));
	}

	public List<ValidationResult> getValidationResult() {
		return validationResults;
	}

	public ValidationTuple addValidationResult(Function<ValidationTuple, ValidationResult> validationResult) {
		List<ValidationResult> validationResults;
		if (!this.validationResults.isEmpty()) {
			validationResults = new ArrayList<>(this.validationResults);
			validationResults.add(validationResult.apply(this));
		} else {
			validationResults = Collections.singletonList(validationResult.apply(this));
		}

		Set<ValidationTuple> compressedTuples = enrichCompressedTuples(t -> t.addValidationResult(validationResult));

		return new ValidationTuple(validationResults, chain, scope, propertyShapeScopeWithValue, compressedTuples,
				contexts);
	}

	public Value getActiveTarget() {
		assert scope != null;
		if (!propertyShapeScopeWithValue || scope != ConstraintComponent.Scope.propertyShape) {
			return chain[chain.length - 1];
		}

		assert chain.length >= 2;

		return chain[chain.length - 2];

	}

	@Override
	public String toString() {
		try {
			return "ValidationTuple(" + scope + ") [ " + Arrays.stream(chain).map(v -> {
				if (v == getActiveTarget()) {
					return "T���" + v;
				} else if (v == getValue()) {
					return "V���" + v + "";
				}
				return v.stringValue();
			}).collect(Collectors.joining(" -> ")) + " }, propertyShapeScopeWithValue=" + propertyShapeScopeWithValue +
					", compressedTuples=" + Arrays.toString(compressedTuples.toArray());

		} catch (Throwable t) {
			return "ValidationTuple(" + scope + ") { "
					+ Arrays.stream(chain).map(Value::stringValue).collect(Collectors.joining(" -> "))
					+ " }, propertyShapeScopeWithValue=" + propertyShapeScopeWithValue +
					", compressedTuples=" + Arrays.toString(compressedTuples.toArray());
		}

	}

	public List<ValidationTuple> shiftToNodeShape() {
		assert scope == ConstraintComponent.Scope.propertyShape;

		if (compressedTuples.isEmpty()) {
			Value[] chain;
			boolean propertyShapeScopeWithValue = this.propertyShapeScopeWithValue;
			ConstraintComponent.Scope scope = ConstraintComponent.Scope.nodeShape;

			if (this.propertyShapeScopeWithValue) {
				propertyShapeScopeWithValue = false;
				chain = Arrays.copyOf(this.chain, this.chain.length - 1);
			} else {
				chain = this.chain;
			}

			return Collections
					.singletonList(new ValidationTuple(this.validationResults, chain, scope,
							propertyShapeScopeWithValue, Set.of(), contexts));

		} else {
			return this.compressedTuples.stream()
					.map(t -> {
						List<Value> chain = Arrays.asList(t.chain);

						boolean propertyShapeScopeWithValue = t.propertyShapeScopeWithValue;
						ConstraintComponent.Scope scope = ConstraintComponent.Scope.nodeShape;

						if (this.propertyShapeScopeWithValue) {
							propertyShapeScopeWithValue = false;
							chain = chain.subList(0, chain.size() - 1);
						}

						return new ValidationTuple(t.validationResults, chain.toArray(new Value[0]), scope,
								propertyShapeScopeWithValue,
								Set.of(), t.contexts);

					})
					.collect(Collectors.toList());

		}

	}

	public List<ValidationTuple> shiftToPropertyShapeScope() {
		assert scope == ConstraintComponent.Scope.nodeShape;
		assert chain.length >= 2;

		boolean propertyShapeScopeWithValue = true;
		ConstraintComponent.Scope scope = ConstraintComponent.Scope.propertyShape;

		if (!compressedTuples.isEmpty()) {

			return compressedTuples.stream()
					.map(t -> new ValidationTuple(t.validationResults, t.chain, scope, propertyShapeScopeWithValue,
							Set.of(), t.contexts))
					.collect(Collectors.toList());

		} else {
			return Collections.singletonList(
					new ValidationTuple(this.validationResults, chain, scope, propertyShapeScopeWithValue,
							Set.of(), contexts));
		}

	}

	public int getFullChainSize(boolean includePropertyShapeValue) {
		if (!includePropertyShapeValue && propertyShapeScopeWithValue) {
			return chain.length - 1;
		}

		return chain.length;

	}

	/**
	 * This is only the target part. For property shape scope it will not include the value.
	 *
	 * @param includePropertyShapeValues
	 */
	public List<Value> getTargetChain(boolean includePropertyShapeValues) {

		if (scope == ConstraintComponent.Scope.propertyShape && hasValue() && !includePropertyShapeValues) {
			return Collections.unmodifiableList(Arrays.asList(chain).subList(0, chain.length - 1));
		}

		return Collections.unmodifiableList(Arrays.asList(chain));
	}

	public ValidationTuple setValue(Value value) {
		assert value != null;
		if (value.equals(getValue())) {
			return this;
		}

		assert scope == ConstraintComponent.Scope.propertyShape
				: "Can't set value on NodeShape scoped ValidationTuple because it will also change the target!";

		Value[] chain;

		if (propertyShapeScopeWithValue) {
			// we will replace the last value, so we just need a copy because the chain should be immutable
			chain = Arrays.copyOf(this.chain, this.chain.length);
		} else {
			chain = Arrays.copyOf(this.chain, this.chain.length + 1);
		}

		chain[chain.length - 1] = value;

		Set<ValidationTuple> compressedTuples = enrichCompressedTuples(t -> t.setValue(value));

		return new ValidationTuple(this.validationResults, chain, scope, true, compressedTuples, contexts);
	}

	public ValidationTuple shiftToPropertyShapeScope(Value value) {
		assert scope == ConstraintComponent.Scope.nodeShape
				: "Can't shift to property shape scope on a property shape scoped ValidationTuple.";

		Value[] chain;

		chain = Arrays.copyOf(this.chain, this.chain.length + 1);

		chain[chain.length - 1] = value;

		Set<ValidationTuple> compressedTuples = enrichCompressedTuples(t -> t.setValue(value));

		return new ValidationTuple(this.validationResults, chain, ConstraintComponent.Scope.propertyShape, true,
				compressedTuples, contexts);
	}

	private Set<ValidationTuple> enrichCompressedTuples(
			Function<ValidationTuple, ValidationTuple> validationTupleValidationTupleFunction) {
		if (compressedTuples.isEmpty()) {
			return compressedTuples;
		}

		return this.compressedTuples.stream()
				.map(validationTupleValidationTupleFunction)
				.collect(Collectors.toSet());

	}

	public int compareValue(ValidationTuple other) {
		Value left = getValue();
		Value right = other.getValue();

		return valueComparator.compare(left, right);
	}

	public ValidationTuple trimToTarget() {
		if (scope == ConstraintComponent.Scope.propertyShape) {
			if (propertyShapeScopeWithValue) {

				Value[] chain = Arrays.copyOf(this.chain, this.chain.length - 1);

				Set<ValidationTuple> compressedTuples = enrichCompressedTuples(ValidationTuple::trimToTarget);

				return new ValidationTuple(validationResults, chain, scope, false, compressedTuples, contexts);
			}
		}
		return this;
	}

	public List<ValidationTuple> pop() {

		if (compressedTuples.isEmpty()) {

			Value[] chain;

			boolean propertyShapeScopeWithValue = this.propertyShapeScopeWithValue;
			if (getScope() == ConstraintComponent.Scope.propertyShape) {
				if (hasValue()) {
					assert this.chain.length > 1 : "Attempting to pop chain will not leave any elements on the chain! "
							+ this;
					chain = Arrays.copyOf(this.chain, this.chain.length - 1);
				} else {
					propertyShapeScopeWithValue = true;
					chain = this.chain;
				}
			} else {
				assert this.chain.length > 1 : "Attempting to pop chain will not leave any elements on the chain! "
						+ this;
				chain = Arrays.copyOf(this.chain, this.chain.length - 1);
			}

			return Collections.singletonList(
					new ValidationTuple(this.validationResults, chain, scope, propertyShapeScopeWithValue,
							Set.of(), contexts));
		} else {

			return compressedTuples.stream().flatMap(t1 -> {
				return t1.pop()
						.stream()
						.map(t -> new ValidationTuple(t.validationResults, t.chain, t.scope,
								t.propertyShapeScopeWithValue, t.compressedTuples, t.contexts));
			}).collect(Collectors.toList());

		}

	}

	public Set<ValidationTuple> getCompressedTuples() {
		return compressedTuples;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}
		ValidationTuple that = (ValidationTuple) o;
		return propertyShapeScopeWithValue == that.propertyShapeScopeWithValue && Arrays.equals(chain, that.chain)
				&& scope == that.scope && validationResults.equals(that.validationResults)
				&& compressedTuples.equals(that.compressedTuples);
	}

	@Override
	public int hashCode() {
		return Objects.hash(Arrays.hashCode(chain), scope, propertyShapeScopeWithValue, validationResults,
				compressedTuples);
	}

	public ValidationTuple join(ValidationTuple right) {

		Set<ValidationTuple> compressedTuples;
		if (this.compressedTuples.isEmpty()) {
			compressedTuples = right.getCompressedTuples();
		} else if (right.compressedTuples.isEmpty()) {
			compressedTuples = this.compressedTuples;
		} else {
			compressedTuples = new HashSet<>(this.compressedTuples);
			compressedTuples.addAll(right.getCompressedTuples());
		}

		Resource[] contexts;

		if (this.contexts != right.contexts) {
			assert this.contexts != null;
			assert right.contexts != null;
			if (this.contexts.length == 1 && right.contexts.length == 1
					&& this.contexts[0] == right.contexts[0]) {
				contexts = this.contexts;
			} else if (this.contexts.length > 0 && right.contexts.length > 0) {
				contexts = Arrays.copyOf(this.contexts, this.contexts.length + right.contexts.length);
				System.arraycopy(right.contexts, 0, contexts, this.contexts.length, right.contexts.length);
			} else if (right.contexts.length > 0) {
				// this.contexts must be an empty array
				contexts = right.contexts;
			} else {
				contexts = this.contexts;
			}
		} else {
			contexts = this.contexts;
		}

		ValidationTuple validationTuple = new ValidationTuple(validationResults, chain, scope,
				propertyShapeScopeWithValue, compressedTuples, contexts);
		if (scope == ConstraintComponent.Scope.propertyShape) {
			if (right.hasValue()) {
				validationTuple = validationTuple.setValue(right.getValue());
			}
		}

		for (ValidationResult validationResult : right.getValidationResult()) {
			validationTuple = validationTuple.addValidationResult(t -> validationResult);
		}

		return validationTuple;
	}

	public Resource[] getContexts() {
		return contexts;
	}

}