/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-06 The eXist Project
* http://exist-db.org
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* \$Id\$
*/
package org.exist.xquery.pragmas;
import org.apache.log4j.Logger;
import org.exist.Namespaces;
import org.exist.collections.Collection;
import org.exist.dom.NodeSet;
import org.exist.dom.QName;
import org.exist.indexing.StructuralIndex;
import org.exist.storage.QNameRangeIndexSpec;
import org.exist.xmldb.XmldbURI;
import org.exist.xquery.*;
import org.exist.xquery.functions.fn.ExtFulltext;
import org.exist.xquery.value.Item;
import org.exist.xquery.value.Sequence;
import org.exist.xquery.value.Type;
import java.util.Iterator;
import java.util.List;
public class Optimize extends Pragma {
public final static QName OPTIMIZE_PRAGMA = new QName("optimize", Namespaces.EXIST_NS, "exist");
private final static Logger LOG = Logger.getLogger(Optimize.class);
private boolean enabled = true;
private XQueryContext context;
private Optimizable optimizables[];
private Expression innerExpr = null;
private LocationStep contextStep = null;
private VariableReference contextVar = null;
private int contextId = Expression.NO_CONTEXT_ID;
private NodeSet cachedContext = null;
private int cachedTimestamp;
private boolean cachedOptimize;
public Optimize(XQueryContext context, QName pragmaName, String contents, boolean explicit) throws XPathException {
super(pragmaName, contents);
this.context = context;
this.enabled = explicit || context.optimizationsEnabled();
if (contents != null && contents.length() > 0) {
final String param[] = Option.parseKeyValuePair(contents);
if (param == null)
{throw new XPathException("Invalid content found for pragma exist:optimize: " + contents);}
if ("enable".equals(param[0])) {
enabled = "yes".equals(param[1]);
}
}
}
public void analyze(AnalyzeContextInfo contextInfo) throws XPathException {
super.analyze(contextInfo);
this.contextId = contextInfo.getContextId();
}
public Sequence eval(Sequence contextSequence, Item contextItem) throws XPathException {
if (contextItem != null)
{contextSequence = contextItem.toSequence();}
boolean useCached = false;
boolean optimize = false;
NodeSet originalContext = null;
if (contextSequence == null || contextSequence.isPersistentSet()) { // don't try to optimize in-memory node sets!
// contextSequence will be overwritten
originalContext = contextSequence == null ? null : contextSequence.toNodeSet();
if (cachedContext != null && cachedContext == originalContext)
{useCached = !originalContext.hasChanged(cachedTimestamp);}
if (contextVar != null) {
contextSequence = contextVar.eval(contextSequence);
}
// check if all Optimizable expressions signal that they can indeed optimize
// in the current context
if (useCached)
{optimize = cachedOptimize;}
else {
if (optimizables != null && optimizables.length > 0) {
for (int i = 0; i < optimizables.length; i++) {
if (optimizables[i].canOptimize(contextSequence))
{optimize = true;}
else {
optimize = false;
break;
}
}
}
}
}
if (optimize) {
cachedContext = originalContext;
cachedTimestamp = originalContext == null ? 0 : originalContext.getState();
cachedOptimize = true;
NodeSet ancestors;
NodeSet result = null;
for (int current = 0; current < optimizables.length; current++) {
NodeSet selection = optimizables[current].preSelect(contextSequence, current > 0);
if (LOG.isTraceEnabled())
{LOG.trace("exist:optimize: pre-selection: " + selection.getLength());}
// determine the set of potential ancestors for which the predicate has to
// be re-evaluated to filter out wrong matches
if (selection.isEmpty())
{ancestors = selection;}
else if (contextStep == null || current > 0) {
ancestors = selection.selectAncestorDescendant(contextSequence.toNodeSet(), NodeSet.ANCESTOR,
true, contextId, true);
} else {
// NodeSelector selector;
final long start = System.currentTimeMillis();
// selector = new AncestorSelector(selection, contextId, true, false);
final StructuralIndex index = context.getBroker().getStructuralIndex();
final QName ancestorQN = contextStep.getTest().getName();
if (optimizables[current].optimizeOnSelf()) {
ancestors = index.findAncestorsByTagName(ancestorQN.getNameType(), ancestorQN, Constants.SELF_AXIS,
selection.getDocumentSet(), selection, contextId);
} else {
ancestors = index.findAncestorsByTagName(ancestorQN.getNameType(), ancestorQN,
optimizables[current].optimizeOnChild() ? Constants.PARENT_AXIS : Constants.ANCESTOR_SELF_AXIS,
selection.getDocumentSet(), selection, contextId);
}
if (LOG.isTraceEnabled()) {
LOG.trace("Ancestor selection took " + (System.currentTimeMillis() - start));
LOG.trace("Found: " + ancestors.getLength());
}
}
result = ancestors;
contextSequence = result;
}
if (contextStep == null) {
return innerExpr.eval(result);
} else {
contextStep.setPreloadedData(result.getDocumentSet(), result);
if (LOG.isTraceEnabled())
{LOG.trace("exist:optimize: context after optimize: " + result.getLength());}
final long start = System.currentTimeMillis();
if (originalContext != null)
{contextSequence = originalContext.filterDocuments(result);}
else
{contextSequence = null;}
final Sequence seq = innerExpr.eval(contextSequence);
if (LOG.isTraceEnabled())
{LOG.trace("exist:optimize: inner expr took " + (System.currentTimeMillis() - start) +
"; found: "+ seq.getItemCount());}
return seq;
}
} else {
if (LOG.isTraceEnabled())
{LOG.trace("exist:optimize: Cannot optimize expression.");}
if (originalContext != null)
{contextSequence = originalContext;}
return innerExpr.eval(contextSequence, contextItem);
}
}
public void before(XQueryContext context, Expression expression, Sequence contextSequence) throws XPathException {
if (innerExpr != null)
{return;}
innerExpr = expression;
if (!enabled)
{return;}
innerExpr.accept(new BasicExpressionVisitor() {
public void visitPathExpr(PathExpr expression) {
for (int i = 0; i < expression.getLength(); i++) {
final Expression next = expression.getExpression(i);
next.accept(this);
}
}
public void visitLocationStep(LocationStep locationStep) {
final List<Predicate> predicates = locationStep.getPredicates();
for (final Predicate pred : predicates) {
pred.accept(this);
}
}
public void visitFilteredExpr(FilteredExpression filtered) {
final Expression filteredExpr = filtered.getExpression();
if (filteredExpr instanceof VariableReference)
{contextVar = (VariableReference) filteredExpr;}
final List<Predicate> predicates = filtered.getPredicates();
for (final Predicate pred : predicates) {
pred.accept(this);
}
}
public void visit(Expression expression) {
super.visit(expression);
}
public void visitFtExpression(ExtFulltext fulltext) {
if (LOG.isTraceEnabled())
{LOG.trace("exist:optimize: found optimizable: " + fulltext.getClass().getName());}
addOptimizable(fulltext);
}
public void visitGeneralComparison(GeneralComparison comparison) {
if (LOG.isTraceEnabled())
{LOG.trace("exist:optimize: found optimizable: " + comparison.getClass().getName());}
addOptimizable(comparison);
}
public void visitPredicate(Predicate predicate) {
predicate.getExpression(0).accept(this);
}
public void visitBuiltinFunction(Function function) {
if (function instanceof Optimizable) {
if (LOG.isTraceEnabled())
{LOG.trace("exist:optimize: found optimizable function: " + function.getClass().getName());}
addOptimizable((Optimizable) function);
}
}
});
contextStep = BasicExpressionVisitor.findFirstStep(innerExpr);
if (contextStep != null && contextStep.getTest().isWildcardTest())
{contextStep = null;}
if (LOG.isTraceEnabled()) {
LOG.trace("exist:optimize: context step: " + contextStep);
LOG.trace("exist:optimize: context var: " + contextVar);
}
}
public void after(XQueryContext context, Expression expression) throws XPathException {
}
private void addOptimizable(Optimizable optimizable) {final int axis = optimizable.getOptimizeAxis();
if (!(axis == Constants.CHILD_AXIS || axis == Constants.SELF_AXIS || axis == Constants.DESCENDANT_AXIS ||
axis == Constants.DESCENDANT_SELF_AXIS || axis == Constants.ATTRIBUTE_AXIS ||
axis == Constants.DESCENDANT_ATTRIBUTE_AXIS)) {
// reverse axes cannot be optimized
return;
}
if (optimizables == null) {
optimizables = new Optimizable[1];
optimizables[0] = optimizable;
} else {
Optimizable o[] = new Optimizable[optimizables.length + 1];
System.arraycopy(optimizables, 0, o, 0, optimizables.length);
o[optimizables.length] = optimizable;
optimizables = o;
}
}
public void resetState(boolean postOptimization) {
super.resetState(postOptimization);
cachedContext = null;
}
/**
* Check every collection in the context sequence for an existing range index by QName.
*
* @param contextSequence
* @return the type of a usable index or {@link org.exist.xquery.value.Type#ITEM} if there is no common
* index.
*/
public static int getQNameIndexType(XQueryContext context, Sequence contextSequence, QName qname) {
if (contextSequence == null || qname == null)
{return Type.ITEM;}
final String enforceIndexUse =
(String) context.getBroker().getConfiguration().getProperty(XQueryContext.PROPERTY_ENFORCE_INDEX_USE);
int indexType = Type.ITEM;
for (final Iterator<Collection> i = contextSequence.getCollectionIterator(); i.hasNext(); ) {
final Collection collection = i.next();
if (collection.getURI().startsWith(XmldbURI.SYSTEM_COLLECTION_URI))
{continue;}
final QNameRangeIndexSpec config = collection.getIndexByQNameConfiguration(context.getBroker(), qname);
if (config == null) {
// no index found for this collection
if (LOG.isTraceEnabled())
{LOG.trace("Cannot optimize: collection " + collection.getURI() + " does not define an index " +
"on " + qname);}
// if enfoceIndexUse == "always", continue to check other collections
// for indexes. It is sufficient if one collection defines an index
if (enforceIndexUse == null || !"always".equals(enforceIndexUse))
{return Type.ITEM;} // found a collection without index
} else {
int type = config.getType();
if (indexType == Type.ITEM) {
indexType = type;
// if enforceIndexUse == "always", it is sufficient if one collection
// defines an index. Just return it.
if (enforceIndexUse != null && "always".equals(enforceIndexUse))
{return indexType;}
} else if (indexType != type) {
// found an index with a bad type. cannot optimize.
// TODO: should this continue checking other collections?
if (LOG.isTraceEnabled())
{LOG.trace("Cannot optimize: collection " + collection.getURI() + " does not define an index " +
"with the required type " + Type.getTypeName(type) + " on " + qname);}
return Type.ITEM; // found a collection with a different type
}
}
}
return indexType;
}
}