UnknownShapesTest.java

/*******************************************************************************
 * Copyright (c) 2019 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.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import org.eclipse.rdf4j.common.exception.ValidationException;
import org.eclipse.rdf4j.model.util.Values;
import org.eclipse.rdf4j.model.vocabulary.RDF;
import org.eclipse.rdf4j.model.vocabulary.RDFS;
import org.eclipse.rdf4j.repository.RepositoryException;
import org.eclipse.rdf4j.repository.sail.SailRepository;
import org.eclipse.rdf4j.repository.sail.SailRepositoryConnection;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.api.parallel.Isolated;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;

@Isolated
public class UnknownShapesTest {

	private TestAppender appender;

	@BeforeEach
	void addAppender() {
		appender = new TestAppender();

		Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
		root.addAppender(appender);
	}

	@AfterEach
	void detachAppender() {
		Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
		root.detachAppender(appender);
	}

	@Test
	@Timeout(5)
	public void testPropertyShapes() throws IOException {
		SailRepository shaclRepository = Utils.getInitializedShaclRepository("unknownProperties.trig");

		try (SailRepositoryConnection connection = shaclRepository.getConnection()) {
			connection.begin();
			connection.add(RDF.TYPE, RDF.TYPE, RDFS.RESOURCE);
			connection.commit();
		}

		Set<String> relevantLog = getRelevantLog(2);

		Set<String> expected = Set.of(
				"Unsupported SHACL feature detected sh:unknownShaclProperty in statement (http://example.com/ns#PersonPropertyShape, http://www.w3.org/ns/shacl#unknownShaclProperty, \"1\"^^<http://www.w3.org/2001/XMLSchema#integer>) [null]",
				"Unsupported SHACL feature detected sh:unknownTarget in statement (http://example.com/ns#PersonShape, http://www.w3.org/ns/shacl#unknownTarget, http://www.w3.org/2000/01/rdf-schema#Class) [null]"
		);

		Assertions.assertEquals(expected, relevantLog);

		shaclRepository.shutDown();

	}

	private Set<String> getRelevantLog(int expectedNumberOfItems) {
		Set<String> relevantLog;
		do {
			relevantLog = appender.logged.stream()
					.filter(m -> m.startsWith("Unsupported SHACL feature"))
					.map(s -> s.replaceAll("\r\n|\r|\n", " "))
					.map(String::trim)
					.collect(Collectors.toSet());
		} while (relevantLog.size() < expectedNumberOfItems);
		assert relevantLog.size() == expectedNumberOfItems;
		return relevantLog;
	}

	@Test
	@Timeout(5)
	public void testComplexPath() throws IOException {
		SailRepository shaclRepository = Utils.getInitializedShaclRepository("complexPath.trig");

		try (SailRepositoryConnection connection = shaclRepository.getConnection()) {
			connection.begin();
			connection.add(RDF.TYPE, RDF.TYPE, RDFS.RESOURCE);
			connection.commit();
		} catch (RepositoryException e) {
			if (!(e.getCause() instanceof ValidationException)) {
				throw e;
			}
		}

		Set<String> relevantLog = getRelevantLog(5).stream()
				.sorted()
				.collect(Collectors.toCollection(LinkedHashSet::new));

		Set<String> expected = Set.of(
				"Unsupported SHACL feature detected: AlternativePath{ [SimplePath{ <http://example.com/ns#father> }, OneOrMorePath{ SimplePath{ <http://example.com/ns#parent> } }, SimplePath{ <http://example.com/ns#mother> }] }. Shape ignored! <http://example.com/ns#alternativePathOneOrMore> a sh:PropertyShape;   sh:path [       sh:alternativePath (<http://example.com/ns#father> [             sh:oneOrMorePath <http://example.com/ns#parent>           ] <http://example.com/ns#mother>)     ];   sh:nodeKind sh:BlankNodeOrIRI .",
				"Unsupported SHACL feature detected: AlternativePath{ [SimplePath{ <http://example.com/ns#father> }, ZeroOrMorePath{ SimplePath{ <http://example.com/ns#parent> } }, SimplePath{ <http://example.com/ns#mother> }] }. Shape ignored! <http://example.com/ns#alternativePathZeroOrMore> a sh:PropertyShape;   sh:path [       sh:alternativePath (<http://example.com/ns#father> [             sh:zeroOrMorePath <http://example.com/ns#parent>           ] <http://example.com/ns#mother>)     ];   sh:nodeKind sh:BlankNodeOrIRI .",
				"Unsupported SHACL feature detected: AlternativePath{ [SimplePath{ <http://example.com/ns#father> }, ZeroOrOnePath{ SimplePath{ <http://example.com/ns#parent> } }, SimplePath{ <http://example.com/ns#mother> }] }. Shape ignored! <http://example.com/ns#alternativePathZeroOrOne> a sh:PropertyShape;   sh:path [       sh:alternativePath (<http://example.com/ns#father> [             sh:zeroOrOnePath <http://example.com/ns#parent>           ] <http://example.com/ns#mother>)     ];   sh:nodeKind sh:BlankNodeOrIRI .",
				"Unsupported SHACL feature detected: InversePath{ ZeroOrMorePath{ SimplePath{ <http://example.com/ns#inverseThis> } } }. Shape ignored! <http://example.com/ns#inverseOfWithComplex> a sh:PropertyShape;   sh:path [       sh:inversePath [           sh:zeroOrMorePath <http://example.com/ns#inverseThis>         ]     ];   sh:datatype xsd:int .",
				"Unsupported SHACL feature detected: InversePath{ ZeroOrMorePath{ SimplePath{ <http://example.com/ns#inverseThis> } } }. Shape ignored! <http://example.com/ns#inverseOfWithComplex> a sh:PropertyShape;   sh:path [       sh:inversePath [           sh:zeroOrMorePath <http://example.com/ns#inverseThis>         ]     ];   sh:minCount 1 ."
		);

		expected = expected.stream().sorted().collect(Collectors.toCollection(LinkedHashSet::new));

		Assertions.assertEquals(expected, relevantLog);

		shaclRepository.shutDown();
	}

	@Test
	public void testThatUnknownPathsAreIgnored() throws IOException {
		SailRepository shaclRepository = Utils.getInitializedShaclRepository("complexPath.trig");

		// trigger SPARQL based validation
		try (SailRepositoryConnection connection = shaclRepository.getConnection()) {
			connection.begin();
			connection.add(RDF.TYPE, RDF.TYPE, RDFS.RESOURCE);
			connection.commit();
		} catch (RepositoryException e) {
			if (!(e.getCause() instanceof ValidationException)) {
				throw e;
			}
		}

		try (SailRepositoryConnection connection = shaclRepository.getConnection()) {
			connection.add(Values.bnode(), RDF.TYPE, RDFS.CLASS);
		}

		// trigger transactional validation
		try (SailRepositoryConnection connection = shaclRepository.getConnection()) {
			connection.begin();
			connection.add(RDFS.RESOURCE, RDF.TYPE, RDFS.RESOURCE);
			connection.commit();
		} catch (RepositoryException e) {
			if (!(e.getCause() instanceof ValidationException)) {
				throw e;
			}
		}

		// trigger requiresEvaluation(...) check
		try (SailRepositoryConnection connection = shaclRepository.getConnection()) {
			connection.begin();
			connection.add(RDFS.RESOURCE, Values.iri("http://example.com/ns#parent"), RDFS.RESOURCE);
			connection.commit();
		}

		shaclRepository.shutDown();
	}

	private static class TestAppender extends AppenderBase<ILoggingEvent> {

		private final Set<String> logged = ConcurrentHashMap.newKeySet();

		@Override
		public void doAppend(ILoggingEvent eventObject) {
			logged.add(eventObject.getFormattedMessage());
		}

		@Override
		protected void append(ILoggingEvent iLoggingEvent) {

		}
	}
}