ShapesGraphTest.java

/*******************************************************************************
 * Copyright (c) 2022 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 static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.eclipse.rdf4j.model.IRI;
import org.eclipse.rdf4j.model.util.Values;
import org.eclipse.rdf4j.model.vocabulary.FOAF;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.RDF4J;
import org.eclipse.rdf4j.repository.Repository;
import org.eclipse.rdf4j.repository.RepositoryConnection;
import org.eclipse.rdf4j.repository.RepositoryException;
import org.eclipse.rdf4j.repository.sail.SailRepository;
import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection;
import org.eclipse.rdf4j.sail.memory.MemoryStore;
import org.eclipse.rdf4j.sail.shacl.ast.ContextWithShape;
import org.eclipse.rdf4j.sail.shacl.ast.planNodes.PlanNode;
import org.eclipse.rdf4j.sail.shacl.wrapper.data.ConnectionsGroup;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class ShapesGraphTest {

	static final private String EX = "http://example.com/ns#";

	static final private IRI data1 = Values.iri(EX, "data1");
	static final private IRI data2 = Values.iri(EX, "data2");

	static final private IRI Human = Values.iri(EX, "Human");

	static final private IRI laura = Values.iri(EX, "laura");
	static final private IRI steve = Values.iri(EX, "steve");
	static final private IRI olivia = Values.iri(EX, "olivia");

	@Test
	public void testValidSplitAcrossGraphs() throws Throwable {

		test(repository -> {

			try (RepositoryConnection connection = repository.getConnection()) {
				connection.begin();
				connection.add(laura, RDF.TYPE, FOAF.PERSON, data1);
				connection.add(laura, FOAF.NAME, Values.literal("Laura"), data1);

				connection.add(laura, RDF.TYPE, FOAF.PERSON, data2);
				connection.add(laura, FOAF.NAME, Values.literal("Laura"), data2);

				connection.add(laura, FOAF.KNOWS, steve, data1);
				connection.add(steve, RDF.TYPE, FOAF.PERSON, data1);
				connection.add(steve, FOAF.NAME, Values.literal("Steve"), data1);

				connection.add(laura, FOAF.KNOWS, olivia, data2);
				connection.add(olivia, RDF.TYPE, Human, data2);

				connection.commit();
			}

		});

	}

	@Test
	public void testInvalid() throws Throwable {

		test(repository -> {

			assertThrows(RepositoryException.class, () -> {
				try (RepositoryConnection connection = repository.getConnection()) {
					connection.begin();
					connection.add(laura, RDF.TYPE, FOAF.PERSON, data2);
					connection.commit();
				}
			});

		});

	}

	@Test
	public void testInvalidUnionGraph() throws Throwable {

		test(repository -> {

			try (RepositoryConnection connection = repository.getConnection()) {
				connection.begin();
				connection.add(laura, RDF.TYPE, FOAF.PERSON, data2);
				connection.add(laura, FOAF.NAME, Values.literal("Laura"), data2);
				connection.commit();
			}

			assertThrows(RepositoryException.class, () -> {
				try (RepositoryConnection connection = repository.getConnection()) {
					connection.begin();

					connection.add(laura, FOAF.PHONE, Values.literal(1));
					connection.add(laura, FOAF.PHONE, Values.literal(2), data1);
					connection.add(laura, FOAF.PHONE, Values.literal(3), data2);

					connection.commit();
				}
			});

		});

	}

	@Test
	public void testValidUnionGraphMinCount() throws Throwable {

		test(repository -> {

			try (RepositoryConnection connection = repository.getConnection()) {
				connection.begin();
				connection.add(laura, RDF.TYPE, FOAF.PERSON, data2);
				connection.add(laura, FOAF.NAME, Values.literal("Laura"), data2);
				connection.commit();
			}

			try (RepositoryConnection connection = repository.getConnection()) {
				connection.begin();

				connection.add(laura, FOAF.INTEREST, Values.literal("golf"));
				connection.add(laura, FOAF.INTEREST, Values.literal("tennis"), data1);
				connection.add(laura, FOAF.INTEREST, Values.literal("chess"), data2);

				connection.commit();
			}

		});

	}

	@Test
	public void testInvalidUnionGraphMinCount() throws Throwable {

		test(repository -> {

			try (RepositoryConnection connection = repository.getConnection()) {
				connection.begin();
				connection.add(laura, RDF.TYPE, FOAF.PERSON, data2);
				connection.add(laura, FOAF.NAME, Values.literal("Laura"), data2);
				connection.commit();
			}

			assertThrows(RepositoryException.class, () -> {
				try (RepositoryConnection connection = repository.getConnection()) {
					connection.begin();

					connection.add(laura, FOAF.INTEREST, Values.literal("golf"));
					connection.add(laura, FOAF.INTEREST, Values.literal("golf"), data1);
					connection.add(laura, FOAF.INTEREST, Values.literal("golf"), data2);

					connection.commit();
				}
			});

		});

	}

	@Test
	public void testInvalidUnionGraphMinCountSparql() throws Throwable {

		test(repository -> {

			assertThrows(RepositoryException.class, () -> {
				try (RepositoryConnection connection = repository.getConnection()) {
					connection.begin(ShaclSail.TransactionSettings.ValidationApproach.Bulk);
					connection.add(laura, RDF.TYPE, FOAF.PERSON, data2);
					connection.add(laura, FOAF.NAME, Values.literal("Laura"), data2);
					connection.add(laura, FOAF.INTEREST, Values.literal("golf"));
					connection.add(laura, FOAF.INTEREST, Values.literal("golf"), data1);
					connection.add(laura, FOAF.INTEREST, Values.literal("golf"), data2);
					connection.commit();
				}
			});

		});

	}

	@Test
	public void testValidUnionGraph() throws Throwable {

		test(repository -> {

			try (RepositoryConnection connection = repository.getConnection()) {
				connection.begin();
				connection.add(laura, RDF.TYPE, FOAF.PERSON, data2);
				connection.add(laura, FOAF.NAME, Values.literal("Laura"), data2);
				connection.commit();
			}

			try (RepositoryConnection connection = repository.getConnection()) {
				connection.begin();

				connection.add(laura, FOAF.PHONE, Values.literal(1));
				connection.add(laura, FOAF.PHONE, Values.literal(1), data1);
				connection.add(laura, FOAF.PHONE, Values.literal(1), data2);

				connection.commit();
			}

		});

	}

	@Test
	public void testValidUnionGraphSparql() throws Throwable {

		test(repository -> {

			try (RepositoryConnection connection = repository.getConnection()) {
				connection.begin();
				connection.add(laura, RDF.TYPE, FOAF.PERSON, data2);
				connection.add(laura, FOAF.NAME, Values.literal("Laura"), data2);
				connection.commit();
			}

			try (RepositoryConnection connection = repository.getConnection()) {
				connection.begin(ShaclSail.TransactionSettings.ValidationApproach.Bulk);

				connection.add(laura, FOAF.PHONE, Values.literal(1));
				connection.add(laura, FOAF.PHONE, Values.literal(1), data1);
				connection.add(laura, FOAF.PHONE, Values.literal(1), data2);

				connection.commit();
			}

		});

	}

	@Test
	public void testInvalidSplitAcrossGraphs() throws Throwable {

		test(repository -> {

			assertThrows(RepositoryException.class, () -> {
				try (RepositoryConnection connection = repository.getConnection()) {
					connection.begin();
					connection.add(laura, RDF.TYPE, FOAF.PERSON);
					connection.add(laura, FOAF.NAME, Values.literal("Laura"), data2);
					connection.commit();
				}
			});

		});

	}

	@Test
	public void testInvalidSwitchGraph() throws Throwable {

		test(repository -> {

			assertThrows(RepositoryException.class, () -> {
				try (RepositoryConnection connection = repository.getConnection()) {
					connection.begin();
					connection.add(laura, RDF.TYPE, FOAF.PERSON);
					connection.add(laura, FOAF.NAME, Values.literal("Laura"));
					connection.commit();

					connection.begin();
					connection.remove(laura, FOAF.NAME, null);
					connection.add(laura, FOAF.NAME, Values.literal("Laura"), data2);
					connection.commit();

				}
			});

		});

	}

	@Test
	public void testValidationRequired() throws IOException, InterruptedException {

		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		SailRepository repository = new SailRepository(shaclSail);

		shaclSail.setShapesGraphs(Set.of(
				Values.iri(EX, "peopleKnowPeopleShapes"),
				Values.iri(EX, "peopleKnowHumansShapes")
		));

		loadShapes(repository);

		try (ShaclSailConnection connection = (ShaclSailConnection) shaclSail.getConnection()) {
			connection.begin();
			connection.addStatement(Values.bnode(), RDF.TYPE, FOAF.PERSON, data1);

			connection.prepareValidation(new ValidationSettings());

			try (ConnectionsGroup connectionsGroup = connection.getConnectionsGroup()) {

				List<PlanNode> collect = shaclSail.getCachedShapes()
						.getDataAndRelease()
						.stream()
						.map(ContextWithShape::getShape)
						.map(shape -> shape.generatePlans(connectionsGroup, new ValidationSettings()))
						.filter(s -> !(s.isGuaranteedEmpty()))
						.collect(Collectors.toList());

				Assertions.assertEquals(0, collect.size());
			}
		}

		repository.shutDown();

	}

	@Test
	public void testDefaultShapesGraph() throws IOException {

		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		SailRepository repository = new SailRepository(shaclSail);

		loadShapes(repository);

		try (RepositoryConnection connection = repository.getConnection()) {
			connection.begin();
			connection.add(laura, RDF.TYPE, FOAF.PERSON, Values.iri("http://example.org/differentGraph"));
			connection.add(laura, FOAF.PHONE, Values.literal(1));
			connection.add(laura, FOAF.PHONE, Values.literal(2), data2);
			connection.commit();
		}

		assertThrows(RepositoryException.class, () -> {
			try (RepositoryConnection connection = repository.getConnection()) {
				connection.begin();
				connection.add(laura, FOAF.PHONE, Values.literal(3), data1);
				connection.commit();
			}
		});

		repository.shutDown();

	}

	@Test
	public void testDefaultShapesGraph2() throws IOException {

		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		SailRepository repository = new SailRepository(shaclSail);

		loadShapes(repository);

		try (RepositoryConnection connection = repository.getConnection()) {
			connection.begin();
			connection.add(laura, RDF.TYPE, FOAF.PERSON, Values.iri("http://example.org/differentGraph"));
			connection.add(laura, FOAF.PHONE, Values.literal(1));
			connection.add(laura, FOAF.PHONE, Values.literal(1), data2);
			connection.commit();
		}

		try (RepositoryConnection connection = repository.getConnection()) {
			connection.begin();
			connection.add(laura, FOAF.PHONE, Values.literal(1), data1);
			connection.commit();
		}

		repository.shutDown();

	}

	private void loadShapes(SailRepository repository) throws IOException {
		try (SailRepositoryConnection connection = repository.getConnection()) {
			connection.begin(ShaclSail.TransactionSettings.ValidationApproach.Disabled);
			connection.add(ShapesGraphTest.class.getClassLoader().getResource("multipleShapesGraphs.trig"));
			connection.commit();
		}
	}

	private void test(Consumer<Repository> testCase) throws Throwable {

		ShaclSail shaclSail = new ShaclSail(new MemoryStore());
		SailRepository repository = new SailRepository(shaclSail);

		shaclSail.setShapesGraphs(Set.of(
				RDF4J.SHACL_SHAPE_GRAPH,
				Values.iri(EX, "peopleKnowPeopleShapes"),
				Values.iri(EX, "peopleKnowHumansShapes"),
				Values.iri(EX, "mustHaveNameShapes"),
				Values.iri(EX, "maxFiveAcquaintances"),
				Values.iri(EX, "nestedKnowsShouldHaveAge"),
				Values.iri(EX, "mustHaveMinThreeInterestsOrNoneAtAll")
		));

		loadShapes(repository);

		try {
			testCase.accept(repository);
		} catch (RepositoryException e) {
			ShaclSailValidationReportHelper.printValidationReport(e.getCause(), System.err);
			throw e;
		}

		shaclSail.shutDown();

	}

}