blob: 44e15e62cbce19e1272ff2e2a4dceb13c9cf20e4 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.rya.forwardchain.rule;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.log4j.Logger;
import org.apache.rya.api.RdfCloudTripleStoreConfiguration;
import org.apache.rya.api.domain.StatementMetadata;
import org.apache.rya.api.resolver.RdfToRyaConversions;
import org.apache.rya.forwardchain.ForwardChainConstants;
import org.apache.rya.forwardchain.ForwardChainException;
import org.apache.rya.forwardchain.strategy.AbstractRuleExecutionStrategy;
import org.apache.rya.sail.config.RyaSailFactory;
import org.openrdf.model.Literal;
import org.openrdf.model.Resource;
import org.openrdf.model.Value;
import org.openrdf.model.vocabulary.OWL;
import org.openrdf.model.vocabulary.RDF;
import org.openrdf.model.vocabulary.RDFS;
import org.openrdf.model.vocabulary.SP;
import org.openrdf.model.vocabulary.SPIN;
import org.openrdf.query.BindingSet;
import org.openrdf.query.MalformedQueryException;
import org.openrdf.query.QueryEvaluationException;
import org.openrdf.query.QueryLanguage;
import org.openrdf.query.TupleQuery;
import org.openrdf.query.TupleQueryResultHandlerBase;
import org.openrdf.query.TupleQueryResultHandlerException;
import org.openrdf.query.algebra.Extension;
import org.openrdf.query.algebra.Join;
import org.openrdf.query.algebra.QueryModelNode;
import org.openrdf.query.algebra.SingletonSet;
import org.openrdf.query.algebra.StatementPattern;
import org.openrdf.query.algebra.TupleExpr;
import org.openrdf.query.algebra.UnaryTupleOperator;
import org.openrdf.query.algebra.ValueExpr;
import org.openrdf.query.algebra.Var;
import org.openrdf.query.algebra.helpers.QueryModelVisitorBase;
import org.openrdf.query.parser.ParsedGraphQuery;
import org.openrdf.query.parser.ParsedQuery;
import org.openrdf.query.parser.sparql.SPARQLParser;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.sail.SailRepository;
import org.openrdf.repository.sail.SailRepositoryConnection;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
/**
* Represents a SPIN Construct rule extracted from the data store, providing
* access to its associated query tree and providing methods to apply the rule.
*/
public class SpinConstructRule extends AbstractConstructRule {
private static Logger logger = Logger.getLogger(SpinConstructRule.class);
private final Resource ruleId;
private final ParsedGraphQuery graphQuery;
private Set<StatementPattern> antecedentStatementPatterns = null;
private Set<StatementPattern> consequentStatementPatterns = null;
/**
* Instantiate a SPIN construct rule given its associated type, URI or bnode
* identifier, and construct query tree. Modifies the query tree to
* incorporate the fact that ?this must belong to the associated type, and
* traverses the modified tree to find antecedent and consequent triple
* patterns.
* @param type This rule applies to objects of this type. Should not be
* null. If the type is owl:Thing or rdfs:Resource, it will be applied to
* any objects. Otherwise, a statement pattern will be added that
* effectively binds ?this to members of the type. Therefore, passing
* owl:Thing or rdfs:Resource yields the intended behavior of
* sp:thisUnbound.
* @param ruleId The Resource representing this rule in the RDF data;
* should not be null.
* @param graphQuery The query tree corresponding to the "construct" text;
* should not be null.
*/
public SpinConstructRule(Resource type, Resource ruleId,
ParsedGraphQuery graphQuery) {
Preconditions.checkNotNull(type);
Preconditions.checkNotNull(ruleId);
Preconditions.checkNotNull(graphQuery);
this.ruleId = ruleId;
this.graphQuery = graphQuery;
// Add the type requirement: ?this must belong to the type
graphQuery.getTupleExpr().visit(new TypeRequirementVisitor("this", type));
// Find all statement patterns that could trigger this rule
AntecedentVisitor aVisitor = new AntecedentVisitor();
graphQuery.getTupleExpr().visit(aVisitor);
antecedentStatementPatterns = aVisitor.getAntecedents();
// Construct statement patterns for all possible conclusions of this rule
ConstructConsequentVisitor cVisitor = new ConstructConsequentVisitor();
graphQuery.getTupleExpr().visit(cVisitor);
consequentStatementPatterns = cVisitor.getConsequents();
}
/**
* Get the URI or bnode associated with this rule in the data.
* @return The rule's identifier.
*/
public Resource getId() {
return ruleId;
}
@Override
public String toString() {
return "SpinConstructRule{" + ruleId.stringValue() + "}";
}
@Override
public ParsedGraphQuery getQuery() {
return graphQuery;
}
@Override
public boolean canConclude(StatementPattern sp) {
Preconditions.checkNotNull(sp);
Value s1 = getVarValue(sp.getSubjectVar());
Value p1 = getVarValue(sp.getPredicateVar());
Value o1 = getVarValue(sp.getObjectVar());
Value c1 = getVarValue(sp.getContextVar());
for (StatementPattern consequent : consequentStatementPatterns) {
Value s2 = getVarValue(consequent.getSubjectVar());
Value p2 = getVarValue(consequent.getPredicateVar());
Value o2 = getVarValue(consequent.getObjectVar());
Value c2 = getVarValue(consequent.getContextVar());
if ((s1 == null || s2 == null || s1.equals(s2))
&& (p1 == null || p2 == null || p1.equals(p2))
&& (o1 == null || o2 == null || o1.equals(o2))
&& (c1 == null || c2 == null || c1.equals(c2))) {
return true;
}
}
return false;
}
@Override
public Collection<StatementPattern> getAntecedentPatterns() {
return antecedentStatementPatterns;
}
@Override
public Collection<StatementPattern> getConsequentPatterns() {
return consequentStatementPatterns;
}
@Override
public long execute(AbstractRuleExecutionStrategy strategy,
StatementMetadata metadata) throws ForwardChainException {
metadata.addMetadata(ForwardChainConstants.RYA_DERIVATION_RULE,
RdfToRyaConversions.convertResource(ruleId));
return super.execute(strategy, metadata);
}
private static Value getVarValue(Var var) {
return var == null ? null : var.getValue();
}
private static class TypeRequirementVisitor extends QueryModelVisitorBase<RuntimeException> {
private static final Var RDF_TYPE_VAR = new Var("-const-" + RDF.TYPE.stringValue(), RDF.TYPE);
private static final Set<Resource> BASE_TYPES = Sets.newHashSet(RDFS.RESOURCE, OWL.THING);
static {
RDF_TYPE_VAR.setConstant(true);
}
private final String varName;
private final StatementPattern typeRequirement;
public TypeRequirementVisitor(String varName, Resource requiredType) {
final Var typeVar = new Var("-const-" + requiredType.stringValue(), requiredType);
typeVar.setConstant(true);
this.varName = varName;
if (BASE_TYPES.contains(requiredType)) {
this.typeRequirement = null;
}
else {
this.typeRequirement = new StatementPattern(new Var(varName), RDF_TYPE_VAR, typeVar);
}
}
@Override
public void meet(SingletonSet node) {
if (typeRequirement != null) {
node.replaceWith(typeRequirement);
}
}
@Override
public void meet(Extension node) {
Set<String> argBindings = node.getArg().getBindingNames();
if (typeRequirement != null) {
node.getElements().removeIf(elem -> {
if (varName.equals(elem.getName())) {
ValueExpr expr = elem.getExpr();
if (expr == null) {
return true;
}
else if (expr instanceof Var) {
String fromName = ((Var) expr).getName();
if (getVarValue((Var) expr) == null && !argBindings.contains(fromName)) {
return true;
}
}
}
return false;
});
meetUnaryTupleOperator(node);
}
}
@Override
public void meetNode(QueryModelNode node) {
if (typeRequirement != null) {
if (node instanceof TupleExpr && ((TupleExpr) node).getBindingNames().contains(varName)) {
final Join withType = new Join((TupleExpr) node.clone(), typeRequirement);
node.replaceWith(withType);
}
else {
node.visitChildren(this);
}
}
}
@Override
public void meetUnaryTupleOperator(UnaryTupleOperator node) {
if (typeRequirement != null) {
if (node.getArg().getBindingNames().contains(varName)) {
node.visitChildren(this);
}
else {
meetNode(node);
}
}
}
}
/**
* Load a set of SPIN rules from a data store.
* @param conf Contains the connection information. Not null.
* @return A map of rule identifiers to rule objects.
* @throws ForwardChainException if connecting, querying for rules, or
* parsing rules fails.
*/
public static Ruleset loadSpinRules(RdfCloudTripleStoreConfiguration conf)
throws ForwardChainException {
Preconditions.checkNotNull(conf);
Map<Resource, Rule> rules = new ConcurrentHashMap<>();
// Connect to Rya
SailRepository repository = null;
SailRepositoryConnection conn = null;
try {
repository = new SailRepository(RyaSailFactory.getInstance(conf));
} catch (Exception e) {
throw new ForwardChainException("Couldn't initialize SAIL from configuration", e);
}
// Load and parse the individual SPIN rules from the data store
String ruleQueryString = "SELECT ?type ?rule ?text WHERE {\n"
+ " ?type <" + SPIN.RULE_PROPERTY.stringValue() + "> ?rule .\n"
+ " {\n"
+ " ?rule a <" + SP.CONSTRUCT_CLASS.stringValue() + "> .\n"
+ " ?rule <" + SP.TEXT_PROPERTY.stringValue() + "> ?text .\n"
+ " } UNION {\n"
+ " ?rule a ?template .\n"
+ " ?template <" + SPIN.BODY_PROPERTY + ">? ?body .\n"
+ " ?body a <" + SP.CONSTRUCT_CLASS.stringValue() + "> .\n"
+ " ?body <" + SP.TEXT_PROPERTY.stringValue() + "> ?text .\n"
+ " }\n"
+ "}";
SPARQLParser parser = new SPARQLParser();
try {
conn = repository.getConnection();
TupleQuery ruleQuery = conn.prepareTupleQuery(QueryLanguage.SPARQL, ruleQueryString);
ruleQuery.evaluate(new TupleQueryResultHandlerBase() {
@Override
public void handleSolution(BindingSet bs) throws TupleQueryResultHandlerException {
// For each rule identifier found, instantiate a SpinRule
Value requiredType = bs.getValue("type");
Value ruleIdentifier = bs.getValue("rule");
Value ruleText = bs.getValue("text");
if (requiredType instanceof Resource
&& ruleIdentifier instanceof Resource
&& ruleText instanceof Literal) {
ParsedQuery parsedRule;
try {
parsedRule = parser.parseQuery(ruleText.stringValue(), null);
if (parsedRule instanceof ParsedGraphQuery) {
SpinConstructRule rule = new SpinConstructRule(
(Resource) requiredType,
(Resource) ruleIdentifier,
(ParsedGraphQuery) parsedRule);
if (rule.hasAnonymousConsequent()) {
logger.error("Skipping unsupported rule " + ruleIdentifier
+ " -- consequent refers to bnode, which is not"
+ " currently supported (creating new bnodes at each"
+ " application could lead to infinite recursion).");
}
else {
rules.put((Resource) ruleIdentifier, rule);
}
}
} catch (Exception e) {
throw new TupleQueryResultHandlerException(e);
}
}
}
});
} catch (TupleQueryResultHandlerException | QueryEvaluationException
| MalformedQueryException | RepositoryException e) {
throw new ForwardChainException("Couldn't retrieve SPIN rules", e);
}
finally {
if (conn != null) {
try {
conn.close();
} catch (RepositoryException e) {
logger.warn("Error closing repository connection", e);
}
}
if (repository.isInitialized()) {
try {
repository.shutDown();
} catch (RepositoryException e) {
logger.warn("Error shutting down repository", e);
}
}
}
return new Ruleset(rules.values());
}
}