PathTest.java

/*******************************************************************************
 * Copyright (c) 2021 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.paths;

import java.io.IOException;
import java.io.StringReader;
import java.util.HashSet;

import org.eclipse.rdf4j.common.transaction.IsolationLevels;
import org.eclipse.rdf4j.model.Model;
import org.eclipse.rdf4j.model.Resource;
import org.eclipse.rdf4j.model.impl.DynamicModel;
import org.eclipse.rdf4j.model.impl.DynamicModelFactory;
import org.eclipse.rdf4j.model.util.Models;
import org.eclipse.rdf4j.model.vocabulary.SHACL;
import org.eclipse.rdf4j.repository.sail.SailRepository;
import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection;
import org.eclipse.rdf4j.rio.RDFFormat;
import org.eclipse.rdf4j.rio.Rio;
import org.eclipse.rdf4j.rio.WriterConfig;
import org.eclipse.rdf4j.rio.helpers.BasicWriterSettings;
import org.eclipse.rdf4j.sail.memory.MemoryStore;
import org.eclipse.rdf4j.sail.shacl.wrapper.shape.RepositoryConnectionShapeSource;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class PathTest {

	@Test
	public void simplePath() throws IOException {

		String pathString = "ex:1 sh:path ex:hasChild.";

		testPath(pathString);

	}

	@Test
	public void inversePath() throws IOException {

		String pathString = "ex:1 sh:path [sh:inversePath ex:childOf].";

		testPath(pathString);

	}

	@Test
	public void alternativePath() throws IOException {

		String pathString = "ex:1 sh:path [sh:alternativePath (ex:hasChild ex:child)].";

		testPath(pathString);

	}

	@Test
	public void sequencePath() throws IOException {

		String pathString = "ex:1 sh:path (ex:hasChild ex:hasChild) .";

		testPath(pathString);

	}

	@Test
	public void nestedSequencePath() throws IOException {

		String pathString = "ex:1 sh:path (ex:hasChild (ex:hasChild (((ex:hasChild ex:hasChild) (ex:hasChild ex:hasChild)) ex:hasChild))) .";

		testPath(pathString);

	}

	@Test
	public void zeroOrMorePath() throws IOException {

		String pathString = "ex:1 sh:path [sh:zeroOrMorePath ex:hasChild].";

		testPath(pathString);

	}

	@Test
	public void oneOrMorePath() throws IOException {

		String pathString = "ex:1 sh:path [sh:oneOrMorePath ex:hasChild].";

		testPath(pathString);

	}

	@Test
	public void zeroOrOnePath() throws IOException {

		String pathString = "ex:1 sh:path [sh:zeroOrOnePath ex:hasChild].";

		testPath(pathString);

	}

	@Test
	public void combination() throws IOException {

		String pathString = "ex:1 sh:path (ex:hasChild [sh:inversePath ex:childOf] [sh:zeroOrMorePath ex:hasChild]).";

		testPath(pathString);

	}

	@Test
	public void combination2() throws IOException {

		String pathString = "ex:1 sh:path [sh:inversePath (sh:childOf sh:childOf)].";

		testPath(pathString);

	}

	@Test
	public void combination3() throws IOException {

		String pathString = "ex:1 sh:path [sh:zeroOrMorePath [sh:inversePath ex:childOf]].";

		testPath(pathString);

	}

	private void testPath(String pathString) throws IOException {
		Model expected = Rio.parse(new StringReader("" +
				"@prefix sh: <http://www.w3.org/ns/shacl#>.\n" +
				"@prefix ex: <http://example.org/>.\n" +
				pathString), "", RDFFormat.TRIG);

		DynamicModel actual = convertToPathAndBackToModel(expected);

		if (!Models.isomorphic(expected, actual)) {

			WriterConfig writerConfig = new WriterConfig();
			writerConfig.set(BasicWriterSettings.PRETTY_PRINT, true);
			writerConfig.set(BasicWriterSettings.INLINE_BLANK_NODES, true);

			expected.setNamespace(SHACL.PREFIX, SHACL.NAMESPACE);
			expected.setNamespace("ex", "http://example.org/");

			actual.setNamespace(SHACL.PREFIX, SHACL.NAMESPACE);
			actual.setNamespace("ex", "http://example.org/");

			System.out.println("Expected:");
			Rio.write(expected, System.out, RDFFormat.TRIG, writerConfig);

			System.out.println("Actual:");
			Rio.write(actual, System.out, RDFFormat.TRIG, writerConfig);

		}

		Assertions.assertTrue(Models.isomorphic(actual, expected));
	}

	private DynamicModel convertToPathAndBackToModel(Model expected) {
		Resource[] defaultContext = { null };

		DynamicModel actual;
		SailRepository sailRepository = new SailRepository(new MemoryStore());
		try (SailRepositoryConnection connection = sailRepository.getConnection()) {
			connection.begin(IsolationLevels.NONE);
			connection.add(expected);

			actual = connection.getStatements(null, SHACL.PATH, null)
					.stream()
					.map(s -> {
						try (RepositoryConnectionShapeSource shapeSource = new RepositoryConnectionShapeSource(
								connection).withContext(defaultContext)) {
							Path path = Path.buildPath(shapeSource, (Resource) s.getObject());

							DynamicModel model = new DynamicModelFactory().createEmptyModel();
							path.toModel((Resource) s.getObject(), null, model, new HashSet<>());

							model.add(s.getSubject(), SHACL.PATH, s.getObject());

							return model;
						}
					})
					.reduce((m1, m2) -> {
						m1.addAll(m2);
						return m1;
					})
					.orElse(new DynamicModelFactory().createEmptyModel());
			connection.commit();
		} finally {
			sailRepository.shutDown();
		}

		return actual;
	}
}