package org.exist.xquery.modules.sort;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.exist.EXistException;
import org.exist.dom.QName;
import org.exist.dom.persistent.NodeProxy;
import org.exist.indexing.sort.SortIndex;
import org.exist.indexing.sort.SortIndexWorker;
import org.exist.indexing.sort.SortItem;
import org.exist.util.FastQSort;
import org.exist.util.LockException;
import org.exist.xquery.*;
import org.exist.xquery.value.*;
import org.w3c.dom.Element;
import java.util.ArrayList;
import java.util.List;
public class CreateOrderIndex extends BasicFunction {
public final static FunctionSignature[] signatures = {
new FunctionSignature(
new QName("create-index", SortModule.NAMESPACE_URI, SortModule.PREFIX),
"Create a sort index to be used within an 'order by' expression.",
new SequenceType[]{
new FunctionParameterSequenceType("id", Type.STRING, Cardinality.EXACTLY_ONE,
"The id by which the index will be known and distinguished from other indexes " +
"on the same nodes."),
new FunctionParameterSequenceType("nodes", Type.NODE, Cardinality.ZERO_OR_MORE,
"The node set to be indexed."),
new FunctionParameterSequenceType("values", Type.ATOMIC, Cardinality.ZERO_OR_MORE,
"The values to be indexed. There should be one value for each node in $nodes. " +
"$values thus needs to contain as many items as $nodes. If not, a dynamic error " +
"is triggered."),
new FunctionParameterSequenceType("options", Type.ELEMENT, Cardinality.ZERO_OR_ONE,
"<options order='ascending|descending' empty='least|greatest'/>")
},
new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, "")),
new FunctionSignature(
new QName("create-index-callback", SortModule.NAMESPACE_URI, SortModule.PREFIX),
"Create a sort index to be used within an 'order by' expression.",
new SequenceType[]{
new FunctionParameterSequenceType("id", Type.STRING, Cardinality.EXACTLY_ONE,
"The id by which the index will be known and distinguished from other indexes " +
"on the same nodes."),
new FunctionParameterSequenceType("nodes", Type.NODE, Cardinality.ZERO_OR_MORE,
"The node set to be indexed."),
new FunctionParameterSequenceType("callback", Type.FUNCTION_REFERENCE, Cardinality.EXACTLY_ONE,
"A callback function which will be called for every node in the $nodes input set. " +
"The function receives the current node as single argument and should return " +
"an atomic value by which the node will be sorted."),
new FunctionParameterSequenceType("options", Type.ELEMENT, Cardinality.ZERO_OR_ONE,
"<options order='ascending|descending' empty='least|greatest'/>")
},
new FunctionReturnSequenceType(Type.ITEM, Cardinality.ZERO_OR_MORE, ""))
};
protected static final Logger LOG = LogManager.getLogger(CreateOrderIndex.class);
private boolean descending = false;
private boolean emptyLeast = false;
public CreateOrderIndex(final XQueryContext context, final FunctionSignature signature) {
super(context, signature);
}
@Override
public Sequence eval(final Sequence[] args, final Sequence contextSequence) throws XPathException {
if (args[1].isEmpty())
return Sequence.EMPTY_SEQUENCE;
final String id = args[0].getStringValue();
// check how the function was called and prepare callback
FunctionReference call = null;
if (isCalledAs("create-index-callback")) {
call = (FunctionReference) args[2].itemAt(0);
} else if (args[2].getItemCount() != args[1].getItemCount())
throw new XPathException(this, "$nodes and $values sequences need to have the same length.");
// options
if (args[3].getItemCount() > 0) {
final NodeValue optionValue = (NodeValue) args[3].itemAt(0);
final Element options = (Element) optionValue.getNode();
String option = options.getAttribute("order");
if (option != null) {
descending = option.equalsIgnoreCase("descending");
}
option = options.getAttribute("empty");
if (option != null) {
emptyLeast = option.equalsIgnoreCase("least");
}
}
// create the input list to be sorted below
final List<SortItem> items = new ArrayList<>(args[1].getItemCount());
final Sequence[] params = new Sequence[1];
SequenceIterator valuesIter = null;
if (call == null)
valuesIter = args[2].iterate();
int c = 0;
final int len = args[1].getItemCount();
final int logChunk = 1 + (len / 20);
for (final SequenceIterator nodesIter = args[1].iterate(); nodesIter.hasNext(); ) {
final NodeValue nv = (NodeValue) nodesIter.nextItem();
if (nv.getImplementationType() == NodeValue.IN_MEMORY_NODE)
throw new XPathException(this, "Cannot create order-index on an in-memory node");
final NodeProxy node = (NodeProxy) nv;
final SortItem si = new SortItemImpl(node);
if (LOG.isDebugEnabled() && ++c % logChunk == 0) {
LOG.debug("Storing item " + c + " out of " + len + " to sort index.");
}
if (call != null) {
// call the callback function to get value
params[0] = node;
final Sequence r = call.evalFunction(contextSequence, null, params);
if (!r.isEmpty()) {
AtomicValue v = r.itemAt(0).atomize();
if (v.getType() == Type.UNTYPED_ATOMIC)
v = v.convertTo(Type.STRING);
si.setValue(v);
}
} else {
// no callback, take value from second sequence
AtomicValue v = valuesIter.nextItem().atomize();
if (v.getType() == Type.UNTYPED_ATOMIC)
v = v.convertTo(Type.STRING);
si.setValue(v);
}
items.add(si);
}
// sort the set
FastQSort.sort(items, 0, items.size() - 1);
// create the index
final SortIndexWorker index = (SortIndexWorker)
context.getBroker().getIndexController().getWorkerByIndexId(SortIndex.ID);
try {
index.createIndex(id, items);
} catch (final EXistException e) {
throw new XPathException(this, e.getMessage(), e);
} catch (final LockException e) {
throw new XPathException(this, "Caught lock error while creating index. Giving up.", e);
}
return Sequence.EMPTY_SEQUENCE;
}
private class SortItemImpl implements SortItem {
NodeProxy node;
AtomicValue value = AtomicValue.EMPTY_VALUE;
public SortItemImpl(final NodeProxy node) {
this.node = node;
}
public NodeProxy getNode() {
return node;
}
public AtomicValue getValue() {
return value;
}
public void setValue(final AtomicValue value) {
if (value.hasOne())
this.value = value;
}
public int compareTo(final SortItem other) {
int cmp = 0;
final AtomicValue a = this.value;
final AtomicValue b = other.getValue();
final boolean aIsEmpty = (a.isEmpty() || (Type.subTypeOf(a.getType(), Type.NUMBER) && ((NumericValue) a).isNaN()));
final boolean bIsEmpty = (b.isEmpty() || (Type.subTypeOf(b.getType(), Type.NUMBER) && ((NumericValue) b).isNaN()));
if (aIsEmpty) {
if (bIsEmpty)
// both values are empty
return Constants.EQUAL;
else if (emptyLeast)
cmp = Constants.INFERIOR;
else
cmp = Constants.SUPERIOR;
} else if (bIsEmpty) {
// we don't need to check for equality since we know a is not empty
if (emptyLeast)
cmp = Constants.SUPERIOR;
else
cmp = Constants.INFERIOR;
} else if (a == AtomicValue.EMPTY_VALUE && b != AtomicValue.EMPTY_VALUE) {
if (emptyLeast)
cmp = Constants.INFERIOR;
else
cmp = Constants.SUPERIOR;
} else if (b == AtomicValue.EMPTY_VALUE && a != AtomicValue.EMPTY_VALUE) {
if (emptyLeast)
cmp = Constants.SUPERIOR;
else
cmp = Constants.INFERIOR;
} else
cmp = a.compareTo(b);
if (descending)
cmp = cmp * -1;
return cmp;
}
}
}