ShapeValidationContainer.java

/*******************************************************************************
 * Copyright (c) 2023 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;

import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import org.eclipse.rdf4j.common.iteration.CloseableIteration;
import org.eclipse.rdf4j.sail.SailException;
import org.eclipse.rdf4j.sail.shacl.ast.Shape;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.PlanNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.SingleCloseablePlanNode;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.ValidationExecutionLogger;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.ValidationTuple;
import org.eclipse.rdf4j.sail.shacl.results.lazy.ValidationResultIterator;
import org.eclipse.rdf4j.sail.shacl.wrapper.data.ConnectionsGroup;
import org.slf4j.Logger;

class ShapeValidationContainer {
	private final Shape shape;
	private final boolean logValidationViolations;
	private final PlanNode planNode;
	private final ValidationExecutionLogger validationExecutionLogger;
	private final long effectiveValidationResultsLimitPerConstraint;
	private final boolean performanceLogging;
	private final Logger logger;

	public ShapeValidationContainer(Shape shape, Supplier<PlanNode> planNodeSupplier, boolean logValidationExecution,
			boolean logValidationViolations, long effectiveValidationResultsLimitPerConstraint,
			boolean performanceLogging, boolean logValidationPlans, Logger logger, ConnectionsGroup connectionsGroup) {
		this.shape = shape;
		this.logValidationViolations = logValidationViolations;
		this.effectiveValidationResultsLimitPerConstraint = effectiveValidationResultsLimitPerConstraint;
		this.performanceLogging = performanceLogging;
		this.logger = logger;
		try {
			PlanNode planNode = planNodeSupplier.get();

			if (logValidationPlans) {

				StringBuilder planAsGraphvizDot = new StringBuilder();

				planAsGraphvizDot.append(
						"rank1 [style=invisible];\n" +
								"rank2 [style=invisible];\n" +
								"\n" +
								"rank1 -> rank2 [color=white];\n");

				planAsGraphvizDot.append("{\n")
						.append("\trank = same;\n")
						.append("\trank2 -> ")
						.append(System.identityHashCode(connectionsGroup.getBaseConnection()))
						.append(" -> ")
						.append(System.identityHashCode(connectionsGroup.getAddedStatements()))
						.append(" -> ")
						.append(System.identityHashCode(connectionsGroup.getRemovedStatements()))
						.append(" [ style=invis ];\n")
						.append("\trankdir = LR;\n")
						.append("}\n");

				planAsGraphvizDot.append(System.identityHashCode(connectionsGroup.getBaseConnection()))
						.append(" [label=\"")
						.append("BaseConnection")
						.append("\" fillcolor=\"#CACADB\", style=filled];")
						.append("\n");

				planAsGraphvizDot.append(System.identityHashCode(connectionsGroup.getAddedStatements()))
						.append(" [label=\"")
						.append("AddedStatements")
						.append("\" fillcolor=\"#CEDBCA\", style=filled];")
						.append("\n");

				planAsGraphvizDot.append(System.identityHashCode(connectionsGroup.getRemovedStatements()))
						.append(" [label=\"")
						.append("RemovedStatements")
						.append("\" fillcolor=\"#DBCFC9r\", style=filled];")
						.append("\n");

				planNode.getPlanAsGraphvizDot(planAsGraphvizDot);

				String[] split = planAsGraphvizDot.toString().split("\n");
				planAsGraphvizDot = new StringBuilder();
				Arrays.stream(split).map(s -> "\t" + s + "\n").forEach(planAsGraphvizDot::append);

				logger.info("Plan as Graphviz dot:\ndigraph G {\n{}}", planAsGraphvizDot);
			}

			this.validationExecutionLogger = ValidationExecutionLogger
					.getInstance(logValidationExecution);
			if (!(planNode.isGuaranteedEmpty())) {
				assert planNode instanceof SingleCloseablePlanNode;
				planNode.receiveLogger(validationExecutionLogger);
				this.planNode = planNode;
			} else {
				this.planNode = planNode;
			}
		} catch (Throwable e) {
			logger.warn("Error processing SHACL Shape {}", shape.getId(), e);
			logger.warn("Error processing SHACL Shape\n{}", shape, e);
			if (e instanceof Error) {
				throw e;
			}
			throw new SailException("Error processing SHACL Shape " + shape.getId() + "\n" + shape, e);
		}

	}

	public Shape getShape() {
		return shape;
	}

	public boolean hasPlanNode() {
		return !(planNode.isGuaranteedEmpty());
	}

	public ValidationResultIterator performValidation() {
		long before = getTimeStamp();
		handlePreLogging();

		ValidationResultIterator validationResults = null;

		try (CloseableIteration<? extends ValidationTuple> iterator = planNode.iterator()) {
			validationResults = new ValidationResultIterator(iterator, effectiveValidationResultsLimitPerConstraint);
			return validationResults;
		} catch (Throwable e) {
			logger.warn("Internal error while trying to validate SHACL Shape {}", shape.getId(), e);
			logger.warn("Internal error while trying to validate SHACL Shape\n{}", shape, e);
			if (e instanceof Error) {
				throw e;
			}
			throw new SailException(
					"Internal error while trying to validate SHACL Shape " + shape.getId() + "\n" + shape, e);
		} finally {
			handlePostLogging(before, validationResults);
		}
	}

	private long getTimeStamp() {
		if (performanceLogging) {
			return System.currentTimeMillis();
		}
		return 0;
	}

	private void handlePreLogging() {
		if (validationExecutionLogger.isEnabled()) {
			logger.info("Start execution of plan:\n{}\n", getShape().toString());
		}
	}

	private void handlePostLogging(long before, ValidationResultIterator validationResults) {
		if (validationExecutionLogger.isEnabled()) {
			validationExecutionLogger.flush();
		}

		if (validationResults != null) {
			if (performanceLogging) {
				long after = System.currentTimeMillis();
				logger.info("Execution of plan took {} ms for:\n{}\n",
						(after - before),
						getShape().toString());
			}

			if (validationExecutionLogger.isEnabled()) {
				logger.info("Finished execution of plan:\n{}\n",
						getShape().toString());
			}

			if (logValidationViolations) {
				if (!validationResults.conforms()) {
					List<ValidationTuple> tuples = validationResults.getTuples();

					logger.info(
							"SHACL not valid. The following experimental debug results were produced:\n\t\t{}\n\n{}\n",
							tuples.stream()
									.map(ValidationTuple::toString)
									.collect(Collectors.joining("\n\t\t")),
							getShape().toString()

					);
				}
			}

		}

	}

}