/* * Copyright 2008 Fedora Commons, Inc. * * Licensed 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.mulgara.krule.rlog.ast; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.mulgara.krule.rlog.ParseContext; import org.mulgara.krule.rlog.parser.TypeException; import org.mulgara.krule.rlog.parser.URIParseException; import org.mulgara.krule.rlog.rdf.RDFNode; import org.mulgara.krule.rlog.rdf.URIReference; import org.mulgara.krule.rlog.rdf.Var; import org.mulgara.util.functional.C; /** * Represents a rule statement. * * @created May 16, 2008 * @author Paula Gearon * @copyright © 2008 <a href="http://www.fedora-commons.org/">Fedora Commons</a> */ public class Rule extends Statement { /** The predicate found in the head of the rule. */ public final Predicate head; /** The list of predicates found in the body of the rule. */ public final List<Predicate> body; /** The list of predicates to be subtracted from the body of the rule. */ public final List<Predicate> bodySubtractions; /** The collection of rules that this rule can trigger for execution. */ public final Collection<Rule> triggers; /** The name of this rule. Used in RDF. */ private String name; protected Rule(List<Predicate> body, ParseContext context) { this(NullPredicate.NULL, body, context); } /** * This constructor takes a head and body for a statement. It normalizes the structure, * and checks for soundness. * @param head A Predicate containing a subset of variables from the body. * @param body A set of Predicates for the body. * @param context The context passed in from the parser. */ public Rule(Predicate head, List<Predicate> body, ParseContext context) { super(context); this.head = head; this.body = body; this.bodySubtractions = new LinkedList<Predicate>(); this.head.setParent(this); for (Predicate p: this.body) p.setParent(this); // shuffle the inversions out of the body and into the body subtractions normalizeBody(this.body, this.bodySubtractions); triggers = new HashSet<Rule>(); try { checkVariables(); } catch (IllegalArgumentException e) { System.err.println("head = '" + head + "'"); System.err.println("body = '" + this.body + "'"); throw e; } } /** * Get all variables referred to by this rule. * @return All variables from the body, since the variables in the head must be a subset. */ public Collection<Var> getVariables() { Collection<Var> vars = new HashSet<Var>(); for (Predicate p: body) vars.addAll(p.getVariables()); for (Predicate p: bodySubtractions) vars.addAll(p.getVariables()); return vars; } /** * Adds a rule into the set of rules to be triggered by this rule. * @param target The rule to be triggered. */ public void addTrigger(Rule target) { triggers.add(target); } /** * Sets the name for this rule. * @param name The new name for the rule. */ public void setName(String name) { this.name = name; } /** * @return The name of this rule. */ public String getName() { return name; } // inheritdoc public void accept(TreeWalker walker) { walker.visit(this); } // inheritdoc public void print(int indent) { System.out.println(sp(indent) + "Rule {"); System.out.println(sp(indent + 1) + "head:"); head.print(indent + 2); System.out.println(sp(indent + 1) + "body [length=" + body.size() + "]:"); for (Predicate p: body) p.print(indent + 2); if (!bodySubtractions.isEmpty()) { System.out.print(sp(indent + 1) + "minus [length=" + bodySubtractions.size() + "]:"); for (Predicate p: bodySubtractions) p.print(indent + 2); } System.out.println(sp(indent) + "}"); } /** {@inheritDoc} */ public boolean equals(Object o) { if (!(o instanceof Rule)) return false; Rule r = (Rule)o; return head.equals(r.head) && body.equals(r.body); } /** {@inheritDoc} */ public int hashCode() { return head.hashCode() * 31 + body.hashCode(); } /** * Tests if this rule is triggered by the given rule. * @param trigger The potential triggering rule. * @return <code>false</code> if this rule <em>cannot</em> be triggered by the trigger. * @throws TypeException If the product of the rule is an illegal type. * @throws URIParseException If the predicates in the rule result in illegal URIs. */ public boolean triggeredBy(Rule trigger) throws TypeException, URIParseException { Predicate triggerProduct = trigger.head; if (triggerProduct instanceof InvertedPredicate) throw new TypeException("Unexpected inversion in a rule head."); if (triggerProduct == NullPredicate.NULL) return false; RDFNode subject = triggerProduct.getSubject(); RDFNode predicate = triggerProduct.getPredicate(); RDFNode object = triggerProduct.getObject(); for (Predicate p: body) if (p.matches(subject, predicate, object)) return true; return false; } /** * Get all the URI references used in this rule. * @return All the references used in this rule. */ public Set<URIReference> getReferences() throws URIParseException { Set<URIReference> refs = new LinkedHashSet<URIReference>(); refs.addAll(head.getReferences()); for (Predicate p: body) refs.addAll(p.getReferences()); return refs; } /** * @return the head */ public Predicate getHead() { return head; } /** * @return the body */ public List<Predicate> getBody() { return Collections.unmodifiableList(body); } /** * @return the bodySubtractions */ public List<Predicate> getBodySubtractions() { return Collections.unmodifiableList(bodySubtractions); } /** * @return the triggers */ public Collection<Rule> getTriggers() { return triggers; } @Override public CanonicalStatement getCanonical() { List<CanonicalPredicate> list = new ArrayList<CanonicalPredicate>(body.size() + 1); // reorder the predicates for (Predicate p: body) C.ascendingInsert(list, p.getCanonical()); return new CanonicalStatement(head.getCanonical(), list); } /** @see java.lang.Object#toString() */ public String toString() { StringBuilder sb = new StringBuilder(head.toString()); sb.append(" :- "); for (int b = 0; b < body.size(); b++) { if (b != 0) sb.append(", "); sb.append(body.get(b)); } sb.append("."); return sb.toString(); } /** * Checks that all variables in the head are found in the body, and that every subtracted predicate * contains at least one variable that appears in the standard matching predicates. * @throws IllegalArgumentException if there are any variables unaccounted for. */ @SuppressWarnings("unchecked") private void checkVariables() { Collection<Var> headVars = head.getVariables(); HashSet<Var> bodyVars = new HashSet<Var>(); for (Predicate p: body) bodyVars.addAll(p.getVariables()); headVars.removeAll(bodyVars); if (!headVars.isEmpty()) throw new IllegalArgumentException("Head of rule must contain variables from the body."); for (Predicate p: bodySubtractions) { HashSet<Var> vars = (HashSet<Var>)bodyVars.clone(); vars.retainAll(p.getVariables()); if (vars.isEmpty()) throw new IllegalArgumentException("Subtractions must match at least one variable in the original predicates."); } } /** * Remove all subtractions from the body, and place them in the bodySubtractions. */ private void normalizeBody(List<Predicate> bdy, List<Predicate> subs) { Iterator<Predicate> preds = bdy.iterator(); boolean changes = false; while (preds.hasNext()) { Predicate current = preds.next(); if (current instanceof InvertedPredicate) { subs.add(((InvertedPredicate)current).getInvertPredicate()); preds.remove(); changes = true; } } // normalize for any nested inversions if (changes) normalizeBody(subs, bdy); } }