/* * 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.store.tuples; // Java 2 standard packages import java.util.*; // Third party packages import org.apache.log4j.*; // Locally written packages import org.mulgara.query.Constraint; import org.mulgara.query.QueryException; import org.mulgara.query.TuplesException; import org.mulgara.query.Variable; import org.mulgara.query.filter.Context; import org.mulgara.query.filter.ContextOwner; import org.mulgara.query.filter.Filter; import org.mulgara.resolver.spi.QueryEvaluationContext; import org.mulgara.resolver.spi.TuplesContext; import org.mulgara.store.tuples.AbstractTuples; /** * Left Filtering operation. * * This operation is used for the case where an OPTIONAL join is being filtered * based on variables that appear in both the LHS and the RHS of the OPTIONAL. * * According to SPARQL: * Diff(Ω1, Ω2, expr) = { μ | μ in Ω1 such that for all μ′ in Ω2, * either μ and μ′ are not compatible or μ and μ' are compatible and * expr(merge(μ, μ')) has an effective boolean value of false } * http://www.w3.org/TR/rdf-sparql-query/#defn_algDiff * * In this case, no variables are common, which simplifies our situation to being * compatible (since all rows are compatible when no variables are shared). So * we just need every μ from Ω1 where every μ' yields an expression of false. * The join is performed by iterating over the lhs, and searching on the * RHS for true rows. If one is found, then the lhs must iterate again. * * @created 2009-12-18 * @author <a href="mailto:pgearon@users.sourceforge.net">Paula Gearon</a> */ public class LeftFiltered extends AbstractTuples implements ContextOwner { private static final Logger logger = Logger.getLogger(LeftFiltered.class.getName()); /** The set of tuples to return all row from. */ protected Tuples lhs; /** The set of tuples to add to the lhs. */ protected Tuples rhs; /** The filter to apply. */ private Filter filter; /** The tuples context */ protected TuplesContext context = null; /** A list of context owners that this owner provides the context for. */ private List<ContextOwner> contextListeners = new ArrayList<ContextOwner>(); /** A collection of the variables on the LHS */ private ArrayList<Variable> lhsVars; /** The offset for indexing into the RHS, while avoiding LHS variables */ private int rhsOffset; /** Indicates that the current row is OK, and {@link #next()} will return true. */ private boolean currentRowValid = false; /** * Configure a filtering join on the left hand side. * * @param lhs The original tuples, including the rows to be removed. * @param rhs The tuples to be joined in cross product for testing. * @param filter The filter that must return FALSE for everything in order to have a LHS row returned. * @throws IllegalArgumentException If the <var>lhs</var> and <var>rhs</var> * contain variables in common. */ @SuppressWarnings("unchecked") LeftFiltered(Tuples lhs, Tuples rhs, Filter filter, QueryEvaluationContext queryContext) throws TuplesException, IllegalArgumentException { if (logger.isDebugEnabled()) { logger.debug("Filtering " + lhs + " by " + rhs + " with expression=" + filter); } // store the operands this.lhs = (Tuples)lhs.clone(); this.rhs = (Tuples)rhs.clone(); this.filter = filter; if (this.filter == null || filter.getVariables().size() == 0) throw new IllegalArgumentException("No need to filter on unfiltered data"); this.context = new TuplesContext(this, queryContext.getResolverSession()); this.filter.setContextOwner(this); // get the variables to merge on Set<Variable> commonVars = Collections.unmodifiableSet((Set<Variable>)TuplesOperations.getMatchingVars(lhs, rhs)); // This is more common than we expected, so just log a debug message if (!commonVars.isEmpty()) throw new IllegalArgumentException("Cannot left filter when data has non-trivial compatability."); // set the variables for this optional conjunction lhsVars = new ArrayList<Variable>(Arrays.asList(lhs.getVariables())); ArrayList<Variable> vars = (ArrayList<Variable>)lhsVars.clone(); ArrayList<Variable> rhsVars = new ArrayList<Variable>(Arrays.asList(rhs.getVariables())); vars.addAll(rhsVars); setVariables(vars); // set the column offset for indexing into the RHS rhsOffset = lhsVars.size(); assert rhsOffset > 0; } // // Methods implementing Tuples // /** {@inheritDoc} */ public long getColumnValue(int column) throws TuplesException { int nrLeftVars = lhs.getNumberOfVariables(); return (column < nrLeftVars) ? lhs.getColumnValue(column) : UNBOUND; } /** {@inheritDoc} */ public long getRawColumnValue(int column) throws TuplesException { int nrLeftVars = lhs.getNumberOfVariables(); if (column < nrLeftVars) return lhs.getColumnValue(column); return rhs.getColumnValue(column - rhsOffset); } /** {@inheritDoc} */ public long getRowUpperBound() throws TuplesException { return lhs.getRowUpperBound(); } /** {@inheritDoc} */ public long getRowExpectedCount() throws TuplesException { // TODO: work out a better expected value. Maybe add about 10% return lhs.getRowExpectedCount(); } /** {@inheritDoc} */ public boolean isEmpty() throws TuplesException { return lhs.isEmpty(); } /** {@inheritDoc} Relies on the lhs of the optional. */ public boolean isColumnEverUnbound(int column) throws TuplesException { int nrLeftVars = lhs.getNumberOfVariables(); return (column >= nrLeftVars) || lhs.isColumnEverUnbound(column); } /** {@inheritDoc} */ public int getColumnIndex(Variable variable) throws TuplesException { if (lhsVars.contains(variable)) return lhs.getColumnIndex(variable); return rhs.getColumnIndex(variable) + rhsOffset; } /** * {@inheritDoc} * @return Always <code>false</code>. */ public boolean isMaterialized() { return false; } /** * {@inheritDoc} */ public boolean hasNoDuplicates() throws TuplesException { return lhs.hasNoDuplicates(); } /** {@inheritDoc} */ public RowComparator getComparator() { return lhs.getComparator(); } /** {@inheritDoc} */ public List<Tuples> getOperands() { return Collections.unmodifiableList(Arrays.asList(new Tuples[] {lhs, rhs})); } /** {@inheritDoc} */ public boolean isUnconstrained() throws TuplesException { return lhs.isUnconstrained(); } /** {@inheritDoc} */ public void renameVariables(Constraint constraint) { lhs.renameVariables(constraint); rhs.renameVariables(constraint); } /** * {@inheritDoc} * This method matches what it can on the LHS, and saves the rest for later searches * on the RHS. Searches on the RHS only happen when the LHS iterates to valid data. */ public void beforeFirst(long[] prefix, int suffixTruncation) throws TuplesException { int lhsVars = lhs.getNumberOfVariables(); int tailLen = prefix.length - lhsVars; if (tailLen <= 0) { // search on the LHS only lhs.beforeFirst(prefix, suffixTruncation); } else { // looking for something that doesn't exist lhs.beforeFirst(new long[] {-1}, suffixTruncation); } currentRowValid = false; } /** {@inheritDoc} */ public boolean next() throws TuplesException { while ((currentRowValid = lhs.next())) { if (!testRhs()) break; } return currentRowValid; } /** * Tests if any row on the right is true * @return true if any row on the right evaluates to true * @throws TuplesException If the RHS cannot be tested. */ private boolean testRhs() throws TuplesException { rhs.beforeFirst(); while (rhs.next()) { if (testFilter()) return true; } return false; } /** * Tests a filter using the current context. * @return The test result. * @throws QueryException If there was an error accessing data needed for the test. */ private boolean testFilter() { // re-root the filter expression to this Tuples filter.setContextOwner(this); try { return filter.test(context); } catch (QueryException qe) { return false; } } /** * Closes all the operands. * @throws TuplesException If either the lhs or the rhs can't be closed. */ public void close() throws TuplesException { lhs.close(); rhs.close(); } /** * @return {@inheritDoc} */ public Object clone() { LeftFiltered cloned = (LeftFiltered)super.clone(); // Copy mutable fields by value cloned.lhs = (Tuples)lhs.clone(); cloned.rhs = (Tuples)rhs.clone(); cloned.context = (context == null) ? null : new TuplesContext(cloned, context); if (cloned.filter == null) throw new IllegalStateException("Unexpectedly lost a filter: " + filter); return cloned; } /** * Tells a filter what the current context is. * @see org.mulgara.query.filter.ContextOwner#getCurrentContext() */ public Context getCurrentContext() { return context; } /** * Allows the context to be set manually. This is not expected. * @see org.mulgara.query.filter.ContextOwner#setCurrentContext(org.mulgara.query.filter.Context) */ public void setCurrentContext(Context context) { if (!(context instanceof TuplesContext)) throw new IllegalArgumentException("LeftJoin can only accept a TuplesContext."); this.context = (TuplesContext)context; for (ContextOwner l: contextListeners) l.setCurrentContext(context); } /** * This provides a context, and does not need to refer to a parent. * @see org.mulgara.query.filter.ContextOwner#getContextOwner() */ public ContextOwner getContextOwner() { throw new IllegalStateException("Should never be asking for the context owner of a Tuples"); } /** * The owner of the context for a Tuples is never needed, since it is always provided by the Tuples. * @see org.mulgara.query.filter.ContextOwner#setContextOwner(org.mulgara.query.filter.ContextOwner) */ public void setContextOwner(ContextOwner owner) { } /** * Adds a context owner as a listener so that it will be updated with its context * when this owner gets updated. * @param l The context owner to register. */ public void addContextListener(ContextOwner l) { contextListeners.add(l); } }