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.
*/
import java.io.StringWriter;
import java.util.HashMap;
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;
return version.getModel();
public final void checkClause() throws SAREFPipelineException {
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());
reportModel.write(sw, "TTL");
LOG.trace("Report model is " + sw.toString());
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);) {
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);
Set<RDFNode> values = valuesMap.get(focusNode);
if (values == null) {
values = new HashSet<>();
valuesMap.put(focusNode, values);
}
if (value != null && !value.equals(focusNode)) {
report(severity, resultMessage, valuesMap);
previousResultMessage = resultMessage;
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()) {
for (RDFNode value : values) {
sw.append("`").append(FmtUtils.stringForRDFNode(value)).append("`").append(" ; ");
}
}
sw.append("\n");
}
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)) {
} 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) {
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);
}