/* * 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; // Java 2 standard packages import static org.mulgara.krule.RuleStructure.UNINITIALIZED; import java.io.Serializable; import java.math.BigInteger; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.apache.log4j.Logger; import org.mulgara.query.Answer; import org.mulgara.query.AnswerImpl; import org.mulgara.query.Cursor; import org.mulgara.query.Query; import org.mulgara.query.QueryException; import org.mulgara.query.TuplesException; import org.mulgara.query.Variable; import org.mulgara.resolver.OperationContext; import org.mulgara.resolver.spi.LocalizedTuples; import org.mulgara.resolver.spi.Resolver; import org.mulgara.resolver.spi.ResolverException; import org.mulgara.resolver.spi.Statements; import org.mulgara.resolver.spi.SystemResolver; import org.mulgara.store.tuples.Tuples; /** * Represents a single executable rule. * * @created 2005-5-16 * @author <a href="mailto:pgearon@users.sourceforge.net">Paula Gearon</a> * @version $Revision: 1.2 $ * @modified $Date: 2005/06/30 01:12:28 $ * @maintenanceAuthor $Author: pgearon $ * @copyright © 2005 <a href="http://www.fedora-commons.org/">Fedora Commons</a> */ public class Rule implements Serializable { private static final long serialVersionUID = 3080424724378196982L; /** Logger. */ private static final Logger logger = Logger.getLogger(Rule.class.getName()); /** The name of this rule. */ protected String name; /** The rules to be triggered when this rule generates statements.*/ private Set<Rule> triggerSet; /** The query for this rule. This contains the information for the base model. */ protected Query query; /** The graph receiving the inferred data. */ private long targetGraph = UNINITIALIZED; /** The most recent size of the data matching this rule. */ private long lastCount; /** The structure containing this rule */ protected RuleStructure ruleStruct; // TODO: Change this to a map of constraints to longs /** * Principle constructor. * * @param name The name of the rule. */ public Rule(String name) { triggerSet = new HashSet<Rule>(); this.name = name; } /** * Gets the name of the rule. * * @return The rule name. */ public String getName() { return name; } /** * Adds a target for triggering. * * @param target The rule to be triggered when this rule is executed. */ public void addTriggerTarget(Rule target) { triggerSet.add(target); } /** * Set the target graph for the rule. * * @param target The URI of the model to insert inferences into. */ public void setTargetGraph(long target) { targetGraph = target; } /** * Sets the query for this rule. Must be a valid query for inserting data, meaning that it * returns a multiple of 3 elements. * * @param queryStruct The query which retrieves data for this rule. * @throws KruleStructureException If the query does not return a multiple of 3 elements. */ public void setQueryStruct(QueryStruct queryStruct) throws KruleStructureException { int e = queryStruct.elementCount(); if (e == 0 || e % 3 != 0) throw new KruleStructureException("Rule \"" + name + "\" attempting to generate the wrong number of elements (must be a multiple of 3): " + e); this.query = queryStruct.extractQuery(); } /** * Retrieves the query from this rule. * * @return The query which retrieves data for this rule. */ public Query getQuery() { return query; } /** * Retrieves the list of subordinate rules. * * @return an immutable set of the subordinate rules. */ public Set<Rule> getTriggerTargets() { return Collections.unmodifiableSet(triggerSet); } /** * Sets the rule system structure containing this rule. * * @param ruleStruct The structure for this rule. */ public void setRuleStruct(RuleStructure ruleStruct) { this.ruleStruct = ruleStruct; } /** * Runs this rule. * TODO: count the size of each individual constraint * TODO: Go back to using a Session once they have been properly refactored for transactions * * @param context The context to query against. * @param resolver The resolver to add data with. * @param sysResolver The resolver to localize data with. */ public void execute(OperationContext context, Resolver resolver, SystemResolver sysResolver) throws QueryException, TuplesException, ResolverException { if (targetGraph == UNINITIALIZED) throw new IllegalStateException("Target graph has not been set"); // see if this rule needs to be run Answer answer = null; try { answer = context.doQuery(query); } catch (Exception e) { throw new QueryException("Unable to access data in rule.", e); } try { // compare the size of the result data long newCount = answer.getRowCount(); if (newCount == lastCount) { logger.debug("Rule <" + name + "> is up to date."); // this rule does not need to be run return; } logger.debug("Rule <" + name + "> has increased by " + (newCount - lastCount) + " entries"); logger.debug("Inserting results of: " + query); if (answer instanceof AnswerImpl) { AnswerImpl a = (AnswerImpl)answer; String list = "[ "; Variable[] v = a.getVariables(); for (int i = 0; i < v.length; i++) { list += v[i] + " "; } list += "]"; logger.debug("query has " + a.getNumberOfVariables() + " variables: " + list); } // insert the resulting data insertData(answer, resolver, sysResolver); // update the count lastCount = newCount; logger.debug("Insertion complete, triggering rules for scheduling."); } finally { answer.close(); } // trigger subsequent rules scheduleTriggeredRules(); } /** * Schedule subsequent rules. */ private void scheduleTriggeredRules() { Iterator<Rule> it = triggerSet.iterator(); while (it.hasNext()) ruleStruct.schedule(it.next()); } /** * Inserts an Answer into the data store on the current transaction. * @param answer The data to be inserted. * @param resolver The mechanism for adding data in the current transaction. * @param sysResolver Used for localizing the globalized data in the answer parameter. * TODO: use a localized Tuples instead of Answer when src and dest are on the same server. * @throws TuplesException There was an error localizing the answer. * @throws ResolverException There was an error inserting the data. */ private void insertData(Answer answer, Resolver resolver, SystemResolver sysResolver) throws TuplesException, ResolverException { Statements statements = convertToStatements(answer, sysResolver); try { resolver.modifyModel(targetGraph, statements, true); } finally { statements.close(); } } /** * Converts an Answer with a multiple of 3 selection values to a set of statements for insertion. * @param answer The answer to convert. * @param resolver The resolver used for localizing the results, since Answers are globalized. * TODO: remove this round trip of local->global->local. * @return A set of Statements. * @throws TuplesException The statements could not be instantiated. */ private Statements convertToStatements(Answer answer, SystemResolver resolver) throws TuplesException { assert answer.getVariables().length % 3 == 0; return new TuplesStatements(new LocalizedTuples(resolver, answer, true)); } /** * Wrapper for converting a Tuples to a Statements object. Unlike the * TuplesWrapperStatements class, this class handles Tuples whose row length is * an arbitrary multiple of 3. Subject, predicate, and object are determined * by column index in the Tuples, not by a variable mapping as is the case * in TuplesWrapperStatements. */ protected static class TuplesStatements implements Statements, Cloneable { private static List<Variable> variables = Arrays.asList(Statements.SUBJECT, Statements.PREDICATE, Statements.OBJECT); private static int statementSize = variables.size(); private int wrappedRowLength; private int statementsPerRow; private int offsetInRow = 0; private Tuples tuples; /** * Construct a wrapper around the tuples. * @param tuples The Tuples to convert to statements; the number of columns * must be a multiple of three. */ public TuplesStatements(Tuples tuples) { this.tuples = tuples; if (tuples.getNumberOfVariables() % 3 != 0) throw new IllegalArgumentException("Number of variables must be a multiple of 3"); this.wrappedRowLength = tuples.getNumberOfVariables(); this.statementsPerRow = wrappedRowLength / statementSize; } public long getObject() throws TuplesException { return tuples.getColumnValue(offsetInRow + 2); } public long getPredicate() throws TuplesException { return tuples.getColumnValue(offsetInRow + 1); } public long getSubject() throws TuplesException { return tuples.getColumnValue(offsetInRow); } public void beforeFirst() throws TuplesException { // Setting the offset past the end of the row forces a call to tuples.next() offsetInRow = wrappedRowLength; tuples.beforeFirst(); } public Object clone() { try { TuplesStatements cloned = (TuplesStatements)super.clone(); cloned.tuples = (Tuples)tuples.clone(); return cloned; } catch (CloneNotSupportedException e) { throw new Error("Clone ought to be supported."); } } public void close() throws TuplesException { tuples.close(); } public int getColumnIndex(Variable column) throws TuplesException { return variables.indexOf(column); } public int getNumberOfVariables() { return variables.size(); } public int getRowCardinality() throws TuplesException { int cardinality = tuples.getRowCardinality(); if (cardinality == Cursor.ONE && statementsPerRow > 1) { cardinality = Cursor.MANY; } return cardinality; } public boolean isEmpty() throws TuplesException { return tuples.isEmpty(); } public long getRowCount() throws TuplesException { if (statementsPerRow > 1) { BigInteger rowCount = BigInteger.valueOf(tuples.getRowCount()); rowCount = rowCount.multiply(BigInteger.valueOf(statementsPerRow)); return rowCount.bitLength() > 63 ? Long.MAX_VALUE : rowCount.longValue(); } return tuples.getRowCount(); } public long getRowUpperBound() throws TuplesException { if (statementsPerRow > 1) { BigInteger rowBound = BigInteger.valueOf(tuples.getRowUpperBound()); rowBound = rowBound.multiply(BigInteger.valueOf(statementsPerRow)); return rowBound.bitLength() > 63 ? Long.MAX_VALUE : rowBound.longValue(); } return tuples.getRowUpperBound(); } public long getRowExpectedCount() throws TuplesException { if (statementsPerRow > 1) { BigInteger rowExpected = BigInteger.valueOf(tuples.getRowExpectedCount()); rowExpected = rowExpected.multiply(BigInteger.valueOf(statementsPerRow)); return rowExpected.bitLength() > 63 ? Long.MAX_VALUE : rowExpected.longValue(); } return tuples.getRowUpperBound(); } public Variable[] getVariables() { return variables.toArray(new Variable[statementSize]); } public boolean isUnconstrained() throws TuplesException { return tuples.isUnconstrained(); } public boolean next() throws TuplesException { offsetInRow += statementSize; if (offsetInRow >= wrappedRowLength) { offsetInRow = 0; return tuples.next(); } return true; } } }