AbstractShaclChecker.java 8.07 KB
Newer Older
/*
 * Copyright 2020 ETSI
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holder nor the names of its contributors 
 *    may be used to endorse or promote products derived from this software without 
 *    specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
 * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package fr.mines_stetienne.ci.saref.checkers;

import java.io.InputStream;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.jena.query.QueryExecution;
import org.apache.jena.query.QueryExecutionFactory;
import org.apache.jena.query.QuerySolution;
import org.apache.jena.query.ResultSet;
import org.apache.jena.query.ResultSetFormatter;
import org.apache.jena.rdf.model.Literal;
import org.apache.jena.rdf.model.Model;
import org.apache.jena.rdf.model.RDFNode;
import org.apache.jena.rdf.model.Resource;
import org.apache.jena.rdf.model.ResourceFactory;
import org.apache.jena.sparql.util.FmtUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.topbraid.jenax.util.JenaUtil;
import org.topbraid.shacl.validation.ValidationUtil;
import org.topbraid.shacl.vocabulary.SH;

import fr.mines_stetienne.ci.saref.SAREF;
import fr.mines_stetienne.ci.saref.SAREFPipelineException;
import fr.mines_stetienne.ci.saref.managers.RepositoryManager;
import fr.mines_stetienne.ci.saref.vocabs.SHACL;

public abstract class AbstractShaclChecker extends AbstractClauseChecker {

	private static final Logger LOG = LoggerFactory.getLogger(AbstractShaclChecker.class);
	public static final String SHAPE_VAR = "shape";
	public static final Pattern SHAPE_PATTERN = Pattern.compile("^(?<shape>Clause((_[0-9]+)+))_Checker$");
	private static final String NS = "https://saref.etsi.org/shape#";

	protected static interface MessageResource {
		default Resource asResource() {
			return ResourceFactory.createResource(NS + toString());
		}
	}

	protected static final String SELECT_VIOLATION = "PREFIX sh: <http://www.w3.org/ns/shacl#>\n"
			+ "SELECT ?sourceShape ?severity ?resultMessage ?focusNode ?value \n" + "WHERE {   \n"
			+ "    ?violation sh:resultSeverity ?severity ; sh:resultMessage ?resultMessage ; sh:focusNode ?focusNode .\n"
			+ "      OPTIONAL { ?violation sh:value ?value . } \n"
			+ "      OPTIONAL { ?violation sh:sourceShape ?sourceShape . } \n" + "}"
			+ "ORDER BY ?severity ?resultMessage ?focusNode ";

	protected final Model shapeModel;

	public AbstractShaclChecker(RepositoryManager repositoryManager, Class<?> clazz, String name)
			throws SAREFPipelineException {
		super(repositoryManager, clazz, name);
		shapeModel = JenaUtil.createDefaultModel();

		final Matcher matcher = SHAPE_PATTERN.matcher(clazz.getSimpleName());
		if (!matcher.find()) {
			throw new IllegalArgumentException();
		}
		final String shape = String.format("shapes/%s.ttl", matcher.group(SHAPE_VAR));
		try (InputStream in = AbstractShaclChecker.class.getClassLoader().getResourceAsStream(shape)) {
			shapeModel.read(in, SAREF.BASE, "TTL");
		} catch (Exception ex) {
			throw new SAREFPipelineException("Exception while reading the shape file", ex);
		}
	}

	public AbstractShaclChecker(RepositoryManager repositoryManager, Class<?> clazz) throws SAREFPipelineException {
		this(repositoryManager, clazz, null);
	}

	protected abstract void updateShapeModel() throws SAREFPipelineException;
	
	protected Model getModel() {
	@Override
	public final void checkClause() throws SAREFPipelineException {
		updateShapeModel();
		Model model = getModel();
		Resource reportResource = ValidationUtil.validateModel(model, shapeModel, true);
		boolean conforms = reportResource.getProperty(SH.conforms).getBoolean();
		Model reportModel = reportResource.getModel();

		if (LOG.isTraceEnabled()) {
			StringWriter sw;
			sw = new StringWriter();
			shapeModel.write(sw, "TTL");
			LOG.trace("SHACL is " + sw.toString());
			sw = new StringWriter();
			reportModel.write(sw, "TTL");
			LOG.trace("Report model is " + sw.toString());
		if (!conforms) {
			if (LOG.isTraceEnabled()) {
				try (QueryExecution exec = QueryExecutionFactory.create(SELECT_VIOLATION, reportModel);) {
					LOG.trace(ResultSetFormatter.asText(exec.execSelect()));
				}
			}
			try (QueryExecution exec = QueryExecutionFactory.create(SELECT_VIOLATION, reportModel);) {
				Literal previousResultMessage = null;
				Resource previousResultSeverity = null;
				Map<Resource, Set<RDFNode>> valuesMap = new HashMap<>();

				for (ResultSet resultSet = exec.execSelect(); resultSet.hasNext();) {
					QuerySolution sol = resultSet.next();
					Resource severity = sol.getResource("severity");
					Literal resultMessage = sol.getLiteral("resultMessage");
					Resource focusNode = sol.get("focusNode").asResource();
					RDFNode value = sol.get("value");
					if (previousResultMessage != null && !resultMessage.equals(previousResultMessage)) {
						report(previousResultSeverity, previousResultMessage, valuesMap);
						valuesMap = new HashMap<>();
					Set<RDFNode> values = valuesMap.get(focusNode);
					if (values == null) {
						values = new HashSet<>();
						valuesMap.put(focusNode, values);
					}
					if (value != null && !value.equals(focusNode)) {
						values.add(value);
					}
					if (!resultSet.hasNext()) {
						report(severity, resultMessage, valuesMap);
					previousResultMessage = resultMessage;
					previousResultSeverity = severity;
	private void report(Resource severity, Literal resultMessage, Map<Resource, Set<RDFNode>> valuesMap) {
		StringWriter sw = new StringWriter();
		for (Resource resource : valuesMap.keySet()) {
			sw.append("- ").append("`").append(FmtUtils.stringForRDFNode(resource)).append("`");
			Set<RDFNode> values = valuesMap.get(resource);
			if (!values.isEmpty()) {
				sw.append(" got: ");
				for (RDFNode value : values) {
					sw.append("`").append(FmtUtils.stringForRDFNode(value)).append("`").append(" ; ");
		String message = String.format("%s\n\n%s\n\n", resultMessage.getString(), sw.toString());
		if (severity.equals(SHACL.Violation)) {
			errorLogger.error(message);
		} else if (severity.equals(SHACL.Warning)) {
			errorLogger.warn(message);
		} else {
			errorLogger.info(message);
	protected void remove(MessageResource message) {
		shapeModel.removeAll(message.asResource(), null, null);
		shapeModel.removeAll(null, null, message.asResource());
	}

	protected void add(MessageResource message, Object... args) {
		add(null, message, args);
	protected void add(String pattern, MessageResource message, Object... args) {
		Resource r = message.asResource();
		if (pattern != null) {
			shapeModel.add(r, SHACL.pattern, pattern);
		}
		shapeModel.add(r, SHACL.message, getMessage(message, args));
	}

	protected String exactly(String string) {
		return String.format("^%s$", string);
	}